Policy Guide
How MAP evaluates actions, how to write rules, and how to change them at runtime.
Policy decision actions
Every action is evaluated to exactly one of three outcomes:
| Action | Description | Next step |
|---|---|---|
allow | Task can proceed immediately | Execute task |
deny | Task is rejected | Return error to requester |
require_approval | Task requires human approval | Await approval via /approve |
The rule DSL
The fastest way to write policy is the flat rule list. Each rule matches a capability pattern, optional conditions, and a required outcome:
const agent = map({
policy: [
// Payments: require approval above $1000
{ when: 'payment.*', amount_gt: 1000, require: 'approval' },
// Database: block all writes to production
{ when: 'db.write', env: 'production', require: 'deny' },
// Infrastructure: always require approval in production
{ when: 'aws.*', env: 'production', require: 'approval' },
{ when: 'k8s.*', env: 'production', require: 'approval' },
// Critical risk: always require approval regardless of capability
{ when: '*', risk: 'critical', require: 'approval' },
// Services: block non-service requesters from internal APIs
{ when: 'internal.*', requester_type: 'user', require: 'deny' },
// Everything else: allow
{ when: '*', require: 'allow' },
],
});The full policy document
For advanced use, policy is a versioned JSON document with named rules and structured conditions:
{
"version": "1.0",
"rules": [
{
"id": "high-value-payment",
"capability": "payment.*",
"condition": { "gt": ["input.amount", 1000] },
"action": "require_approval"
},
{
"id": "production-db-write",
"capability": "db.write",
"condition": { "eq": ["constraints.environment", "production"] },
"action": "deny"
}
]
}Load it from a file:
const agent = map({ policy: './policy.json' });Hot-swap at runtime
Policy changes never require a restart:
// Instant lockdown
agent.setPolicy([
{ when: '*', require: 'deny' }
]);Over HTTP against a running server:
curl -X POST http://localhost:8787/policy -d @policy.jsonCheck without executing
Preview what policy would decide without running the action:
const check = agent.check('payment.execute', { amount: 5000 });
// { action: 'require_approval', reason: 'Rule: high-value-payment' }What policies evaluate against
The engine sees four inputs on every dispatch:
- Requester identity - type (user/service/agent), id, tenant
- Task constraints - common and domain-specific constraint objects
- Agent descriptor - risk level, domain, capabilities
- Environment - development, staging, production
The default policy engine
The
map()DSL, theExecutor, and the built-in adapters live in the reference tree (src/). The published npm package exposes policy through thePolicyEngineclass below. Both evaluate the same way: match rules, apply the most severe outcome.
The reference map() helper compiles the flat DSL into full policy documents internally - { always: true } for catch-all rules, { and: [...] } for multi-condition rules, and require_approval for require: 'approval'.
import { PolicyEngine, PolicyEffect } from '@sidianlabs/map';
const engine = new PolicyEngine();
// Highest priority wins; DENY and DENY_WITH_REASON stop evaluation
engine.addRule({
id: 'no-prod-db-writes',
name: 'Block production DB writes',
target: { capability: 'db.write' },
condition: {
operator: 'eq',
field: 'constraints.environment',
value: 'production',
},
effect: PolicyEffect.DENY,
reason: 'Writes to production databases are forbidden',
priority: 100,
});
// CHALLENGE routes to human approval with named approver slots
engine.addRule({
id: 'big-payments',
name: 'Approve large payments',
target: { capability: 'payment.execute' },
condition: { operator: 'gt', field: 'input.amount', value: 1000 },
effect: PolicyEffect.CHALLENGE,
priority: 90,
});
const result = engine.evaluate(envelope, {
requester,
target_agent: 'agent-payment',
capability: 'payment.execute',
risk_class: 'high',
constraints: {},
});
// result.effect, result.reason, result.required_approvals, result.policy_logsCondition operators: and, or, not, eq, neq, gt, lt, gte, lte, in, contains.
Prefer a risk-first posture? createRiskBasedPolicy() builds a ready-made engine where high and critical risk tasks require human approval:
import { createRiskBasedPolicy } from '@sidianlabs/map';
const engine = createRiskBasedPolicy();Custom policy engines
Wrap the engine or compose constraints for anything the rule list cannot express:
import { evaluateTaskConstraints } from '@sidianlabs/map';
const verdict = evaluateTaskConstraints(envelope, envelope.constraints.common ?? {});
// { valid: boolean; errors: string[] }Built-in adapters
Adapters ship in the reference tree (
src/adapters/) alongside themap()helper - clone the repo to use them. They are not part of the published npm package surface.
import { map } from './src/map.js';
import { HttpAdapter } from './src/adapters/http-adapter.js';
import { PaymentExecuteAdapter } from './src/adapters/payment-adapter.js';
import { DbReadAdapter } from './src/adapters/db-read-adapter.js';
const agent = map({ policy: [...] });
// HTTP requests (SSRF protection built in)
agent.can('http.request', new HttpAdapter());
// Stripe-compatible payments (plus PaymentRefundAdapter for refunds)
agent.can('payment.execute', new PaymentExecuteAdapter());
// Database reads (SELECT-only; summary, structured, or count_only output modes)
agent.can('db.read', new DbReadAdapter());Or register any function as a capability:
agent.can('crm.update', async (input, context) => {
await salesforce.update(input.record_id, input.fields);
return { updated: true, record_id: input.record_id };
});