GNL
Docs menu
Core · Free@gnldev/durable

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

agent side — wire the guard
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:

Studio — update a rule
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.

Match order
Rule matching is tried in order: first the exact tool name, then the '*' 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#

fnpolicyGuard

Takes a journal and an optional { key, fallback }; returns a Guard that reads the __policy__ document on every tool call and produces a GuardDecision.

fnevaluatePolicy

Pure evaluation function: (doc, toolName, fallback) → GuardDecision. Decides in order: exact match, then wildcard, then fallback.

constPOLICY_KEY

The key of the policy document in the journal ("__policy__") — invisible to parseJournalKey, a special system key.

typePolicyRule

{ tool: string, action: 'allow' | 'deny' | 'require-approval', reason?: string } — tool name can be an exact match or '*'.

typePolicyDoc

{ version: number, rules: PolicyRule[], updatedAt?: number } — every PUT increments the version number.

Scope
Studio's 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.