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.
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.
All figures are the trailing 14 days unless noted.
| Measure | Value |
|---|---|
| Actor codes that consumed model tokens | 101 |
Agents in the org chart (agent_ops.agents) | 26 |
| Actor codes using models that are not in the org chart | 98 |
| Usage rows from unregistered actors | 1,200 of 1,218 (98.5%) |
| Actor codes that took a claim and are not in the org chart | 76 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.
| Measure | Value |
|---|---|
| Usage events with no live claim by that actor at that moment | 227 of 1,218 (19%) |
| Project claims with no overlapping billing session at all | 322 of 658 (49%) |
| Billing sessions on a slug with no customer assignment | 6 |
| Sessions shorter than 60 seconds | 103 |
| Open leases right now / expired-but-unreleased | 3 / 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.
| Measure | Value |
|---|---|
Usage rows recorded at $0 or null cost | 1,069 of 1,218 (88%) |
| Tokens priced at zero | 55.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.
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.
Worth stating plainly, because the redesign should not touch it:
slug_resources + claim_conflicts already catch cross-slug resourcecollisions 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.
decide_by capped at 12h with a mandatory default_action and a 10-minute 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.
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:
a faster answer. The incentive gradient runs away from compliance on every single turn.
re-learned or not, per session, forever.
The unwritten receipt has no consequence the agent can perceive.
/api/1099/* is ungated by design. Thatis 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.
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.
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.
The lease already exists and already expires. Give it two new properties:
slug_resources (whichrepos, which Railway services, which Supabase schemas). This already exists for conflict detection; reuse it as the capability scope.
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.
Four chokepoints. Two already exist and need a precondition added; two need building.
| Chokepoint | Status | Rule to add |
|---|---|---|
| gh-proxy (repo reads/writes) | built — 29 grants/14d | Refuse 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/14d | Same lease check, plus refuse when change_approvals has no matching approval row for a gated-lane repo. |
Portal write API (status, document, facts) | ungated | Registered actor required. Status/close-out already implies a lease; make that explicit rather than assumed. |
| Supabase service-key writes | ungated | Route 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.
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.
close_reason = 'expired' so those hours arevisibly different from clean ones.
billing.projects or unassigned to a customer is refused atclaim time**, not discovered at audit time. The directive already requires this; enforce it where the requirement is cheap to satisfy.
CREDITS_PER_HOUR = 6000 lives in one place and is imported by theother three, so the four-way drift the directive warns about becomes a compile-time impossibility.
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:
next and gets an assignment, not a menu. Ranking comes from 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.
enqueue and gets a position and an ETA 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.
claim_conflictsalready 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.
A pg_cron job every 10 minutes (alongside the existing take_default_actions()) that writes a defect row for each of:
| Defect class | Detection | Current count (14d) |
|---|---|---|
unclaimed_work | usage event with no live lease for that actor at that timestamp | 227 |
unregistered_actor | actor code absent from the registry | 98 actors |
missing_receipt | project claim with no overlapping work session | 322 |
silent_zero | usage row with tokens > 0 and cost 0/null | 1,069 |
unbilled_slug | session on a slug absent from billing.projects or unassigned | 6 |
unapproved_prod_change | ship-log write to a gated repo with no change_approvals row | 19 |
orphan_lease | lease past expiry with no release | 0 ✅ |
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.
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.
The honest test of a control plane is not the happy path.
| Bypass attempt | What stops it |
|---|---|
| Invent a new handle and claim | Registry check at claim; unknown actor quarantined, attempt logged |
| Register a throwaway handle | Registration requires a machine fingerprint + named operator; throwaways cluster visibly on one machine |
| Work without claiming at all | Cannot commit, deploy, migrate or write the portal. Thinking is uncontrolled; effect is not |
| Push to GitHub with a personal token | Real residual gap — see §6. Mitigated by branch protection requiring the proxy as the only writer |
| Claim, then work on a different slug | Lease scope comes from slug_resources; the proxy grant is scoped to the repos of the claimed slug |
| Hold a lease open to look busy | Sessions >12h auto-close and flag; open-lease duration is a customer-visible cost and is audited |
| Release without a status | Release requires a --done string; empty or boilerplate is a missing_receipt defect |
| Record $0 usage | silent_zero defect with the actor's name; per-lease LiteLLM keys make it moot on metered paths |
| Ask Dispatch nicely for an exception | Dispatch cannot grant one. Exceptions are policy rows, written by 01, with a rationale |
| Deploy without chat approval | Ship 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.
Phase 1 — See it, name it, back-bill it (this week). Log-only; no agent can be stranded.
pending/active/quarantined/revoked states, register endpoint, operator approval path. Log unknown actors; do not refuse yet. Seed the ~26 seated agents as active.
agent_ops.actor_tokens table + issuance for seated agents (enforcement waits for Phase 2).engine/model; backfill the fifteen Claude spellings.pg_cron job + the defect views, including actor_signals scoring.6 unbilled slugs, produce the one-time cost report for the CEO.
#/ops panel: defect counts by class and actor with trend; live actor board (heartbeat dataarrives Phase 2, board ships now with lease data).
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).
/api/1099/* write. 14-day gracefor the 98 unregistered handles starts at Phase 1 launch, ends here.
writer — queued CEO action (changes how Joey pushes).
change_approvals row on all repos (decision 3 — the ungatedPROD-IT lane closes). Approval writes only via the admin portal surface (§7A.2).
billing.projects / customer assignment.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.
dispatch.queue + next returns an assignment; enqueue returns a position and ETA; aging prevents starvation; preemption only for critical, only via policy.
blocked_work, chases expiring leases, posts consolidated handovers.dispatch.revoke_actor() kill switch live end-to-end.permission. Accepted: gate effect, not thought.
queued Phase 2 CEO action (decision 5) — it changes how Joey personally pushes.
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.
face dies, the plane keeps refusing correctly. Never invert that.
agent_ops.policy witha documented one-statement rollback, and Phase 1 runs in log-only mode first for exactly this reason.
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.
source='reconstructed' label is the guarantee an estimate can never masquerade as a receipt.
| # | Decision | Ruling |
|---|---|---|
| 1 | Unregistered actors | Every 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. |
| 2 | Who posts billables and project updates | Somebody 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. |
| 3 | Production changes | All 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). |
| 4 | Historical unbilled work | Back-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. |
| 5 | Architecture calls | Delegated — 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). |
| 6 | Second-pass gap review | Ordered and done — §7A. |
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.
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:
pending and returns nothing usable. A named operator (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.
/api/1099/ requires the token* once Phase 2 flips: claim, status, release,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.
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.
change_approvals is already append-only by trigger. Keep it that way.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.
v1 could tell you what actors did (handovers, ship log). The CEO asked what they are doing.
heartbeat_at. Make heartbeat required while holding: every 15 minutes, renew-lite with a mandatory working_on string (≤120 chars, plain language).
#/ops) shows: actor → slug → working_on → last heartbeat age → tokens this 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.
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:
| Signal | Weight trigger |
|---|---|
| Refused capability request (no lease / out of scope) | each occurrence |
| Scope probing — grants requested for resources outside the held lease | high |
| Handle burst — >3 new registrations from one machine/source in 24h | high |
| Token drift — token used from an unregistered machine | automatic quarantine |
| Claim-without-work — lease held, zero usage, zero heartbeat content | medium |
| Silent-zero repeat offender | low, cumulative |
| Registration attempts against the rate cap | high |
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.
The 322 receipt-less claims get sessions reconstructed from claimed_at → coalesce(released_at, expires_at), with:
source = 'reconstructed', meta.reconstructed_at, meta.claim_id — permanent, queryable label; [reconstructed] so a customer reading the work log sees exactly what it is;
billing.projects get catalogued and customer-assigned before the sessionsare written (6 known);
sees what the leak actually cost before any of it reaches an invoice;
missing_receiptdefect — 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.
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 policies — jbnx-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.
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.
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.