Saphan StudioDocs
Security/Guardrails

Observe and enforce

The two modes, the signed owner act without which no stage enforces, and what the agent and the record see when a call is stopped.

The two modes

modewhat happens on block, error, a timeout or no answer
observe (default)the decision is recorded; the call goes through. Responses stream to the agent as they arrive
enforcethe call is stopped. The whole response — including a streamed one — is held until the plugin has decided, then released or stopped

Start in observe. Read the audit records until the plugin's decisions are the ones you want, then arm enforcement.

Enforcement needs a signed act

Setting a stage to enforce is only a request. A stage actually enforces only when the file named by guardrail.enforce_acts holds an act that:

  • is signed with the installation's root key, and
  • names this plugin (its id and its manifest's SHA-256), this stage, the actor and the time it was recorded.

The file is read where the stage runs: on the machine where the agent runs for the model stages, on the dispatching machine for spawn.handoff, and on the gateway's machine for bridge.ingress. For the model stages the path is the dispatching machine's setting, passed on unread: the acts file, the plugin manifest and the plugin executable must exist at those absolute paths on the agent's machine. Each of those machines must also hold the pinned root public key — ~/.saphan/trust/root.key.pub on a per-user install — to verify the act.

Produce and sign the act with:

saphan guardrail act --plugin-manifest <path> --plugin-digest <sha256> --stage <stage> \
  --actor <who> --recorded-at <RFC 3339 time>                    # print the act, to review
saphan guardrail act … --root-key <path to the root key>         # sign it
the acts fileresult
not configured, or holds no act for this plugin and stagethe stage observes, and the proxy says so when it starts: OBSERVES, and the configuration asked for ENFORCE … [guardrail-enforce-not-authorized]
configured but does not verify — a bad line, or no pinned root public key on that machinethe proxy does not start, and the handoff and ingress stages refuse. A broken acts file is never read as "observe"
holds a matching, verified actthe stage enforces

Three consequences are deliberate:

  • A configuration change alone cannot arm enforcement. Someone with the root key must sign it.
  • Re-pinning the plugin disarms it. The manifest digest is inside the signed act, so a new plugin version observes until a new act is signed.
  • A plugin cannot arm itself. A manifest that tries to carry a mode or an "enabled" switch is refused.

What the agent sees when a call is stopped

Every response the proxy writes itself carries the X-Saphan-Guardrail header (stop, or upstream-unreachable).

situationresponse
the plugin blocked the call (in enforce)403, guardrail_stop
the plugin failed, timed out or gave no decision (in enforce)502, guardrail_stop
the request could not be read, or its path is not one the proxy admits — in observe and enforce alike502, guardrail_stop
the model provider could not be reached502, guardrail_upstream_unreachable

The body is a fixed error object — stage, request id, mode, decision, reason code and rule id, with a fixed message. The plugin's own message is never passed to the agent.

What the record shows

Each decision of the model stages is written to the run's guardrail audit file, .saphan-guardrail/audit.jsonl in its working directory, one line per decision. A spawn.handoff decision goes onto the run record's enforcement note instead, and bridge.ingress decisions are written to .saphan/guardrail/bridge-ingress.jsonl in the workspace on the gateway's machine.

The audit line's fields:

fieldmeaning
stage, plugin id and version, modewhere and by what the decision was taken
decision, action, would_stopwhat the plugin said, what the proxy did, and whether enforce would have stopped it
rule id, reason codethe plugin's reason
plugin failed, latencyhealth of the plugin
itemsfor each judged item: its id, kind, digest and size — never its content

would_stop is what makes observe useful: it shows, for every call that went through, whether enforcement would have stopped it.

A run that is not recorded or judged says so on its run record, with the reason.

Refusals before a run starts

refusalmeaning
guardrail-unavailablethe run's backend is judged, but the guardrail cannot run for it — an agent it does not cover, a lane it cannot be carried into, or a remote saphan too old for it. See Coverage. When the lane is the cause, --egress-open runs that one run unwalled; for the other causes, change the agent or update the remote build
guardrail-handoff-refusedthe spawn.handoff stage blocked what the agent was about to be given — or, in any mode, the plugin could not be loaded or the hand-off could not be read
a refused push at the bridgethe bridge.ingress stage refused an arriving commit. The refusal carries the plugin's own rule id, or bridge-refusal-guardrail-plugin when it has none

On this page