Article III Of dispositions
3.1
A second, purer layer
The gate answers one question about one action. A running tick, however, has several safety inputs at once: what the gate said; whether the intent has already reached a terminal status; whether this tick has read untrusted external content; whether a recovery retry has been sanctioned by the kernel; whether a web body was auto-dispatched.
lib/safety-compose.js folds all of them into one disposition — a frozen
object carrying a label, four boolean consequences, the coarse gate feedback and a reason list. The
module’s contract is unusually explicit about its own constraints: it is “part of the
kernel — deterministic code the brain can
never reach,” and it is pure: “no LLM, no I/O, no DB,
no imports of operator.js / s5-lib.mjs.”
3.2
The lattice
Five levels. The first two short-circuit; the rest only refine. Restrictions compose by intersection — most restrictive wins — and taint is monotone: it ORs across every input and propagates to everything derived from them.
web_readonly, the more
restrictive of the two. lib/safety-compose.js:150–2253.3
Fail-closed truthiness
One asymmetry in this module is worth reading closely, because it encodes a general principle about how permissions and denials should treat sloppy input.
A grant is tested with strict identity. isSanctionedRetry accepts only
=== true, and refuses the string 'true'. A denial is tested with
coercion: taint trips on Boolean(consumedUntrusted), so the integer 1, the
string 'untrusted' or a non-empty object all taint.
// FAIL-CLOSED: a deny/safety gate must taint on ANY truthy consumedUntrusted (1, 'untrusted', // a non-empty object), matching taintInputs' Boolean() truthiness — the OPPOSITE of a grant // like sanctioned (strict===true). Using === true here let a truthy-but-not-true value from an // upstream integration silently bypass the P1 halt and WRONGLY allow escalation + memory write. const taint = Boolean(safe.consumedUntrusted) || (Array.isArray(safe.taintInputs) && safe.taintInputs.some(Boolean));
The same asymmetry appears at P0, where the policy decision and intent status are both
case-folded before comparison so a non-canonical 'BLOCKED' still halts, and at P3, where
any truthy dispatch descriptor confines the body.
3.4
Taint at the source
Taint is not inferred from content; it is derived deterministically from the type of the action a tick produced. One frozen set, single-sourced in the pure kernel module so the staging tick and the verifying executor tick “classify taint identically and can never drift”:
| Group | Types |
|---|---|
| Correspondence | email, send_email, reply, email_reply |
| Web | web, web_search, browse |
| Systems of record | crm, crm_lookup |
| Inbound | telegram_read, inbound, ingest |
Reading the operator’s own wiki or memory is trusted and is not flagged. The consequence of taint
is narrow and severe: the tick may not escalate to a higher-capability body, and it may not write
durable memory — “a lesson can never be minted from a possibly-poisoned tick.” The perceiving layer
applies the same principle at the database: observations default to untrusted = 1, and
the MCP tool that writes them has no untrusted field at all, so an external caller cannot un-taint by
construction.
3.5
Coarsening: what the model is allowed to learn from a refusal
coarsenGateFeedback takes a policy verdict and returns at most two strings. The
granular reasons array is discarded outright.
| Matched | Category returned |
|---|---|
| spend · money · purchase · subscription · paid | spending requires human approval |
| irreversible · high-impact · delete · publish · deploy | irreversible action requires human approval |
| phone · call | phone calls are not autonomous |
| secret · token · redact | sensitive content needs human review |
| night · recipient | recipient restricted for this shift |
| sender · disclosure | required disclosure missing |
| anything else | requires human approval |
The verb is INTERRUPT for a block, VERIFY for a required approval, and FALLBACK for a fallback decision. An allowed verdict returns null — there is nothing to say.
Coarsening is applied at four call sites, not one: the audit detail written with the intent, the
gate_feedback field of every pending intent in the context packet, and the
failed_reason stored by both the fast tick and the general tick-runner on a gate stop.
The last two are pinned by tests that name the exact source lines they defend.
3.6
Composition by intersection
When several dispositions must be folded into one — the intended consumer is a work router that
receives more than one input — composeRestrictions applies four rules in a fixed order:
- taint firstMonotone OR across every input, computed before anything else.
- label by rankThe highest-ranked disposition label present wins; a tainted fold is forced up to
halt_taintedif no input already carried a halt label. - bind flags to labelA halted or tainted fold can never escalate or write durable memory regardless of the input flags — a self-inconsistent input cannot smuggle permissions in behind a halt label.
- conservative ANDEscalation and memory require unanimity;
readOnlyis a disjunction, and taint or halt imply it.
The third rule is the interesting one. Its comment explains the failure it prevents: without it, “most-restrictive wins” would have picked the halt label but not its meaning, and a malformed or future input could defeat the monotone propagation guarantee. Every return value is frozen, and a test asserts immutability.
3.7
The brain-cannot-assert guarantee
Six key names are declared kernel-minted. Before any field of the context is read, those six are deleted from a shallow copy. The module states the design intent: the brain “MAY try to assert sanctioned / capability / disposition / allow* — they are ALWAYS stripped and re-minted internally.”
This mirrors a rule enforced one layer up. Every runner overwrites the actor field on the model’s proposed arguments unconditionally, before dispatch, with its own fixed identity:
// Slice 8.1 security MUST-FIX A: HARD-OVERWRITE the actor UNCONDITIONALLY. A model-supplied // actor (e.g. forging 'ian' to look like a human approver) is NEVER honored — the runner's // identity is fixed here and 'operator-fast-tick' matches RUNNER_ACTOR_PATTERN, so it can // never be treated as a human approver by the gate. args.actor = { subject: 'operator-fast-tick', name: FAST_MODEL };
Two independent mechanisms therefore have to fail before a model could claim to be a person: the runner would have to forget the overwrite, and the approval function would have to accept a subject matching its own runner pattern.
3.8
Verification of effects, severed from self-report
A related module-level rule governs how a tick’s success is judged.
effectActionConsistent binds a declared effect to the action’s real type before any
world check happens: a file effect declared for an external send is not merely unverified, it is
unbound, and mints nothing.
| Effect type | Bound when | Verdict |
|---|---|---|
| file, note, draft, research | a path is declared and the action really writes a file | success if the file exists; otherwise a verified failure of type tool_execution_error |
| task_status | an expected status is declared and the action mutates a task | compared against the status observed in the database, never the model’s claim |
| external, irreversible, absent | never | unknown — never rewarded |
The point of the binding is named in the source: it is an anti-Goodhart measure. Without it, a
model could declare a trivially satisfiable effect for a difficult action and harvest a success
label. The success and failure label is written only by the deterministic verifier; the model’s own
outcome column survives for display and is never the learning label.