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.