Skip to content

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:

ActionDescriptionNext step
allowTask can proceed immediatelyExecute task
denyTask is rejectedReturn error to requester
require_approvalTask requires human approvalAwait 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:

typescript
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:

json
{
  "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:

typescript
const agent = map({ policy: './policy.json' });

Hot-swap at runtime ​

Policy changes never require a restart:

typescript
// Instant lockdown
agent.setPolicy([
  { when: '*', require: 'deny' }
]);

Over HTTP against a running server:

bash
curl -X POST http://localhost:8787/policy -d @policy.json

Check without executing ​

Preview what policy would decide without running the action:

typescript
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:

  1. Requester identity - type (user/service/agent), id, tenant
  2. Task constraints - common and domain-specific constraint objects
  3. Agent descriptor - risk level, domain, capabilities
  4. Environment - development, staging, production

The default policy engine ​

The map() DSL, the Executor, and the built-in adapters live in the reference tree (src/). The published npm package exposes policy through the PolicyEngine class 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'.

typescript
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_logs

Condition 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:

typescript
import { createRiskBasedPolicy } from '@sidianlabs/map';

const engine = createRiskBasedPolicy();

Custom policy engines ​

Wrap the engine or compose constraints for anything the rule list cannot express:

typescript
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 the map() helper - clone the repo to use them. They are not part of the published npm package surface.

typescript
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:

typescript
agent.can('crm.update', async (input, context) => {
  await salesforce.update(input.record_id, input.fields);
  return { updated: true, record_id: input.record_id };
});

Released under the Apache 2.0 License.