open-standards/jcp/jcp-rs/crates/jcp-orchestrate/MAPPING.md
Transaction Science b77b9d70f6
All checks were successful
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 11m52s
jcp-orchestrate: steps wait for steps, and a retry needs a name for the work
The loop was a flat queue of independent tasks routed by kind. A flat queue is
a work pool, not automation: nothing could express "transform after fetch, and
never publish because fetch failed". `Task::after` names the steps a task waits
on, and only an item whose every dependency is `Done` is dispatched.

An edge is one task id naming another in this run, resolved against the run's
own journal — not an endpoint in someone else's system. That is the whole point
of putting the graph here rather than in an integration fabric: the fabrics put
their edges across trust boundaries they do not own, which is where their
failures live, and an edge that does not cross a boundary cannot fail at one.

A step whose dependency did not complete is settled `WorkState::Blocked`, naming
what it waited on and why that did not happen. Blocked is deliberately not
Denied: a denial is a decision about a grant and carries a sealed receipt, while
a block is a missing precondition — no harness was offered the task, no grant was
consulted, nothing was spent, so there is nothing to seal. The blocking pass
runs to a fixed point, because settling one step is what determines why the next
can never run; a single pass would have to call a long chain behind one failure a
cycle, which it is not. A real cycle lands there too, so the loop still
terminates on any graph a caller can submit.

`Status::Retry` was taken at its word: the harness said transient, so the loop
ran the task again. But transient describes why the attempt failed, not whether
it left anything behind — a harness can fail after its effect has landed, and the
second run then pays for and performs that effect twice. The crate already
refuses that bet for `Interrupted`; it was taking it here. A retry is now
honoured only for a task carrying `Task::idempotency_key`, and refused otherwise
with that reason recorded. An attempt bound was never a substitute: it limits how
many times an effect may be duplicated, it does not make duplicating it safe.

SPEC §4.11 gains the rule. It already governed a decision recorded with no
outcome; this is the neighbouring case its own reasoning implies — an outcome
recorded as "run it again" — and it requires the key to be part of the durable
record, so a restarted enforcement point can tell a replay-safe action from one
that merely has attempts left.

Both the graph edges and the replay identity are journalled with the task, so a
resumed run rebuilds the graph and not a flat queue. The example is now a
five-step pipeline, submitted in reverse so the ordering cannot be an artefact of
the queue: the ship is denied for want of a capability and the announcement
behind it is never started. jouledesk gains the matching `TagOutcome::Blocked`,
which seals no efficiency receipt — there were no joules to account for.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-12 07:43:11 -05:00

179 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# The background-agent orchestrator pattern → `jcp-orchestrate`
`jcp-orchestrate` does not invent an orchestration shape. It **ingests** the one
the agent-work ecosystem converged on — a background-agent orchestrator that runs
coding agents in the cloud — and supplies the three things every deployed
instance of that pattern lacks: a **capability gate**, a **joule budget**, and a
**sealed audit**.
This document maps the pattern — as built by the production background-agent
orchestrators — onto (1) this crate's reconcile loop, (2) the Sandbox S0–S7
isolation ladder its container model sits low on, (3) the Joule Table seams its UX
surfaces never carried, and (4) the JCP grant its credential model stops short of
— then scopes the frontier the whole category leaves empty.
## 0. Why map a commodity pattern
The background-agent orchestrator is a well-worn shape: a tracker of work items;
each item run by a coding agent in an isolated container on its own repo copy,
auto-branched, checkpointed to a forge as machine-authored commits and pull
requests; a CLI for local handoff that doubles as an MCP server; bring-your-own
subscription or API keys; recurring and event-triggered automations; chat and
forge @-mention bindings. Several products shipped exactly this. The shape is not
in dispute and not a moat — it is commodity.
What none of them produced is the durable artifact: a **signed, energy-accounted,
verifiable receipt** of what ran, under whose authority, for how many joules.
Everyone can build the orchestrator; the accountability is the open lane. This
crate keeps the loop and makes each dispatch a JCP decision made **outside** the
agent.
## 1. The reconcile loop → `Orchestrator` (ingest, not compete)
The pattern's control plane is a tracker → route → dispatch → retry loop over a
*heterogeneous* fleet, where a worker is one coding-agent backend on one item and
a different backend on the next. `Orchestrator` keeps that loop and gates every
dispatch.
| Background-agent orchestrator concept | `jcp-orchestrate` symbol | What TX adds |
|---|---|---|
| Work tracker, one branch per task | `Task { id, kind, payload, blocked_by, idempotency_key }` + the sealed decision ledger (`receipts()`) | The run is an **auditable ledger of sealed decisions**, not just a branch list |
| A coding-agent backend (any CLI/framework) | `Harness` — any backend that quotes a joule cost and reports an outcome | The orchestrator is **blind to what runs inside**; backends are `register`ed, not hardcoded |
| Assign a task to a backend | `dispatch` **decided against a grant, outside the backend** (`route` → gate) | Who-runs-what is a runtime authorization, never the agent's own choice |
| Retry on failure | bounded retry **+ failover through a sealed denial** to a backend still under budget | An exhausted backend is denied *on the record*, then fails over; a flaky one retries to `max_attempts` |
| Bring-your-own subscription / API key | `Capability` (task kinds) **+ `budget_uj`** per backend | Authority, not just authentication — see §4 |
| Unbounded agent loop (the documented cost failure) | joule budget **debited + enforced before the side effect** | A hard, pre-side-effect energy ceiling per grant |
| Machine-authored commit / PR as the record | JCR-1 receipt sealed per decision, allow **and** deny | The **non-repudiable audit** a git history cannot be |
Net: the pattern is *what the loop looks like in production*; `Orchestrator` is
*the same loop with the gate, the budget, and the sealed audit the meta-harness
leaves to the platform*. The `background_agents` example drives it end to end.
**What is and isn't sealed.** Every decision made *against a grant* is sealed —
the allow, and the deny when a backend is out of budget or out of scope. Budget
enforcement in particular must leave an artifact: a joule ceiling that fires
silently is an enforcement nobody can audit, so a backend that cannot pay is
denied through the runtime and that denial is on the record *before* the loop
fails over to the next. The one refusal that seals nothing is a task whose kind
**no** registered backend holds: there is no grant to decide against, and JCP
seals decisions about a grant, not the absence of one. Such an item is recorded
denied in the `RunReport` and never reaches a backend.
## 2. Container isolation → Sandbox S0–S7 (attested execution)
The pattern isolates each task in a container with its own repo copy. On the
Sandbox ladder that is roughly **S2–S3**: namespace/cgroup isolation, no attested
boot, no measured execution, no energy-counter provenance. The container keeps
tasks from colliding; it proves nothing to a third party about what ran.
| Pattern isolation | Sandbox rung | The step up |
|---|---|---|
| Container, own repo copy | ~S2/S3 (namespace isolation) | Integrity asserted by the host, attested to no one |
| — | **S5** — microVM (proven on real bare-metal KVM + RAPL) | A hardware-isolation domain with a measured boot; a receipt can name the image that ran |
| — | **S6** — TEE attestation (`sandbox-tee::energy`) | An `EnergyStatement` bound into a TEE report's `report_data`; the honest `IntegrityAttested` vs `PhysicallyRooted` trust ladder |
The move is not a better container. It is running the *same* task inside an S5
microVM under an attested boot, and binding the energy reading's provenance into
the receipt. The pattern's isolation stops at "tasks don't collide"; the ladder
continues to "a third party can verify what ran and what it cost."
## 3. The UX surfaces → Joule Table seams (gate / meter / receipt)
The surfaces the pattern converged on are the right surfaces — they are simply
unmetered, ungated, and unsealed. Each rebinds through an existing binding, and
each gains the three seams the bare orchestrator never had.
| Pattern surface | Joule Table binding | The three seams it gains |
|---|---|---|
| Local-handoff CLI + MCP task server | **Joule Hook** (editor/host binding) | gate = grant on task creation; meter = joules per task; receipt = MCP receipt server |
| Chat / forge @-mention kicks off a task | **Joule Desk** (teammate binding) | signed-grant authority for the mention; a per-receipt-verifiable ledger of what the teammate did |
| Recurring + event-triggered automations | **Durable agent execution** (Sandbox + JCP) | durable resumable runs, env-manifest identity, standing grants with a joule ceiling, mid-run steering |
| Browser streaming / notifications | mid-run steering over an attested run | monitoring becomes attested control |
The pattern proved the surfaces convert. It never made any of those actions
accountable. The seams are exactly that missing accountability.
## 4. Credentials → JCP grant (authentication → authority)
Bring-your-own subscription answers **who is paying** — an identity with a quota
at a vendor. A JCP grant answers **what this principal may do, for how many
joules, decided where the model can't reach**: a capability and an energy budget
are the same signed, bounded, debited object. A vendor key lets an agent run
until the bill stops it; a grant denies the action itself — an out-of-scope task
kind, or one over the joule ceiling — *before* the side effect, and seals both the
allow and the deny. Auth says "the card works"; the grant says "this action, this
budget, this receipt."
## 4a. The integration fabric → edges inside one stack
The adjacent category is the workflow-automation tool: a canvas of nodes wired
into a graph, where each node reaches an endpoint in a different company's
system. Those products are worth taking seriously on shape and not on structure.
Their value *is* the bridging, so their failure surface is the bridges — a
renamed field, a rotated credential, a partial write, a rate limit mid-run, an
approval one system believes the other checked. The dominant open tool in the
category drew 237 published advisories in eighteen months, 22 of them critical,
including five approval-forgery or bypass advisories in a single month. That is
not a quality problem to out-engineer; it is what a graph whose edges cross trust
boundaries nobody owns costs to run.
`jcp-orchestrate` takes the graph and not the bridges. `Task::after` names the
steps a task waits on, and an edge is **one task id naming another in this run**,
resolved against the run's own journal. A step that waits on a step that did not
complete is settled `Blocked`, naming what it waited on — distinct from `Denied`,
which asserts a decision about a grant was made and sealed. Nothing crosses a
boundary this stack does not own, so the whole class of boundary error has
nowhere to occur, and the approval that gates a dispatch is a signed grant
decided outside the harness rather than a message one service hopes another
validated.
| Integration-fabric concept | `jcp-orchestrate` symbol | What changes |
|---|---|---|
| Node graph with branch/merge | `Task::after` → dependency edges, `WorkState::Blocked` | Edges name tasks in this run, not third-party endpoints |
| Retry on a failed node | `Status::Retry` gated by `Task::idempotency_key` | A retry needs a name for the work (SPEC §4.11); without one it is refused, not risked |
| ~400 app connectors + hosted OAuth broker | *deliberately absent* | Connectors are product surface, not a standards problem; the stack's own capabilities are first-class harnesses |
| Approval step in the canvas | JCP grant decided outside the harness, sealed allow **and** deny | An approval that cannot be forged between two services, because there are not two services |
The connector count is the category's moat and is not contested here. What is
contested is the premise that automation must be assembled out of other people's
interfaces at all.
## 5. The frontier the pattern leaves open
The background-agent orchestrators shipped every convenience and none of the
trust layer. The empty lane is the one no instance of the pattern occupies: a
**per-run, per-task, energy-accounted, attested, third-party-verifiable receipt**
of agentic work — `efficiency/1` rolled up from JCR-1 decisions, with the energy
counter's provenance attested through `sandbox-tee`. No competitor issues it;
regulation mandates nothing per-task; the category publishes rates, not
outcome-anchored receipts.
## 6. A conformant flow (the profile in one paragraph)
Register each coding-agent backend as a `Harness` with a **capability** (the task
kinds it may handle) and a **joule budget**. Track work items as the pattern does;
run the reconcile loop, but make every dispatch a JCP decision against that grant,
**sealed as JCR-1 before the task runs**. Run the task inside a **Sandbox S5
microVM** under an attested boot, with the energy reading's provenance bound via
`sandbox-tee::energy`. Roll the per-task receipts up with `aggregate()` into an
`efficiency/1` figure (`uj_per_accepted_token`, waste share, outcome anchor).
Rebind the surfaces — local-handoff/MCP through **Joule Hook**, chat/forge through
**Joule Desk**, automations through **Durable agent execution** — each carrying
gate / meter / receipt. The result is the pattern's exact UX, with the one thing
it structurally could not produce: a signed, energy-accounted, verifiable record
of every action.
## Status
- Reconcile loop + capability/budget gate + sealed ledger — shipped (`src/lib.rs`).
- Step dependencies (`Task::after`, `WorkState::Blocked`) and the idempotency
precondition on retry (SPEC §4.11) — shipped (`src/lib.rs`), with the graph
edges and the replay identity carried in the journal so a resumed run rebuilds
the graph and not a flat queue.
- End-to-end demonstration — shipped (`examples/background_agents.rs`), a
five-step pipeline where the ship is denied for want of a capability and the
announcement behind it is never started.
- Sandbox rung integration (run the harness inside an S5 microVM) and the
`efficiency/1` roll-up — the composition this document scopes; the primitives
(`sandbox-tee::energy`, `jcp-efficiency::aggregate`) are already shipped in their
own crates.