On June 5, commit commit-9547 closed an authority hole that had given every agent the same answer when it asked for permission: skip the checks.

We had built a proposal store so agents could take approved work and dispatch it without waiting for us to re-enter the same instruction by hand. A proposal had a handoff, required capabilities, a trust context, and a dispatch posture. Approval turned that proposal into a bridge session.

The problem sat in a default lower down the stack.

The bridge engine represented posture as posture | null. That looked harmless while we were wiring the first paths. null meant no explicit posture had been chosen yet. But the code that turned a posture into permission arguments treated null as the unrestricted CLI path:

--dangerously-skip-permissions

Missing authority was never a temporary state. It was an authorization failure, and the code treated it as a green light.

Resume made it worse. A fresh spawn could carry posture in from the caller. A recreated session had no equivalent guarantee. When send() rebuilt a session, it could do so with no posture at all, and the bridge silently used the unrestricted argument again. We had a permission system on paper and a bypass on the two paths that mattered most: autonomous dispatch and resume.

Tracing a bug that produced no error

The first symptom was the absence of an error.

We expected an autonomous act with no resolved posture to be rejected before a session existed. Instead, the bridge ran. Tracing the call chain showed the sequence: approveAndDispatch assembled its own capability gate and posture, spawnFresh() accepted an optional posture, and permissionArgs() accepted null. Once that null reached the argument builder, the engine selected unrestricted mode.

The proposal store knew whether a proposal was approved. The bridge knew how to launch or resume a process. Neither one owned the complete question: may this act run with these capabilities, under this trust context, in this area?

Our first option was to tighten the proposal store. We could have made approveAndDispatch require a posture and added another guard before it invoked the bridge. That would have fixed autonomous dispatch, but it would have missed interactive calls from /api/bridge/spawn, message sends through /:sid/message, and session recreation. It would also have left several places responsible for assembling the same policy.

The second option was to keep the nullable field and change its meaning, so null mapped to an enforced posture instead of unrestricted mode. That was better, but absence would still look valid. A later caller could forget to resolve authority, receive an implicit posture, and carry on. We did not want a safer fallback. We wanted no fallback.

The third option was the one we shipped: introduce a single authority object and make every act obtain one before it can run.

One function that resolves authority

The new server/actAuthority.ts exports one function:

authorizeAct({ act, trust, required, areaKey })

It composes the existing capability gate with dispatch posture and returns an explicit ActAuthority. It has two valid outcomes.

An interactive act, with the operator present and authenticated by key, receives explicit trusted authority. An autonomous act receives an enforced posture only after its requested capabilities pass the gate. Otherwise authorizeAct() refuses the act.

There is no result that means “run unrestricted because nobody specified anything.”

The old type let omission slide through several functions unnoticed. The new type forces resolution at the boundary. The caller either holds an ActAuthority or it does not get to cross into the bridge.

In bridgeEngine.ts, permissionArgs() now throws if it receives no authority. spawnFresh() and send() guard up front as well. The change to send() mattered most. It now accepts the authority explicitly, so re-creating a session cannot silently forget the authority that launched it.

The API routes resolve interactive authority before touching the bridge. Both /api/bridge/spawn and /:sid/message call authorizeAct() and pass the returned object through. The proposal store resolves autonomous authority through that same function in approveAndDispatch, rather than inlining its own gate and posture logic.

interactive API route -> authorizeAct(...) -> ActAuthority -> bridge
proposal approval -> authorizeAct(...) -> ActAuthority -> bridge

Every spawn and every resume needs an authority resolved for the act in front of it.

Keeping the handoff contract

We did not use this fix as an excuse to rewrite dispatch. The existing SpawnFn(handoff, posture) contract stayed intact. That preserved the proposal store’s tested handoff behavior while moving the policy decision out of the store and into the authority module.

The existing lock layer already funnels mutation routes. authorizeAct() exposes act: "mutation" as the extension point for the same pattern. The immediate hole was spawn and resume, so that is what we closed. The new boundary gives mutations a place to attach without pretending they were already covered.

The change touched four files: 175 insertions and 29 deletions. The new authority module accounts for 101 lines. Putting the policy in one place means a future dispatch path makes one explicit call instead of re-checking posture, capability, and trust conditions on its own.

The verification was unglamorous, which is what we wanted. tsc --noEmit reported the same nine pre-existing errors and no new ones. proposalStore.test.ts passed all 6 of 6 cases, preserving the existing gate behavior. The full suite still had eight failures, all reproduced at commit commit-58C7: stale nodes.test.ts cases and two live OpenAI tests with no key. tsx-watch reloaded cleanly on port 3010.

Check your own fallbacks

The original bug was a silent default: when no one specified authority, the unrestricted option was chosen. Look at every automated or resume path in your systems and find out what it does when permission is missing. It should refuse, and every act should have to obtain an explicit authorization before it can run.