Maniac Docs
Permissions

Permission policies

PermissionPolicy interface, StaticPermissionPolicy rule lists, and custom inline evaluators.

Permission policies

A PermissionPolicy evaluates each PendingAction — a concrete tool call with principal, toolset, tool name, and serialized arguments — before the runner invokes the handler.

import type { PermissionPolicy, PendingAction, Decision } from "@maniac-ai/agents";

interface PermissionPolicy {
  evaluate(action: PendingAction): Decision | Promise<Decision>;
  evaluateMany?(actions: PendingAction[]): Decision[] | Promise<Decision[]>;
}

The optional evaluateMany hook lets HTTP- or database-backed policies answer batched sub-action fan-outs in a single round trip. The default evaluateMany in @maniac-ai/agents/permissions loops over evaluate.

StaticPermissionPolicy

Rule-list evaluator for most production setups. Import from @maniac-ai/agents or @maniac-ai/agents/permissions.

import { StaticPermissionPolicy } from "@maniac-ai/agents";

const policy = new StaticPermissionPolicy(
  [
    {
      id: "allow-readonly",
      principal: "*",
      scope: { toolset: "files", tool: "read_file", arg_constraints: [] },
      effect: "allow"
    },
    {
      id: "ask-destructive",
      principal: "*",
      scope: { toolset: "files", tool: "delete_file", arg_constraints: [] },
      effect: "require_approval"
    },
    {
      id: "deny-shell",
      principal: "*",
      scope: { toolset: "shell", arg_constraints: [] },
      effect: "deny"
    }
  ],
  { allowed: false, requires_approval: false } // default when no rule matches
);

Rule effects

EffectResult
allow{ allowed: true, requires_approval: false }
deny{ allowed: false, requires_approval: false }
require_approval{ allowed: false, requires_approval: true }
allow_unless_matchesAllow unless arg constraints match
deny_unless_matchesDeny unless arg constraints match

Rules are evaluated in order; the first match wins.

Matching dimensions

Each rule matches on:

  • principal — caller identity (often "*" or a user/session id)
  • scope.toolset — toolset id (e.g. "chat", "computer_use", "mcp:github")
  • scope.tool — individual tool name (optional wildcard)
  • arg_constraints — path grammar over serialized arguments

Arg constraint paths

resolvePath supports JSON-path-style selectors:

// Match any element in calls array
{ path: "calls[*].destination", operator: "equals", value: "prod" }

// Match header keys
{ path: 'headers["X-Env"]', operator: "equals", value: "production" }

See tests/permission-scope.test.ts in the package for exhaustive path-grammar examples.

Shipped convenience policies

ClassBehavior
AllowAllPolicyAlways { allowed: true, requires_approval: false }
DenyAllPolicyAlways { allowed: false, requires_approval: false }

Custom inline policies

For dynamic logic, implement PermissionPolicy directly:

const approvalPolicy: PermissionPolicy = {
  evaluate(action) {
    if (action.call.tool === "lookup") {
      return { allowed: true, requires_approval: false };
    }
    return {
      allowed: false,
      requires_approval: true,
      reason: `${action.call.tool} requires approval`
    };
  }
};

Wiring on Agent vs Maniac

// App-wide default
const app = new Maniac({ policy: appPolicy, agents: [...] });

// Per-agent override
app.agent({
  id: "support",
  instructions: "...",
  model,
  policy: stricterPolicy
});

Per-agent policy replaces the app default for that agent's runs (including nested sub-agent sessions that inherit the parent's policy unless overridden).

Evaluation order with guardrails

On the tool path the runner evaluates in this order:

  1. tool_middleware.beforeInvoke
  2. tool_guardrails.checkCall (guardrails)
  3. policy.evaluate (this page)
  4. Tool handler
  5. tool_guardrails.checkResult
  6. tool_middleware.afterInvoke

A guardrail require_approval decision routes through the same HITL pause path as a policy requires_approval decision.

On this page