paybondpaybond
Sign in

Policy hot-reload for long-lived agents

Reload paybond.policy.yaml at runtime without re-binding intents — file watch, Gateway poll, CLI reload, and safe in-flight semantics.

Agent policy-as-code loads policy once at PaybondAgentRun.bind() by default. Long-lived processes — daemon workers, persistent LangGraph graphs, MCP servers — need to pick up policy changes without restarting the run or re-bootstrapping intents.

Hot-reload adds versioned policy snapshots and safe reload semantics on top of the existing registry and interceptor pipeline.

When you need hot-reload

ScenarioWithout reloadWith hot-reload
Edit paybond.policy.yaml on diskNo effect until re-bindFile watcher or manual reload applies changes
Org publishes new base policyTenants stale until restartGateway poll reloads effective inherited policy
Stricter cap mid-runOld cap until re-bindNew cap applies to subsequent tool calls
In-flight tool call during reloadN/ACompletes under policy digest active at authorize time

Prefer hot-reload when your agent process stays alive across many tool calls and you want GitOps-friendly policy updates without dropping the funded intent.

Policy snapshot model

Every loaded policy carries a versioned snapshot:

type PaybondPolicySnapshot = {
  digest: string;           // sha256 of canonical policy JSON
  version: string;          // policy name + digest short
  loadedAt: string;         // RFC3339
  source: "file" | "remote" | "effective";
  registry: PaybondToolRegistry;
};

PaybondAgentRun holds the active snapshot. Each authorization pins policyDigest for audit and evidence submission.

Reload triggers

1. File watcher (local dev / single-tenant deploy)

TypeScript:

const run = await paybond.agentRun.bind({
  policyFile: "./paybond.policy.yaml",
  registry: policy.toToolRegistry(),
  policySnapshot: snapshot,
  bootstrap: policy.sandboxBootstrap({ operation: "travel.book_hotel" }),
  reload: { watch: true, debounceMs: 500 },
});

Python:

run = await paybond.agent_run.bind({
    "policy_file": "./paybond.policy.yaml",
    "registry": registry,
    "policy_snapshot": snapshot,
    "bootstrap": policy.sandbox_bootstrap(operation="travel.book_hotel"),
    "reload": {"watch": True, "debounce_ms": 500},
})

On file change: parse → local validate → optional remote validate → atomic registry swap.

CLI bind records watch config for persisted runs:

paybond agent run bind --policy-file paybond.policy.yaml --watch --format json

Long-lived SDK processes should call bind() with reload.watch directly; the CLI persists reload.watch in .paybond/runs/<run_id>.json for status inspection.

2. Gateway poll (production / inherited policies)

For tenant overlays that extend an org base policy (enterprise policy inheritance):

reload: {
  poll: {
    intervalMs: 60_000,
    remote: true,
    resolveInheritance: true,
  },
},

Polls POST /v1/org-policies/{id}/effective?digest=current. When the digest differs, fetch → validate → swap.

3. Manual reload (CLI or SDK)

CLI for persisted runs:

paybond agent run reload-policy --run-id <id> --format json
paybond agent run reload-policy --run-id <id> --policy-file ./paybond.policy.yaml --remote --format json

Flags:

FlagPurpose
--policy-fileOverride policy path (defaults to path stored at bind)
--remoteRun Gateway POST /v1/policy/validate before applying
--resolve-inheritanceMerge org base server-side for v2 overlays
--allow-loosenPermit higher caps or new side-effecting tools (default deny)

SDK:

const result = await run.reloadPolicy({
  file: "./paybond.policy.yaml",
  remote: true,
});

run.on("policyReloaded", ({ previousDigest, newDigest }) => { /* ... */ });
run.on("policyReloadFailed", (error) => { /* ... */ });

Inspect reload state or middleware trace:

paybond agent run status --run-id <id> --format json
paybond agent run trace --run-id <id> --format table

Example status fields:

{
  "run_id": "...",
  "policy_digest": "sha256:...",
  "policy_loaded_at": "2026-06-29T12:00:00Z",
  "reload": { "watch": true, "last_reload_at": "2026-06-29T12:05:00Z" }
}

Safe reload semantics

Critical rules (fail-closed):

  1. In-flight tool calls pin the policy digest at authorize time; evidence submit uses the same digest.
  2. Reload during execute waits for in-flight interceptors to finish (ref count) before swap.
  3. Failed reload keeps the previous snapshot; emits structured error; no partial apply.
  4. Stricter policy applies immediately after swap to new tool calls.
  5. Loosening policy (higher caps, new side-effecting tools) requires --allow-loosen or allowLoosen: true; default deny in production.
  6. Intent scope — reload cannot change intent_id / capability_token. If reloaded policy requires tools outside the bound intent, reload fails with intent_rebind_required.

Structured error codes:

CodeMeaning
loosening_deniedPolicy relaxed caps or added side-effecting tools without approval
intent_rebind_requiredNew allowed_tools drift from funded intent
remote_validate_failedGateway rejected the policy document
invalid_overlayPoll reload requires v2 overlay with extends.org_policy_id

MCP servers

Long-lived MCP processes support env-driven reload. See MCP server:

export PAYBOND_POLICY_FILE="./paybond.policy.yaml"
export PAYBOND_POLICY_RELOAD="watch"   # watch | poll | off
export PAYBOND_POLICY_RELOAD_ALLOW_LOOSEN="0"

The MCP gate reloads the registry between tool invocations without restart.

Production checklist

  1. Bind with a policy file and track policy_digest in run status.
  2. Use --remote (CLI) or remote: true (SDK) before accepting reloads in production.
  3. For inherited policies, enable resolveInheritance on poll or manual reload.
  4. Keep loosening disabled unless an operator explicitly approves --allow-loosen.
  5. Monitor policyReloadFailed events; failed reloads never partial-apply.