Governance
Every tool call in keelson, whether a rib tool, a workflow prompt node, or the
agent’s own built-in shell and file edits, passes through one policy engine.
The engine is a single ordered stack: an operator floor of builtins plus any
policies the installed ribs contribute. It runs on all four surfaces (chat, a
workflow prompt node, a rib agent turn, and the /api/mcp gateway), so a rule
you set holds wherever a tool runs. A tool call an external agent makes over
/api/mcp passes the same stack (denylist, ask_on_shell, redact), so a
governance rule covers those callers too.
Out of the box the engine only enforces the operator tool denylist. Approvals, budgets, and redaction are opt-in builtins, each enabled by one environment variable. This guide turns them on; the rib contract covers contributing a policy from a rib.
The decision model
Section titled “The decision model”A policy returns one of three decisions for an event. The stack is walked in
order and the first deny wins, so any policy can veto a call a later one would
allow.
| Decision | Effect |
|---|---|
allow | No opinion, the call proceeds. On a tool result or a node’s output, an allow that carries replacement text substitutes it: this is how redaction works. |
deny | Short-circuits. The agent receives the reason as a tool error, or the tool is dropped from the set offered to the model. |
ask | Pauses for a human. Accept becomes allow, reject or a timeout becomes deny. With no approval channel wired, an ask degrades to deny. |
The engine evaluates at several points in a turn: when the tool set is projected to the model, before a specific call runs (with its arguments), once before a turn starts (carrying the running token and turn usage, which backs budgets), after a tool result returns but before the model reads it, and on a workflow node’s assembled output before it flows downstream. A builtin only adds cost at a phase some policy actually reads, so an unused phase is free.
Approvals
Section titled “Approvals”Set KEELSON_ASK_ON_SHELL=1 to enable the ask_on_shell builtin. It pauses for
human approval before any shell or file-mutating call: keelson’s own
Bash, Edit, Write, MultiEdit, and NotebookEdit tools, and the canonical
shell tools a rib might register.
This covers the agent’s own built-in capabilities too, not just keelson
tools. Copilot routes its built-in shell and write permission requests through
the engine, and Claude’s built-in Bash / Edit / Write now route through it
as well, so an ask_on_shell prompt fires whether the write came from a keelson
tool or the agent reaching for its own.
KEELSON_ASK_ON_SHELL=1 keelson startResolving an approval
Section titled “Resolving an approval”A pending approval surfaces two ways. In the app it appears as an in-chat prompt,
published over the snapshot WebSocket on the keelson:policy:approvals key. From
the CLI, list and resolve them:
keelson approval listkeelson approval resolve <id> accept # or: rejectAccept lets the call proceed (later policies still apply); reject denies it with a reason. An approval that no one answers within five minutes auto-rejects, so a forgotten prompt fails closed rather than hanging the turn. When more than one policy asks about the same call, the engine coalesces them into a single prompt.
Budgets
Section titled “Budgets”Two builtins cap a session’s spend, where a session is one chat conversation or
one workflow run. KEELSON_TURN_BUDGET=<n> limits model-calling turns;
KEELSON_COST_BUDGET=<tokens> limits accumulated input plus output tokens. Both
check once before each turn. Leaving a variable unset, blank, or non-positive
leaves that builtin off.
KEELSON_TURN_BUDGET=40 KEELSON_COST_BUDGET=2000000 keelson startThey are a downgrade gate, not a hard wall. Once the ceiling is reached, a turn is denied only while it runs on an expensive model: a premium cost tier, or a metered per-token login. A turn on a cheaper or flat-rate subscription model keeps going. The denial message tells you to switch models. So the budget pushes spend off your metered model rather than killing the session outright. A model keelson cannot price is treated as expensive, failing closed.
Redaction
Section titled “Redaction”Set KEELSON_REDACT_PATTERN=<regex> to enable the redact builtin. Every match
of the pattern is replaced with [REDACTED] in a tool’s result before the model
consumes it, and in a workflow prompt node’s output before it flows to a
dependent node. Use it to scrub a secret a tool might surface.
KEELSON_REDACT_PATTERN='sk-[A-Za-z0-9]{20,}' keelson startAlternation covers several patterns at once, for example (sk-[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{36}).
A blank or unset value leaves the builtin off. A pattern that is invalid, or one
that risks catastrophic backtracking (ReDoS) against adversarial tool output, is
reported and ignored, so a bad value disables redaction rather than wedging the
server.
The operator denylist
Section titled “The operator denylist”KEELSON_WORKFLOW_TOOL_DENYLIST is the always-on floor: a comma-separated list
of tool names no workflow prompt node may use, subtracted on top of whatever a
node’s own allowed_tools permits. It is the one governance control active
without opt-in, and it cannot be overridden by a node or a rib.
KEELSON_WORKFLOW_TOOL_DENYLIST=Bash,Write keelson startKEELSON_APPROVAL_REVIEWER=off is its sibling for approval gates. A workflow’s
approval node may declare a reviewer that answers the gate when it approves
with high confidence; this floor disables every such reviewer so each gate
pauses for a human. See the approval node reference.
Filesystem confinement
Section titled “Filesystem confinement”The path_confinement builtin is always registered, alongside the operator
denylist. It stays inert until a rib agent turn declares its own working
roots: when a turn carries allowedDirectories, the builtin denies any tool call
whose file paths or shell arguments canonicalize outside those roots. It
realpath-canonicalizes both the roots and each candidate path before comparing,
so a symlink inside an allowed root that points elsewhere cannot be traversed to
escape confinement.
This is a rib-agent-turn capability, not a global operator toggle. Chat, workflow
prompt nodes, and MCP calls declare no roots, so the builtin allows everything
there; a rib that runs an agent turn against a scoped directory gets confinement
for free.
Policies from ribs
Section titled “Policies from ribs”A rib contributes policies through its contributePolicies hook. The harness
collects them at boot and appends them to the same ordered stack, after the
builtins, namespaced as rib:<id>:<policy>. They share every semantic above:
the three decisions, first-deny-wins, the ASK round-trip, and the coalescing of
duplicate prompts. A malformed policy is dropped with a warning rather than
crashing the boot. See contributePolicies in
the contract for the shape.
On the workflow surface the context also carries workflowName and nodeId,
so a rib policy can scope itself to its own workflow’s node rather than firing on
every workflow-surface turn. Both are absent everywhere else, and on an older
harness that does not populate them, where they read as undefined: a check like
ctx.workflowName === "x" does not error there, it simply stops matching, so a
policy that must hold against an older harness decides for itself whether an
absent name fails open or closed. The rib contract
documents the full context.
Cross-rib grants
Section titled “Cross-rib grants”A rib can reach another rib’s tool, but crossing that line is default-deny:
with no grant, it is refused before it reaches the policy engine. The operator
opts a caller in per caller → target → tool, in config.json:
{ "crossRibGrants": { "swarm": { "beads": ["beads_ready", "beads_show", "beads_update", "beads_close"] } }}"*" covers every tool the target owns; list tool names to narrow it. The
equivalent KEELSON_CROSS_RIB_GRANTS env string (caller:target:tool triples,
;-separated) unions with the config rather than replacing it. Prefer the config
file for a standing grant, since an env-only grant lapses whenever the server
starts from a shell that never exported it.
The grant gates two paths: a rib’s explicit callTool, and the tools
projected onto a rib’s agent turn. The second is easy to miss: a rib that offers
an agent a capability backed by a sibling rib’s tools gets nothing without the
grant, and the tools are simply absent rather than refused, so the agent cannot
report the gap. If a rib’s agent says it lacks a tool you expected it to have,
check the grant before the rib.
The turn’s log is where that denial surfaces. When a rib explicitly requests
sibling-owned tools no grant clears, the server writes one [rib-agent-turn]
warning per caller and owner pair, naming the dropped tools and the grant that
would clear them. A rib can also ask before it spends a turn: getToolReachability
on the rib contract returns a per-name status, and
cross-rib-denied is the missing grant.
The check runs first, ahead of the policy stack, so an ungranted call never surfaces an approval prompt for a call that would be denied regardless. See Configuration for the config key, the env string, and the cross-rib call timeout.
Rib workflow grants
Section titled “Rib workflow grants”A rib starting a catalog workflow through startWorkflow follows the same order.
It is default-deny, the operator opts a rib in per workflow name under
ribWorkflowGrants, and the grant is checked before the policy stack. A granted
start is then evaluated as a workflow_run tool call on the rib surface, so a
policy can still deny it. The rib can read and cancel the runs it started. See
Configuration for the config key and the
env string.
Answering those runs’ approval gates is a second grant, ribApprovalGrants, in the
same order: default-deny, opted in per workflow name, checked before the policy
stack, then evaluated as a workflow_respond call on the rib surface. Without
it the gate waits for you. A rib answers only on runs it started, and a paused
run’s status hands it the files the gate names, such as the plan, so it can judge
what it approves. See Configuration.
Related
Section titled “Related”- Configuration: every
KEELSON_*variable, including the governance ones, in one table. - The CLI: the
keelson approvalcommand group. - The rib contract: contributing a policy from a
rib with
contributePolicies. - Operating the server: the running server the approval round-trip needs.