Skip to content

Operator authority

Cross-agent pre-edit enforcement for every coding agent on this VPS.

A wiki orientation page describing the operator-authority system — the program that converts Steelmoth’s hard rules, architecture invariants, and protected paths from documentation into mechanical enforcement. See PRD 0042 / RFC 0079 / ADRs 0110..0118 for the full design; this page is the repo-orientation summary.

Steelmoth has 9+ HARD RULES in CLAUDE.md, an architecture-invariants file, engineering standards, system-wiki orientation pages, threat models, and per-agent rule files. These describe what coding agents on this VPS — Claude Code, Codex, Morpheus, plus future dashboard-coding agents — must and must not do. Until 2026-05-23 the enforcement posture was ~40% pre-action, ~40% documentation, ~20% post-fact detection — and a real incident on that date (an agent materialised a local secret cache at /etc/agent-vault/* in violation of INV-024, HARD RULE 10, and ADR 0105) showed the gap.

One canonical policy + one shared gate + per-agent shims + kernel floor + GitHub enforcement + dashboard control plane + CI invariant tests.

engineering-standards/AGENT_POLICY.yaml ← single source of truth
steelmoth_runtime.operator_authority.gate ← one program, one API
┌───────────┬───────┴────────┬──────────┬──────────┐
▼ ▼ ▼ ▼ ▼
Claude Code Codex shim Morpheus Moth Dashboard
hooks + (no bypass, scope guard read-only approval
Landlock Landlock consumes (no write gate
floor floor) the same path)
manifest
GitHub branch
protection +
CODEOWNERS
  • Policy: engineering-standards/AGENT_POLICY.yaml (machine-readable) and engineering-standards/PROTECTED_PATHS.md (human-readable, derived from / cross-checked against the YAML).
    • In-container the runtime process runs from cwd /app (not the repo root), so load_policy()’s cwd walk-up cannot find the policy. The steelmoth-api service therefore sets STEELMOTH_AGENT_POLICY_PATH=/app/repo/engineering-standards/AGENT_POLICY.yaml (the baked repo) so policy loads resolve to the canonical file via policy.py:_resolve_policy_path. Without it the self-serve scope flow (decide_task_plan) fails closed to task-approved only. Set in compose.steelmoth.private.yml.
  • Gate: steelmoth-runtime/src/steelmoth_runtime/operator_authority/ Python package — policy.py, manifest.py, landlock.py, gate.py, approval.py, violations.py, posthoc.py.
  • Shims: ~/.local/bin/codex (host) + /usr/local/bin/codex (steelmoth-api container) + ~/.local/bin/claude (host). Source-of-truth scripts at scripts/operator-authority-codex-shim.py and scripts/operator-authority-claude-code-shim.py.
  • Manifests: /var/lib/operator-authority/scopes/<worktree-hash>.json — per-worktree scope grants. Operator-only writes.
  • Violation log: /var/log/operator-authority/violations.jsonl — append-only JSONL. Per-file Landlock allow-rule for the gate to write its own audit.
  • Dashboard control plane: dashboard/app/agent-authority/ pages
    • routes_operator_authority.py backend + operator_authority.* Postgres schema. The sidebar section (its own top-level Agent Authority category) is led by the Overview page (dashboard/app/agent-authority/overview/page.tsx, adminOnly, added 2026-07-25): an at-a-glance all-time stats landing backed by GET /v1/operator-authority/stats (require_admin_read) — real COUNT(*) aggregates over the gate tables plus per-day/per-month trend series (repository.compute_stats()), never a bounded-window extrapolation, with approvals counted in operator-queue scope so the numbers match the Protected Paths page; a /agent-authority/overview/details subpage carries the full breakdown.
  • Claude Code — Hooks at ~/.claude/settings.json PreToolUse for Write / Edit / Bash. Landlock applied via the Python shim at ~/.local/bin/claude.
  • Codex — Native command-approval rules at .codex/rules/default.rules (restored to active by removing --dangerously-bypass-approvals-and-sandbox). Landlock applied via the Python shim that wraps the real Codex binary.
  • Morpheus — Existing morpheus_scope_guard.py migrates to read from AGENT_POLICY.yaml via a back-compat shim. Landlock applied at the morpheus-drain-daemon entrypoint to cover terminal-bridge runs.
  • GLM + Kimi — the glm (~/.glm) and kimi (~/.kimi) terminal launchers each carry the same PreToolUse gate hooks in their own settings.json. Since 2026-07-24 their settings.json / settings.local.json / hooks/** are protected paths in AGENT_POLICY.yaml (mirroring the .claude set, category agent-policy-config), closing the self-disable gap where either agent could edit its own settings and switch its own gate off.
  • Future dashboard-coding agents — The Agent Authority dashboard surface (its own top-level sidebar category, led by Overview) and the operator_authority.sessions.agent_kind enum are designed to accommodate additional agents without rework. Add an agent_kind enum value, register a shim or hook, consume the same YAML.

Runtime users of the durable approval queue

Section titled “Runtime users of the durable approval queue”

The operator_authority.approval_requests queue also serves two runtime (non-coding-agent) askers. Both kinds are owner-scoped (migration 0132): the create route stamps owner_id from the gateway’s own canonical email, the owner-facing read (GET /v1/operator-authority/approvals/mine, repository.list_owner_browser_action_approvals) returns only the caller’s own rows of these kinds, and the decide route authorizes the row’s owner (or an admin) — kind-agnostic.

  • browser_action (migration 0068; ADR 0197) — the browser agent pauses before a consequential page action and files a durable ask.
  • background_action (migration 0135; ADR 0204) — a background run (PRD 0058 / RFC 0116) that reaches a mutating step mid-run. The chat chip’s 5-minute TTL cannot gate a minutes-long job, so the chip-emission path reroutes: the run canonicalizes and mints its approval token through the same per-pair emitter the chat uses (long TTL — ask timeout + slack), files the durable request, flips its ledger row to awaiting_approval (claim lease extended past the approval window so the restart reaper leaves it alone), and blocks its worker thread on the outcome. One Telegram DM per ask, from the request’s on_created callback. Approve → the token redeems through the exact routes_write_action execute + audit path a chat approval uses and the run returns to running; deny / timeout / unreachable → the token is cancelled and the run fails with an honest error, never a mutation. A run orphaned mid-wait by a restart is reclaimed once the extended lease lapses: the reaper resolves the stale request to expired and cancels the run’s still-issued tokens (keyed by its bgrun_<run_id> message id).

No chat chip hint is emitted or persisted for a background ask; the approvals panel labels the two kinds (“Website sign-in” / “Background task”) on the owner’s Governance page.

  • Moth (in-product chat assistant) — READ-only for code by design. Phase 6 test scans steelmoth-runtime/src/steelmoth_runtime/moth/** for filesystem writes outside the YAML’s moth_allowed_writes allowlist; CI fails the build on any new write target. The /self-code/* sidebar routes stay reachable under Advanced per HARD RULE 7-bis but are observability for any external self-edit runner (Morpheus today; not Moth).
  • Dispatch Team workers (Lloyd) — removed 2026-06-16 (dispatch subsystem deleted in #354/#363/#367; ADR 0036 superseded). Historically out of scope: they operated on tenant data and never edited repo source.
  1. Current operator instruction in this session.
  2. Safety / security invariants: INV-024, HARD RULES 1/2/3/4/6/7-bis/8/9/10/11.
  3. Engineering standards: branch-per-phase, plan-mode before push, pre-phase bundles ship as one PR.
  4. Live evidence: source code, schemas, tests, scripts, runtime state.
  5. Wiki prose (including this page).

Granting a scope token to an agent for a task

Section titled “Granting a scope token to an agent for a task”

Today (pre-Phase-8): edit the per-worktree scope manifest by hand at /var/lib/operator-authority/scopes/<worktree-hash>.json. The hash is sha256(realpath(toplevel))[:12]. Schema:

{
"task_id": "phaseN-feature-name",
"agent_identity": "claude-code|codex|morpheus",
"worktree_path": "/home/mareekkk/steelmoth/.claude/worktrees/feature-name",
"allowed_paths": ["src/steelmoth_runtime/feature_name/**"],
"scope_tokens": ["compose"],
"expires_at": "2026-05-24T18:00:00+08:00",
"issued_by": "mark@canarybuilds.com"
}

Post-Phase-8: Agent Authority → Scope Manifests panel in the dashboard.

Today (pre-Phase-8): the agent reports the deny; operator edits the scope file; agent retries.

Post-Phase-8: the agent’s gate calls the dashboard’s approval API and exits 2; operator sees the request in Approval Queue; click approve; gate resumes; action proceeds.

tail -f /var/log/operator-authority/violations.jsonl for live view. Post-Phase-8: Agent Authority → Violation Log panel.

Edit AGENT_POLICY.yaml’s protected_paths: list with the glob + category + required_scope + reason. The matching prose entry in PROTECTED_PATHS.md should also be added in the same PR (CI check verifies consistency). The PR is operator-authored or requires #scope:meta per the existing Morpheus scope guard.

Edit AGENT_POLICY.yaml’s denied_commands: list with the regex + reason + optional exception_path + optional required_scope.

Adding a new architecture invariant with enforcement

Section titled “Adding a new architecture invariant with enforcement”

Edit engineering-standards/ARCHITECTURE_INVARIANTS.md: write the prose for the new INV-NNN and include a ### enforcement block per ADR 0118 format. The Phase 9 fitness-test runner picks it up on the next CI run.

See docs/operations/FAILURE_MODES.md rows 303–311 for the operator-authority-specific failures: YAML parse failure, manifest missing, Landlock install fails, dashboard unreachable during approval, post-hoc sweep reverts legitimate change, PATH ordering bypass, CI check failing for unrelated reason, fitness-test regex bug, new agent without schema integration.

See docs/operations/THREATS.md “Operator Authority” section for STRIDE coverage: agent self-escalation via scope-manifest forgery, shim bypass via absolute path, YAML tampering, gate module replacement, Bash-pattern bypass, audit log integrity, denial of service via flooding.

  • docs/prds/0042-operator-authority.md — PRD (program-level)
  • docs/rfcs/0079-operator-authority-architecture.md — RFC (system design)
  • docs/adr/0110-canonical-agent-policy-yaml.md
  • docs/adr/0111-shared-preflight-gate.md
  • docs/adr/0112-landlock-floor-codex-and-claude-code.md
  • docs/adr/0113-codex-bypass-removal.md
  • docs/adr/0114-morpheus-migration-to-shared-policy.md
  • docs/adr/0115-moth-read-only-code.md
  • docs/adr/0116-github-enforcement.md
  • docs/adr/0117-dashboard-control-flip.md
  • docs/adr/0118-invariant-fitness-tests.md

Bundle (this PR) → Phases 1..9 ship in subsequent PRs per the branch-per-phase workflow. See PRD §Phasing for the per-phase branch names and dependencies.