Identity & authority
Every actor in the system, and exactly what each one is allowed to do: who can ask for things, who can say yes, and who only gets to watch.
The one human
Section titled “The one human”Steelmoth has exactly one operator, and the system knows them by two names:
mark@canarybuilds.com— the canonical operator email. The dashboard compares the signed-in email against this exact value (lowercased, exact match). A different email, even one delivered by the trusted login proxy, is refused with “not the canonical operator”. There is no list of allowed emails — there is one.@mareekkk— the GitHub account. CODEOWNERS assigns every protected path to it, so no high-stakes change merges without this account’s review.
Admin rights on the dashboard come from group membership only: the login provider
(Authentik, application steelmoth-dashboard) attaches the user’s groups to the request,
and a member of dashboard-admins gets the admin role. An older email-based admin list was
deliberately removed (RFC 0074 / ADR 0097) so there is one mechanism, not two. Note both checks
apply: the email must be canonical and the group must be present.
The bot accounts
Section titled “The bot accounts”| Account | What it’s for | What it can do | Where its key lives |
|---|---|---|---|
| cnysoho | Authors pull requests on behalf of coding agents, via the gh-as-soho.sh wrapper. PRs from this account let the operator review and approve someone else’s work instead of self-approving their own. |
Repo collaborator: can push branches and open PRs. | Broker alias gh-token-soho, fetched from prontera at use time — never on disk here. |
| cnry-morpheus | Authors the Morpheus drain’s pull requests, and nothing else. It never approves anything. | Deliberately not a repo collaborator — its pushes and PR calls fail until the operator decides otherwise. The narrowest identity in the system. | Broker alias morpheus-gh-token, same broker, same just-in-time fetch. |
Both tokens are the only broker secrets that fail loudly: if the broker can’t produce them, the helper raises an error instead of returning an empty string. Pushing with the wrong identity would silently break the review flow, so it’s better to stop.
Agent kinds — how the control plane names its workers
Section titled “Agent kinds — how the control plane names its workers”Every workshop session reports who it is. The free-form identity string is kept as-is, and a
normalized kind is derived from it: claude_code, codex,
morpheus — anything unrecognized becomes other. The database refuses any
other value outright: the sessions table carries a CHECK constraint listing exactly those four. An
agent can’t invent a privileged-sounding kind, because the set of kinds is fixed in the schema.
Database roles — two hats, never one
Section titled “Database roles — two hats, never one”| Role | Owns | Can do |
|---|---|---|
steelmoth_migrator |
Every table and sequence. | Change the shape of the database — only ever used by migrations. |
steelmoth_app |
Nothing. | Read and write rows, table by table, exactly as granted. On the violations table the grant is SELECT + INSERT only — the running system can add audit rows but can never edit or delete one. |
Data is also split into two schemas: steelmoth for the product (user data, the
governance audit family) and operator_authority for the workshop control plane (sessions,
scopes, approvals, violations). Scoped product tables carry row-level security with
FORCE turned on — meaning the rule applies even to the table’s own connection role,
so the app can’t sidestep its own tenant isolation. A few operator-global control-plane tables
(waitlist, agreements, pool slots) instead use a deliberately permissive policy, because they belong
to the operator, not to any tenant.
Capability groups — authority inside the product
Section titled “Capability groups — authority inside the product”When the product’s assistant acts for a user, what it may do comes from the user’s groups, mapped to named capabilities (25 distinct in total):
| Group | Capabilities |
|---|---|
prt-admins |
16 — the full administrative set. |
prt-org-managers |
2 — organization-level management. |
prt-workspace-managers |
2 — workspace-level management. |
dashboard-admins |
Aliased to the prt-admins set, so the one operator group works in both worlds. |
An unknown group contributes nothing — membership in a group the system has never heard of grants zero capabilities. The group-to-capability lookup is also the designed seam for the future trust kernel: today it reads login-provider groups; later it will read claims from a signed trust token, and nothing else in the pipeline has to change.
Two ways to be trusted by the approval API
Section titled “Two ways to be trusted by the approval API”The gate (and the shims) talk to the dashboard’s approval API. The API accepts a caller in exactly
two ways: the request comes from localhost or a private network address (the normal case — everything
runs on one box), or it presents a shared token in the
X-Operator-Authority-Gate-Token header, compared in constant time. Anything else is
refused.
There is also one special local identity: when the private dashboard runs loopback-only with
STEELMOTH_DASHBOARD_PRIVATE_AUTH_BYPASS=1, it synthesizes an admin identity — but only on
loopback hosts, and if an operator email is configured for it that isn’t the canonical one, the bypass
yields nothing. Even the backdoor checks the one-human rule.
Who outranks whom
Section titled “Who outranks whom”When instructions conflict, agents follow a fixed order, written in AGENTS.md:
1. what the user just said 2. safety and security invariants 3. the engineering standards 4. the source code and live system evidence 5. wiki prose and other documentation ← lowestSo a user instruction wins — unless it collides with a safety invariant, which nothing overrides from below. And documentation (including this site) never outranks what the code actually does.
The whole cast at a glance
Section titled “The whole cast at a glance”| Actor | Can ask | Can decide | Watched by |
|---|---|---|---|
Operator (mark@canarybuilds.com / @mareekkk) |
Anything | Approvals, scope grants, merges, settings | Settings audit, CHANGELOG, git history |
Coding agents (claude_code, codex) |
Commands, edits, scope widening, deploys — through the gate | Nothing | Gate, Landlock, sweep, violations log, sessions |
| Morpheus (drain) | Drain PRs via cnry-morpheus |
Nothing — can’t even push yet | Same stack + its own run audit |
cnysoho (bot) |
Open PRs | Nothing | GitHub history, CODEOWNERS review |
| Product assistant | Actions for users, through the four-stage pipeline | Nothing — server classifies, policy decides | Hash-chained ledger, governance decisions |
| Product users | Whatever their group’s capabilities allow | Their own escalations | Scoped audit tables (FORCE RLS) |
steelmoth_app (DB role) |
Row reads/writes as granted | Nothing structural | Grants + RLS it cannot disable |

