frameworkdispatch-control-plane · v21099:anthropic-cowork-pmdesign2026-08-12served from databaseAll documents

Dispatch — a central authority agents cannot route around (v2: decisions + gap review)

Dispatch — a central authority agents cannot route around

Status: v2 — CEO decisions incorporated, gap review complete, approved for Phase 1 · Author: 1099:anthropic-cowork-pmdesign · Date: 2026-08-12 Slug: dispatch-control-plane · Measured against: live jbnx Postgres (ngjmqdzpnhwpybtssykz), 14-day window ending 2026-08-12

v2 changes: the five open decisions are answered (§7); a second-pass gap review found six holes in v1 and closes them (§7A) — actor authentication, the approval-writer problem, live actor visibility, bad-actor detection, back-billing mechanics, and the billing schema's disabled RLS.


BLUF

JBNX already has most of a coordination layer — claims, leases, handovers, work sessions, usage events, conflict detection, a ship log. What it does not have is authority. Every one of those mechanisms is something an agent chooses to call. The directive at hi.jbnx.io is well written and widely ignored, and the database can prove it.

The fix is not a better directive and not a smarter manager agent. It is moving the coordination layer from the honour system to the critical path: make the lease the thing that issues the capability, so an agent that skips the project manager cannot produce a commit, a deploy, a migration, or a portal write. Coordination stops being a rule and becomes a precondition.

One correction to the framing before anything else. "Gate the model key" only works where JBNX pays per token. 99.7% of recorded tokens do not pass through llm.jbnx.io — they come from subscription surfaces (Cowork, Claude Code, Cursor, claude.ai) where inference cannot be withheld. So the gate cannot sit on thinking. It has to sit on effect: the repo, the deploy, the database, the portal. An agent that works unclaimed can still burn its own context. It just cannot change anything, and the attempt is logged with its name on it.


1. The numbers

All figures are the trailing 14 days unless noted.

1.1 Identity is a self-asserted string

MeasureValue
Actor codes that consumed model tokens101
Agents in the org chart (agent_ops.agents)26
Actor codes using models that are not in the org chart98
Usage rows from unregistered actors1,200 of 1,218 (98.5%)
Actor codes that took a claim and are not in the org chart76 of 78

While writing this document I invented the handle 1099:anthropic-cowork-pmdesign, which had never existed, and the API issued it an 8-hour exclusive lease on projects-portal on the first attempt — the service that serves the directive itself. The only check was the shape of the string. There is no registry, so there is no such thing as an unauthorised agent.

That single fact explains most of what follows. You cannot hold an agent accountable to a project manager when the agent chooses its own name at the moment it acts.

1.2 Work happens outside the lease

MeasureValue
Usage events with no live claim by that actor at that moment227 of 1,218 (19%)
Project claims with no overlapping billing session at all322 of 658 (49%)
Billing sessions on a slug with no customer assignment6
Sessions shorter than 60 seconds103
Open leases right now / expired-but-unreleased3 / 0

Roughly a fifth of all model work in the company happened while its actor held no lease. Half of all claims produced no billable receipt. The lease-expiry mechanism is working (nothing is stranded), but the claim→session link is not — the two systems agree less than half the time.

1.3 Spend is recorded, but not measured

MeasureValue
Usage rows recorded at $0 or null cost1,069 of 1,218 (88%)
Tokens priced at zero55.6 million
Recorded model cost, 14d$1,670.71
Total vendor charges ever loaded (vendor_charges, all time)$1,447.93
Rows with source = 'untracked'132
Tokens through the metered gateway (engine = litellm)380,343 of 139.8M — 0.3%

The directive calls silent $0 a defect. It is the majority case. And the one path that would make metering automatic — LiteLLM — carries 0.3% of traffic and still records $0 cost on all 560 of its rows, so even the metered path is not metering.

engine is free text and has fifteen spellings of the same vendor (claude, claude-app, claude-chat, claude-chrome, claude-cowork, claude-desktop, claude-mobile, claude.ai, claude-opus-5, cowork, anthropic, …). No rollup by engine can be trusted.

1.4 The approval gate leaves no evidence

agent_ops.change_approvals — described in its own comment as "CC8.1 evidence: durable record of the chat approval that authorised a production change" — contains 0 rows, against 19 production changes and 74 ship-log entries in the same period. The approval ritual happens in chat and dies there. If a customer or an auditor asks who authorised a change, there is no answer.

1.5 What is already working

Worth stating plainly, because the redesign should not touch it:

collisions at claim time — 49 recorded. This is the good bones of the whole design.

secrets. 29 grants in 14 days. The hardest part of the gate is already built.

pg_cron sweep. Silence is safe. Do not disturb this.

The pieces are there. They are not wired to each other, and nothing depends on them.


2. Why this cannot be fixed with instructions

The directive already says, in bold: never work unclaimed; never double-post; silent $0 is a defect. Compliance is 51% on billing and 12% on cost. The reason is structural, not cultural:

  1. The agent is judged on the task, not the bookkeeping. Every skipped step is a saved token and

a faster answer. The incentive gradient runs away from compliance on every single turn.

  1. A fresh context has no memory of the rule. Most JBNX agents are one-shot chats. The rule is

re-learned or not, per session, forever.

  1. Nothing downstream depends on the record. An agent that never posts a status still ships code.

The unwritten receipt has no consequence the agent can perceive.

  1. The enforcement surface is the agent's own goodwill. /api/1099/* is ungated by design. That

is correct for availability and fatal for authority.

A manager agent that asks other agents what they are doing inherits all four problems. It becomes another thing to ignore, and now you are paying tokens to be ignored.

The only durable enforcement is a capability an agent needs and cannot mint itself.


3. The design

Dispatch is one authority in two parts, per the CEO's decision:

billing truth. Deterministic, always on, costs nothing per decision.

the plane questions, negotiate a queue position, and get a straight answer about who is doing what.

The rule that keeps the two honest: the face never decides anything the plane enforces. Dispatch the agent can explain, prioritise and chase. It cannot grant an exception — an exception is a row in the plane, written by a policy, or it does not exist. This is what stops Dispatch itself from becoming the next thing to social-engineer.

Layer 0 — Identity: no unregistered actor

Today: any string of the right shape gets a lease.

Change agent_ops.agents from an org chart into a registry. Every actor code must exist and be active before it can claim, ship, or record. Registration is cheap — one row, self-service via a register call that requires a machine fingerprint (agent_ops.machines already exists and has 3 rows) and an operator. Unknown actor is not rejected into a void; it is quarantined: the claim is refused with a hint telling it exactly how to register, and the attempt is logged.

This is the load-bearing change. Everything else assumes you can tell agents apart.

Also: collapse engine and model onto a controlled vocabulary with a check constraint. Fifteen spellings of Claude means no spend report is real.

Layer 1 — The lease is the unit of authority

The lease already exists and already expires. Give it two new properties:

  1. It carries a scope. Not just a slug — the concrete resources from slug_resources (which

repos, which Railway services, which Supabase schemas). This already exists for conflict detection; reuse it as the capability scope.

  1. It is the only thing that mints capability. See Layer 2.

Nothing about the claim/renew/release loop changes for a well-behaved agent. agent.sh claim keeps working exactly as it does today. The difference is what happens to an agent that skips it.

Layer 2 — Gate the effect, not the inference

Four chokepoints. Two already exist and need a precondition added; two need building.

ChokepointStatusRule to add
gh-proxy (repo reads/writes)built — 29 grants/14dRefuse a grant unless the requesting actor holds a live lease whose slug_resources includes that repo. Write grants additionally require the lease work_scope to permit writes.
ship edge function (deploys/prod changes)built — 74 writes/14dSame lease check, plus refuse when change_approvals has no matching approval row for a gated-lane repo.
Portal write API (status, document, facts)ungatedRegistered actor required. Status/close-out already implies a lease; make that explicit rather than assumed.
Supabase service-key writesungatedRoute agent DDL/DML through a dispatch.apply() RPC that checks the lease, instead of handing out the secret key. Longer horizon; the other three cover most real damage.

An agent working without a lease can still think, read, and draft. It cannot commit, deploy, migrate, or write to the portal. Every refusal is a row with its handle on it — refusals become the metric that tells you whether the gate is holding.

LiteLLM stays in the design but demoted. For anything JBNX pays per token — API agents, batch jobs, the model gateway — issue a per-lease virtual key so metering is automatic and cost is real. That fixes the 0.3% path properly. It cannot fix subscription surfaces, and pretending otherwise is how you end up with a gate everyone walks around.

Layer 3 — Billing derives from the lease, not from agent honesty

Today an agent must remember to produce a receipt, and half the time does not.

Make it structural:

One lease → exactly one work_sessions row, enforced by a unique constraint on (agent_actor, project_slug, lease_id). Double-posting becomes impossible rather than forbidden.

visibly different from clean ones.

claim time**, not discovered at audit time. The directive already requires this; enforce it where the requirement is cheap to satisfy.

other three, so the four-way drift the directive warns about becomes a compile-time impossibility.

Layer 4 — The queue: claims become assignments

Today claiming is self-service — an agent picks a slug and takes it. That is why coordination is best-effort, and it is what the CEO asked to change.

Add dispatch.queue:

agent_ops.next_work() and portfolio.task_deps, which already lift blockers to the severity of what they transitively unblock — that logic exists and is good; Dispatch becomes its consumer.

from the current lease's expiry. claim_watchers (7 rows) already models this; promote it from a notification list to a real queue with ordering.

already detects these 49 times over. Today it records the collision. It should prevent it.

This is where "who is free" stops being a board a human reads and becomes a dispatcher that assigns.

Layer 5 — The reconciler: defects, named and owned

A pg_cron job every 10 minutes (alongside the existing take_default_actions()) that writes a defect row for each of:

Defect classDetectionCurrent count (14d)
unclaimed_workusage event with no live lease for that actor at that timestamp227
unregistered_actoractor code absent from the registry98 actors
missing_receiptproject claim with no overlapping work session322
silent_zerousage row with tokens > 0 and cost 0/null1,069
unbilled_slugsession on a slug absent from billing.projects or unassigned6
unapproved_prod_changeship-log write to a gated repo with no change_approvals row19
orphan_leaselease past expiry with no release0 ✅

Defects are attributed to the actor and the operator, batched per the existing ceo_action_batching policy, and surfaced on #/ops. Not one alert per defect — one standing count per class, with the trend. The number you care about is whether each class trends to zero after its gate lands.

Layer 6 — The face: seat #21 Dispatch

A skill file plus a seat in agent_ops.agents, reporting to 02 (Delivery Lead), escalating to 01.

What it does: answers "who is working on what, and what did they finish"; assigns from the queue; chases leases about to expire with work still open; drains blocked_work (the hourly resumer already exists); posts one consolidated handover per block per the push_batching policy; routes messages between agents through agent_ops.messages instead of agents inventing side channels.

What it must not do: grant an exception, waive a defect, decide anything classified ceo, or become a second attention list competing with Kaylee at seat #20. Kaylee owns your attention. Dispatch owns the agents' coordination. If they ever both claim the same surface, Kaylee wins and Dispatch loses the seat.


4. Threat model — how an agent would route around this, and what stops it

The honest test of a control plane is not the happy path.

Bypass attemptWhat stops it
Invent a new handle and claimRegistry check at claim; unknown actor quarantined, attempt logged
Register a throwaway handleRegistration requires a machine fingerprint + named operator; throwaways cluster visibly on one machine
Work without claiming at allCannot commit, deploy, migrate or write the portal. Thinking is uncontrolled; effect is not
Push to GitHub with a personal tokenReal residual gap — see §6. Mitigated by branch protection requiring the proxy as the only writer
Claim, then work on a different slugLease scope comes from slug_resources; the proxy grant is scoped to the repos of the claimed slug
Hold a lease open to look busySessions >12h auto-close and flag; open-lease duration is a customer-visible cost and is audited
Release without a statusRelease requires a --done string; empty or boilerplate is a missing_receipt defect
Record $0 usagesilent_zero defect with the actor's name; per-lease LiteLLM keys make it moot on metered paths
Ask Dispatch nicely for an exceptionDispatch cannot grant one. Exceptions are policy rows, written by 01, with a rationale
Deploy without chat approvalShip function refuses a gated-lane write with no change_approvals row

The remaining honest gaps are in §6. A design that claims zero gaps is lying.


5. Phase plan

Phase 1 — See it, name it, back-bill it (this week). Log-only; no agent can be stranded.

  1. Registry: pending/active/quarantined/revoked states, register endpoint, operator

approval path. Log unknown actors; do not refuse yet. Seed the ~26 seated agents as active.

  1. agent_ops.actor_tokens table + issuance for seated agents (enforcement waits for Phase 2).
  2. Controlled vocabulary for engine/model; backfill the fifteen Claude spellings.
  3. Reconciler pg_cron job + the defect views, including actor_signals scoring.
  4. Back-billing (decision 4): reconstruct the 322 receipt-less sessions, catalogue + assign the

6 unbilled slugs, produce the one-time cost report for the CEO.

  1. Billing RLS enabled on all 12 tables (§7A.6), with immediate verification of bill.jbnx.io.
  2. #/ops panel: defect counts by class and actor with trend; live actor board (heartbeat data

arrives Phase 2, board ships now with lease data).

  1. Seat #21 Dispatch registered; skill file written; read-only — reports, does not yet assign.

Exit criterion: §1's numbers on a dashboard and moving; reconstructed hours report in the CEO's hands.

Phase 2 — Close the gates (after Phase 1 runs clean for ~a week).

  1. Registry + token check flips from log to refuse on every /api/1099/* write. 14-day grace

for the 98 unregistered handles starts at Phase 1 launch, ends here.

  1. gh-proxy grant requires a live, scoped lease. Branch protection makes the proxy the only GitHub

writer — queued CEO action (changes how Joey pushes).

  1. Ship function requires lease + change_approvals row on all repos (decision 3 — the ungated

PROD-IT lane closes). Approval writes only via the admin portal surface (§7A.2).

  1. Lease trigger owns session open/close; unique constraint kills double-posting.
  2. Claim refused on a slug missing from billing.projects / customer assignment.
  3. Mandatory heartbeat with working_on; stale-heartbeat defect + release path.

Exit criterion: unclaimed_work, missing_receipt, unregistered_actor at zero; refusal count non-zero and falling — the proof the gate is load-bearing rather than decorative.

Phase 3 — Dispatch assigns.

  1. dispatch.queue + next returns an assignment; enqueue returns a position and ETA; aging

prevents starvation; preemption only for critical, only via policy.

  1. Resource conflicts queue instead of race.
  2. Per-lease LiteLLM virtual keys for metered paths.
  3. Dispatch drains blocked_work, chases expiring leases, posts consolidated handovers.
  4. Quarantine tier + dispatch.revoke_actor() kill switch live end-to-end.

6. Residual gaps — stated plainly

  1. Subscription inference cannot be gated. An agent in Cowork or Cursor can always think without

permission. Accepted: gate effect, not thought.

  1. A human with a personal GitHub token bypasses gh-proxy. Closing via branch protection is now a

queued Phase 2 CEO action (decision 5) — it changes how Joey personally pushes.

  1. The Supabase secret key bypasses everything. Anywhere that key reaches an agent, the plane is

advisory. mkt.jbnx.io holds an RLS-bypassing key on a marketing page today — it should lose it. Long-term close is Layer 2's dispatch.apply() RPC.

  1. Dispatch is a single point of failure. Mitigation: it is Postgres, not a running agent. If the

face dies, the plane keeps refusing correctly. Never invert that.

  1. Enforcement can strand agents. Every gate ships behind a policy flag in agent_ops.policy with

a documented one-statement rollback, and Phase 1 runs in log-only mode first for exactly this reason.

  1. The portal admin surface becomes the crown jewel. Once approvals, quarantine-clears and token

issuance all flow through it (§7A.2), compromising billing.admin_auth compromises the plane. That table is currently RLS-off (§7A.6) — enabling RLS is a Phase 1 prerequisite, and the admin session deserves 2FA before Phase 3.

  1. Reconstructed sessions are estimates, permanently. Decision 4 accepts this; the

source='reconstructed' label is the guarantee an estimate can never masquerade as a receipt.


7. Decisions — taken by the CEO, 2026-08-12

#DecisionRuling
1Unregistered actorsEvery actor must be registered, with a security mechanism against hackers and unknown AIs obtaining a lease. Registration is authenticated (see §7A.1); unknown actors are refused, and the refusal is logged and scored.
2Who posts billables and project updatesSomebody must — structurally. Ruling implemented as: the plane posts them. Session open/close is a database trigger on the lease, and the status/close-out clerk owns the project handover. No agent's memory is ever the mechanism. Dispatch (seat #21) chases the residue.
3Production changesAll production is gated and approved. Architected in — the two-lane model collapses to one: every repo gets a change_approvals requirement at the ship function, including the currently ungated PROD-IT lane (see §7A.2 for who can write an approval).
4Historical unbilled workBack-billed. Always. Cost visibility beats evidentiary purity. Mechanics in §7A.5: reconstructed sessions are created from claim→release timestamps, permanently labelled source='reconstructed', and never silently mixed with clean receipts — the invoice shows both, distinguished.
5Architecture callsDelegated — recommend best. Recommendations adopted: seat #21 reporting to 02; gh-proxy becomes the only permitted GitHub writer via branch protection (queued as a CEO action, since it changes how Joey pushes); billing RLS enabled (§7A.6).
6Second-pass gap reviewOrdered and done — §7A.

7A. Gap review (v2) — what v1 missed

Re-reading v1 against three tests the CEO set — handle the claim queue, know what every actor is doing, detect bad actors — six gaps. Each gets a fix and a phase.

7A.1 Registration without authentication is just slower forgery ⟶ actor tokens

v1's registry checked that a handle exists. A hacker registers a handle and is now "legitimate." Registration must issue a credential, not just a row:

(a human in portfolio.members, or seat 01/02 by policy) must approve it to active. This is the human gate that stops unknown AIs self-admitting.

(agent_ops.actor_tokens: actor, token_hash, created, expires, revoked, last_used). Tokens expire (default 90 days) and rotate on demand.

document, record-usage, facts. Reads stay ungated — the directive's availability principle holds for reads only.

from is a token_drift defect and an automatic quarantine until an operator clears it.

a burst of new pending actors from one source is itself a bad-actor signal (§7A.4).

Bootstrap: the ~26 seated agents get tokens issued at Phase 1 cutover by the operator. The 98 unregistered handles get 14 days of log-only grace to register or die.

7A.2 The approval gate had no trustworthy writer ⟶ approvals come from a human surface, never an agent

v1 said "ship refuses without a change_approvals row" but never said who writes the row. If an agent can write it, an agent will write it — "the CEO approved in chat" is unfalsifiable.

Fix: agents can request approval; only a human surface can grant it.

ceo_action_proposals row (table exists, 8 rows) with the PR URL, diff summary, and verify evidence.

delegate seat with the power, per agent_ops.policy) clicks approve on #/ops, authenticated by billing.admin_auth / portal admin session. The ship function checks for that row and nothing else.

and change_approvals stops being a 0-row table.

This also resolves the decision-3 ruling cleanly: with all production gated, the approval cost per ship is one click on a page Joey already reads.

7A.3 "Know what every actor is doing" needs liveness, not archaeology ⟶ heartbeat with a working-on line

v1 could tell you what actors did (handovers, ship log). The CEO asked what they are doing.

renew-lite with a mandatory working_on string (≤120 chars, plain language).

session. A lease silent for 30+ minutes is stale_heartbeat — Dispatch pings it; at 60 minutes the lease is releasable to the queue.

closed sessions, no interrogation of agents required.

7A.4 Bad-actor detection was implicit ⟶ a scored signal table and a kill switch

v1 quarantined unknowns but had no concept of a known actor going bad, or a probing attacker.

agent_ops.actor_signals — append-only, written by the reconciler and the gates:

SignalWeight trigger
Refused capability request (no lease / out of scope)each occurrence
Scope probing — grants requested for resources outside the held leasehigh
Handle burst — >3 new registrations from one machine/source in 24hhigh
Token drift — token used from an unregistered machineautomatic quarantine
Claim-without-work — lease held, zero usage, zero heartbeat contentmedium
Silent-zero repeat offenderlow, cumulative
Registration attempts against the rate caphigh

Score crosses the policy threshold → actor drops to quarantine tier: existing leases release, no new leases, no gh-proxy grants, no ship writes, portal writes refused; Dispatch notifies the operator. Kill switch: select dispatch.revoke_actor('<code>', '<why>') — one statement, deactivates the actor, invalidates tokens, releases leases, closes sessions. Runnable by 01, 02, or the portal admin surface. All of it lands in the defect feed with the trend.

External attackers (not agents at all): the write API refusing tokenless writes is the primary wall; signals catch the ones probing for the shape of a valid request.

7A.5 Back-billing mechanics (decision 4)

The 322 receipt-less claims get sessions reconstructed from claimed_at → coalesce(released_at, expires_at), with:

[reconstructed] so a customer reading the work log sees exactly what it is;

are written (6 known);

sees what the leak actually cost before any of it reaches an invoice;

defect — with the actor named, because after Phase 2 this class should not exist and each one is a gate failure to investigate.

Same treatment for spend: vendor_charges ($1,447.93 ever loaded) is stale against $1,670.71 of recorded 14-day model cost. Loading vendor receipts becomes a monthly reconciler check — recorded cost vs. bank-verified cost, with the gap as a standing defect.

7A.6 The billing schema is unlocked (found during the audit, out of v1's scope, in Dispatch's)

All 12 billing.* tables have RLS disabled — including work_sessions, admin_auth, auth_sessions, password_reset_tokens. Anyone holding the anon key can read or modify every row: customer credentials, session tokens, and the very billing truth this whole design is built on. A control plane whose ledger is world-writable is not a control plane.

Fix: enable RLS on all 12 with no policiesjbnx-bill and the portal talk to these tables via the service key, which bypasses RLS, so this is a lockout for the anon/authenticated paths only. Verify bill.jbnx.io login and the claim→session trigger immediately after. Staged in Phase 1 SQL, applied only with explicit approval, one-statement-per-table rollback.

Re-testing the three CEO tests against v2

plus aging (a queued request gains priority with wait time, so a chatty agent cannot starve a quiet one) and a preemption rule: only a critical-severity assignment may shorten a live lease, only via policy, never by another agent's request.

(what they are doing now) + handover (what they did) + defects (what they failed to do).

log as the tripwire that catches whatever the walls miss.


8. Verification

Each phase is done when its numbers move, not when its code merges.

-- the one query that says whether Dispatch is working
select class, count(*) from dispatch.defects
where at > now() - interval '14 days' group by class order by 2 desc;

Phase 1 passes when that query returns the seven classes with the counts in §1. Phase 2 passes when unclaimed_work, missing_receipt and unregistered_actor read zero and the refusal log is non-empty. Phase 3 passes when the median time from next to claim is under a minute and claim_conflicts stops growing.


v2, 2026-08-12: decisions taken, gap review complete. Phase 1 is approved for implementation; all schema changes stage for chat approval before touching production.