Docs menu
Data-driven guard/policy
Produces allow/deny/require-approval guard decisions from the __policy__ document in the journal — rule changes need no code deploy.
What it's for / when to use it#
Guard, is a general policy hook that runs before every tool call (see human-in-the-loop tool approvals). A hand-written guard function requires a redeploy for every rule change. If you'd rather move rules out of code and into data — e.g. if an operator needs to apply the decision "the send_email tool should now require approval" instantly from Studio — policyGuard is built exactly for this. Rules are stored in the journal under the __policy__ key; policyGuard reads this document live on every call, so an update made from Studio takes effect immediately on the next tool call.
Setup / import#
import { policyGuard } from '@gnldev/durable';There's no separate sub-package path — policyGuard, evaluatePolicy and POLICY_KEY come directly from the @gnldev/durable root export.
Step-by-step usage#
1. Create the guard by wiring it to the journal and pass it to runDurable:
import { policyGuard } from '@gnldev/durable';
const guard = policyGuard(journal, { fallback: 'allow' });
await runDurable({ runId, journal, model, tools, guard, prompt });fallbackis the default decision applied when the journal has no rules at all (or no rule matches) — if you leave the default as 'allow' , behavior doesn't change even while the document is empty and policyGuard is wired in; rules take effect as they're added from Studio.
2. Edit rules live from Studio (requires a writable journal + operator authority) — GET/PUT /policy:
PUT /policy
{
"rules": [
{ "tool": "send_email", "action": "require-approval", "reason": "external communication" },
{ "tool": "delete_record", "action": "deny", "reason": "irreversible" },
{ "tool": "*", "action": "allow" }
]
}Every PUT increments the version field of the journal's document and is written to the audit log as policy.update (with the full rule set) — past versions can be read back from the audit trail.
'*' wildcard; if neither matches, the fallback is applied. That's why putting the wildcard rule at the end of the list (as in the example above) is recommended for readability, but it doesn't affect match order.API reference#
policyGuardTakes a journal and an optional { key, fallback }; returns a Guard that reads the __policy__ document on every tool call and produces a GuardDecision.
evaluatePolicyPure evaluation function: (doc, toolName, fallback) → GuardDecision. Decides in order: exact match, then wildcard, then fallback.
POLICY_KEYThe key of the policy document in the journal ("__policy__") — invisible to parseJournalKey, a special system key.
PolicyRule{ tool: string, action: 'allow' | 'deny' | 'require-approval', reason?: string } — tool name can be an exact match or '*'.
PolicyDoc{ version: number, rules: PolicyRule[], updatedAt?: number } — every PUT increments the version number.
PUT /policy updates ONE GLOBAL rule set for ALL orgs (org-unscoped) — so an org-bound identity cannot write to this endpoint, only an unscoped operator can update it; otherwise it returns a 403.