Skip to content

Authorization pipeline

Everything else on this site governs the agents that build Steelmoth. This governs the assistant inside Steelmoth, when it acts on a user’s behalf — sending an email, driving a browser.

The product’s assistant can take real-world actions for its users. That needs its own rule system — one that is multi-tenant (every user’s actions judged separately), fail-closed, and auditable in a way a user or operator can trust after the fact. It lives in the steelmoth_runtime/authz/ package and is deliberately separate from the operator-authority code: different threat, different machinery.

Every governed action passes through govern.py, which runs four stages in a fixed order:

the assistant wants to act (send email, run browser step)
the SERVER builds the request envelope ◀── not the model. The description of
│ what's being attempted comes from
▼ trusted code, so the model can't
1. RISK score the action sweet-talk its own paperwork.
│ (what band is this? reversible?)
2. POLICY decide: ALLOW / DENY / ESCALATE
│ (fail-closed — see below)
3. DLP scan the content itself
│ (may only TIGHTEN: an ALLOW can
│ become ESCALATE, never the reverse)
4. AUDIT append to the hash-chained ledger
the action runs, waits for a human, or is refused

Stage 4 writes to a ledger (audit.py) where every record contains the hash of the one before it — the same idea that makes a blockchain tamper-evident, without any of the rest of the machinery:

record 1 record 2 record 3
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ prev: GENESIS │ ◀───── │ prev: hash(1)│ ◀───── │ prev: hash(2)│
│ decision … │ │ decision … │ │ decision … │
└──────────────┘ └──────────────┘ └──────────────┘
verify_chain() recomputes every hash in order. Change a record,
delete one, or reorder them — and the chain stops adding up.

Hashes are computed over a canonical form of the record (keys sorted, stable formatting), so the same record always produces the same hash. An ordinary log says what happened; this ledger can also prove nobody edited the story afterwards — including someone with write access to the log itself.

A blocking pipeline that is wrong blocks real users. So the pipeline shipped in shadow mode first: it runs on every governed action, makes its decision, writes its audit record — and then does nothing. The action proceeds as it always did. The decisions accumulate where the operator can review them, so the policy can be tuned against reality before it is given teeth.

  • The mode switch (governance_settings.py) defaults to shadow, and an unrecognised value also means shadow — the unsafe direction (silently enforcing, or silently off) is not reachable by typo.
  • The wiring (governance_wiring.py) sits at the top of the runtime’s action-execution path, wrapped so that no failure in the governance code can break a user’s action while shadowing. A kill switch (STEELMOTH_GOVERNANCE_SHADOW_ENABLED) turns the seam off entirely.
  • The enforcement half (enforce_decision()) is already written and tested as a pure function — flipping to enforce mode is a settings change, not a code change.

Decisions land in a governance_decisions database table protected by row-level security: each user can only ever see rows about their own actions. The table stores the decision and a hash — not the raw content of the email or page involved, so the audit trail doesn’t itself become a copy of everyone’s private data. A second table, governance_settings_audit, records every change to the governance settings themselves: who flipped what, old value to new value, whether they had passed two-factor auth, and when. The switches that control the watchdog are watched too.