The automation plane gets its own token, and the host enforces the boundary
Date: 2026-08-12 Status: open Supersedes: none Superseded-by: none — current
Context
Section titled “Context”INV-3 of PTV-SCF-0001 is fatal: “The automation plane (n8n) is a C4
observer and C2 composer, never a C3 mutator. Any generated workflow that can
apply a verb, merge, or push is a defect regardless of how well it works.”
(docs/specs/2026-08-12-ptv-scf-0001-scaffold-rev-a.xml.)
Conformance has been tracked on one axis — the n8n management credential,
measured owner-grade on 2026-08-12 (POST /api/v1/workflows → 200,
docs/decisions/2026-08-12-ptv-sr-0015-automation-plane-boundary.md:52-85).
There is a second axis, and it had not been recorded. An n8n flow reads the
estate through petrova.host, and petrova.host authenticates with a single
shard token: host/src/config.ts takes exactly one PETROVA_HOST_BEARER, and
dashboard/src/pages/api/mcp.ts:26-32 states in its own header that the token
“grants the whole surface, mutating verbs included.” Handing that token to a
flow grants all 42 tools, the 17 mutating ones included — 16 petrova.act.*
write verbs plus petrova.sweeps.trigger.
So INV-3 was breached by the credential regardless of flow content, on an axis
the n8n account question does not touch. The same header pushed the remedy onto
the caller: “a consumer that wants a read-only surface must deny the mutating
tools on its side.” That is a convention, not a boundary. P5 is required to
attack exactly this shape — refusals must hold “by credential scope or CI, not
by the prompt asking nicely” — and a boundary the caller enforces fails that
test by construction.
Decision
Section titled “Decision”Add PETROVA_HOST_READONLY_BEARER: an optional second token resolving to a
readonly scope in which tools/list omits the mutators and any call to one is
refused.
Four properties, each load-bearing:
- Derived from the
mutatingflag, never from the name.MUTATING_TOOLS(host/src/registry.ts) already filters on the flag.petrova.sweeps.triggermutates — it fires aworkflow_dispatch— and carries noact.prefix, so a prefix filter leaks a dispatch to the plane. There is a test whose only job is to fail if someone rewrites the filter that way. - Enforced on the call, not only in the listing. A caller that knows a tool’s
name never reads
tools/list. Refusal sits before the handler lookup. - All three transports, one resolver.
resolveScopelives inhost/src/auth.tsand is called bydashboard/src/pages/api/mcp.ts,dashboard/src/pages/api/rpc.ts, andhost/src/http.ts(the Railway server, easy to miss — it has its own bearer check). This repo has lost tools to a forked registry three times; a scope check implemented three times would drift the same way. - Opt-in and fail-closed. Absent config leaves behaviour identical. A read-only bearer configured equal to the full bearer throws at load rather than serving: it would grant the whole surface to a caller every artefact described as read-only, and the scope check would pass on the wrong branch with nothing downstream able to detect it.
The refusal is JSON-RPC -32601, not MCP’s isError. isError means the tool
ran and failed; a client retrying on that would be retrying a refusal.
This does not make the estate INV-3-conforming. It closes one of three
axes. The n8n management key is still owner-grade, and INV-1’s ambient
estate-wide gh token is untouched.
Alternatives considered
Section titled “Alternatives considered”- Deny on the consumer side, as the header advised — rejected. It is the
status quo, it is a convention rather than a capability, and
P5’s self-improvement attack is specified to defeat exactly this. It also scales wrongly: every future consumer would re-implement the deny list, and one that got it wrong would be indistinguishable from one that got it right until it fired a verb. - Per-tool API keys / full RBAC — rejected for now as disproportionate. The requirement is one narrower principal, not an authorisation system, and a scope enum invites a policy engine nobody asked for. Revisit if a second distinct consumer needs a third surface.
- A separate read-only deployment of the host — rejected: it doubles the
deploy surface and the
state/staleness problem (D-09/D-12) with it, to express something a token already expresses. - Filter on the
petrova.act.prefix — rejected, and recorded here because it is the attractive wrong answer: it reads as equivalent, is shorter, and silently exemptspetrova.sweeps.trigger.
Consequences
Section titled “Consequences”For code:
host/src/auth.tsgainsresolveScopeand theAuthScopetype;verifyBeareris unchanged and still used by it.host/src/config.tsgains optionalreadonlyBearer, and throws when it equalsbearer.host/src/registry.tsgainsREADONLY_TOOLS_LISTandisMutating, both derived.dashboard/src/pages/api/rpc.tsgains its first tests. It had none, and it is the transport the CLI actually speaks.
For operations:
- The token must not be added to
~/code/env/_loader.zsh. Loading it into every interactive shell is what made C-1 ambient, which is the conditionINV-1halts on. - Vercel remains the source of truth for both bearers; a rotation needs a redeploy, as today.
For the scaffold:
P4’s prompt flows can now run “under plan mode with a read-only credential” as specified. Before this they could not, on any credential that exists.P5gains a refusal it can attack by scope. The negative test — a mutating call under the read-only bearer, refused — is a deliverable there, not an assertion here.
For invariants: INV-3 moves from breached on three axes to breached on
two. No MR is added, modified or repealed.
References
Section titled “References”docs/specs/2026-08-12-ptv-scf-0001-scaffold-rev-a.xml—INV-3, andP4/P5gate text.docs/decisions/2026-08-12-ptv-sr-0015-automation-plane-boundary.md— the boundary law, and the owner-grade measurement on the n8n axis.docs/findings/20260811-2027-ptv-scf-0001-credential-scope.md— C-1, C-3, C-5 and the ambient-credential problem_loader.zshcreates.host/src/registry.ts—mutatingflag,MUTATING_TOOLS, and the header explaining why every transport derives from one registry.
Sign-off
Section titled “Sign-off”- Subagent: PTV-SCF-0001 Phase A (session 2026-08-12)
- Human countersign — that the automation plane authenticates with a distinct
read-only bearer, that the token is never loaded by
_loader.zsh, and that this closes oneINV-3axis without making the estate conforming