Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .abcd/development/brief/02-constraints/02-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@ with the external tool available as an **opt-in adapter** over the same seam
present tool is an upgrade, never a prerequisite — a missing or misbehaving
adapter degrades to the native path rather than breaking abcd.

## The runner route's fence (proposed)
## The runner route's fence

**Proposed, awaiting the product thinker's sign-off.** One route is planned to
need an external program: the agents abcd's runner launches run inside a fence
**Signed off by the product thinker on 2026-10-22** (dcn-2610222041417934). One
route needs an external program: the agents abcd's runner launches run inside a fence
the operating system enforces, and that fence is Anthropic's sandbox-runtime
(`srt`, which needs Node, and on Linux bubblewrap and `socat`), installed by
the person
([adr-2610161621415708](../../decisions/adrs/2610161621415708-abcd-s-command-guard-steers-agents-an-os.md),
spc-2610161751056096). It is adr-22's first declared exception, scoped to the
runner route: a fence has no native default to degrade to, so a machine
without one refuses a runner launch, and the person's per-machine off switch
in `~/.abcd.noindex/config.json` takes the place of degradation. Nothing of it
is built yet, and every other route still needs no external tool.
in `~/.abcd.noindex/config.json` takes the place of degradation. Where `srt` is
missing, abcd offers to install it and installs only after the person's
explicit yes (dcn-2610222041424027). Nothing of it is built yet, and every
other route still needs no external tool.

## Adapters over native defaults

Expand Down
1 change: 1 addition & 0 deletions .abcd/development/brief/02-constraints/03-invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,4 @@ The following are non-negotiable invariants — any architectural choice that vi

22. **A private advisory stays private until it is published, and abcd acts on GitHub for it only on an approval per act** — a private security advisory's record never enters the public ledger while the advisory is unpublished: it lives only in the embargo store, a detached worktree in the machine-scoped worktree store that is never committed, and every verb that touches the store refuses one that is on a branch, tracks a private record, or lies inside the checkout; nothing public carries an unpublished advisory's id, its reserved CVE id, its title or the location of the private records, and the outbound check refuses an artefact that does, naming where without repeating it; and publishing or closing an advisory on GitHub happens only on the person's explicit approval of exactly the plan or text shown, one approval per act, never supplied by an agent on its own, refining [adr-44](../../decisions/adrs/0044-remote-mutation-and-caller-identity.md)'s confirmed remote write. Ruled by [adr-2610120558328615](../../decisions/adrs/2610120558328615-a-private-advisory-s-record-never-enters.md), accepted by the product thinker on 2026-10-12. Consumed by itd-2610112239022766 (`abcd embargo`).
23. **No work-in-progress branch reaches the default branch, and a receipt-less push is admitted only for a host connector's declared work-in-progress prefix** — a cloud session abcd starts pushes its work, checked or not, only to `<wip-prefix><item-id>`, where the prefix is declared by that host's connector (never named in the core or the hook), ends in the path segment `abcd-wip/` with at most one segment before it, and is one entry in the pre-push hook's declared list of receipt-less prefixes, with its own commit shape (a valid record id after the prefix, every pushed commit passing the attribution gate) and this rule as its record; every other branch still needs a preflight receipt. A required check on the default branch's ruleset refuses any pull request or merge-queue entry whose source branch holds the segment `abcd-wip` or whose diff adds a cloud report file, and the work reaches the default branch only through the person's own collect, which re-runs preflight on a copy on their computer and mints the receipt there; no receipt minted in the cloud is trusted. The hook is not the boundary (`--no-verify` skips it); the required check, the ruleset and the collect are. Ruled by [adr-2610120937435856](../../decisions/adrs/2610120937435856-a-cloud-session-s-unchecked-work-goes.md), decided by the technical facilitator on the product thinker's delegation of 2026-10-12. Consumed by itd-2610120738075547 (work handed to cloud sessions).
24. **A session's checklist names a credential only by its kind, scope and expiry, never its value or anything that retrieves it** — the checklist a session keeps on disk, which is re-injected at every start and after every compaction and read by every later session in that working copy, records a credential it needs only as three typed fields: its kind, what it reaches, and when it expires; it never holds a secret, a part, hash or encoding of one, or a pointer that hands one to its reader (an environment variable's value, a key file's path, a keychain item's name, a command that prints a token). The checklist writer runs every field through abcd's native secret scanner and refuses a write that matches, naming the field and the pattern kind without echoing it, and the reader refuses to re-inject a file that matches, falling back to the last good copy; the scanner catches the shapes it knows, and the rule binds whatever it misses. Ruled by [adr-2610222026265719](../../decisions/adrs/2610222026265719-a-session-s-checklist-names-a-credential.md), the rule ruled by the product thinker on 2026-10-22, its enforcement decided by the technical facilitator on their delegation. Consumed by itd-2610170638184486 (every session keeps a checklist).
2 changes: 2 additions & 0 deletions .abcd/development/brief/06-delivery/03-out-of-scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ That is what keeps "not hand-counted" true after the day it was written.
- `itd-2610161710316538` — Drains and builds a person's session drives report how isolated they were
- `itd-2610170634394993` — Headless agents in parallel coordinate through the shared remote, not conversation
- `itd-2610170638184486` — Every session keeps a checklist on disk, so no run stalls and no stall spreads
- `itd-2610222029228127` — Every session's checklist reaches a shared copy and the phone
- `itd-2610222215485265` — abcd drain --drive works through the ledger from a plain terminal
<!-- /index -->

**Later-phase items with no intent id.** These four were written into the brief
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ itd-47, the intent that rested on the same bundled cascade:
rather than breaking abcd.
- The doctor's job narrows from policing foreign bypass artifacts to checking
abcd's own health and which adapters are wired.
- **Proposed, awaiting the product thinker's sign-off (2026-10-16): the first
declared exception.** The fence around the agents abcd's runner launches
- **The first declared exception (proposed 2026-10-16, signed off by the
product thinker on 2026-10-22, dcn-2610222041417934).** The fence around the agents abcd's runner launches
([adr-2610161621415708](2610161621415708-abcd-s-command-guard-steers-agents-an-os.md),
spc-2610161751056096) is Anthropic's sandbox-runtime (`srt`), an external
program abcd does not ship. On the runner route alone it is a requirement,
Expand All @@ -93,5 +93,6 @@ itd-47, the intent that rested on the same bundled cascade:
fence exists to remove. A machine without it refuses a runner launch, and
the person's per-machine off switch (`runner.sandbox.off` in
`~/.abcd.noindex/config.json`) takes the place of degradation. Every other
route still needs no external tool. Until the sign-off this consequence is
a proposal, not part of the decision.
route still needs no external tool. Where `srt` is missing, abcd offers to
install it and installs only after the person's explicit yes
(dcn-2610222041424027).
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ We will decide limiting the network as a separate, later record.

## Consequences

- The machine-load gate and every `pgrep`-style waiter run in abcd, outside the boundary: inside a sandbox `ps` sees only the sandbox's own processes, so a check run there would read a busy machine as clear.
- The machine-load gate and every `pgrep`-style waiter run in abcd, outside the boundary: inside a sandbox the process table is not the machine's: on macOS `ps` cannot start at all and `pgrep` sees no process (the spec's step 1 evidence, 2026-10-17), and elsewhere it may see only the sandbox's own, so a check run there cannot read the machine.
- A run without a boundary is the person's choice, recorded in their user layer under `~/.abcd.noindex/`; no repository file can make it for them.
- Network limiting is deferred to its own record. When it comes, its honest claim is a declared list of hosts, which narrows mistakes and does not stop code being sent elsewhere: allowing the forge allows a push to any repository the credential reaches.
- Drains and builds a person's own session drives stay unconfined by abcd; reporting their isolation is proposed as a separate part of the intent.
- The guard keeping its place inside the boundary depends on iss-2610161612203555.
- Platform limits become the intent's scope conditions: Go tools' TLS under Seatbelt, git over SSH behind a proxy, the keychain credential helper, `sandbox-exec`'s deprecation on macOS, nested bubblewrap, and Ubuntu's restriction on unprivileged user namespaces.
- A brief invariant states the rule once this record is accepted.
- The boundary sits behind a seam (`internal/core/fence`): a fence-neutral policy and one interface each mechanism implements, so the mechanism the spec picks (sandbox-runtime, `spc-2610161751056096`) can be replaced by another without changing the runner or the loop.
- The mechanism is a program abcd does not ship, and a boundary has no native default to degrade to, so on the runner route it is a requirement: [adr-22](0022-bundled-deps-as-pluggable-adapters.md)'s first declared exception, proposed until the product thinker signs it off, with the person's off switch in their user layer in place of degradation.
- A signing key is not opened to a confined agent. Where the person's commits are signed, the spec proposes that a confined agent commits unsigned and abcd signs outside the boundary when it lands the work, a question for the product thinker.
- The mechanism is a program abcd does not ship, and a boundary has no native default to degrade to, so on the runner route it is a requirement: [adr-22](0022-bundled-deps-as-pluggable-adapters.md)'s first declared exception, signed off by the product thinker on 2026-10-22 (dcn-2610222041417934), with the person's off switch in their user layer in place of degradation.
- A signing key is not opened to a confined agent. Where the person's commits are signed, a confined agent commits unsigned and abcd signs outside the boundary when it lands the work (the product thinker's ruling of 2026-10-22, dcn-2610222041439951).
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
id: adr-2610222026262980
slug: coordination-between-headless-agents-is
status: proposed
date: 2026-10-22
supersedes: null
superseded_by: null
related_intents: [itd-2610170634394993, itd-2609091034175565, itd-2609151838312703]
related_rfcs: []
related_adrs: [adr-2610222026265719]
---

# ADR-2610222026262980: Coordination between headless agents is bounded by deadlines, fenced by the claim's tip, version-checked, and done under each session's own credential

## Context

The coordination intent (itd-2610170634394993) replaces the messages
sessions sent each other on 2026-10-16 with records on the shared remote that
any session reads and acts on. Its decomposition, confirmed by the product
thinker on 2026-10-17 (the calibration note under that date), routed four
rules to an ADR rather than to the intent, because each one is a boundary
other work must not cross, not a feature:

- That evening the load gate deadlocked between two builds of its own queue
rule (iss-2610161634043273) and no wait timed out for eight hours.
- A claim taken over from a dead holder can be acted on by that holder if it
comes back, unless a later act re-checks that the claim is still its own
(Chubby's sequencers, Kubernetes leases; the research note
`2026-10-17-parallel-headless-agent-coordination.md`).
- Two sessions on different abcd versions read each other's records; an
older reader that guesses at a newer record can wait on it forever.
- Every session on the machine used the person's own forge login, which
holds admin rights, so any session could change a setting meant for the
person alone (the reservation cap, captured on 2026-10-17). The product
thinker ruled the same day that autonomous sessions never work under that
login, and on 2026-10-22 that unattended runs happen only where the login
is absent (C1) and that each session's own App token ships with the
coordination (C3).

The planning prep of 2026-10-18 settled the mechanics each rule rests on
(dcn-2610180720366282,
dcn-2610180720397970, dcn-2610180720379765, dcn-2610180723332655): the generation number is the claim
branch's tip, every expiry is derived when the records are read, and the
credential is a GitHub App token minted for the person.

## Decision

Four rules bind every coordination act abcd makes, by any verb, in any
session:

1. **Every wait has a deadline.** A wait on a slot, a dependency, a claim, a
merge or another session ends by a stated time, and a wait that reaches it
ends in a refusal that names what it waited on and for how long. No wait
is open-ended, and no deadline depends on a process staying alive: each is
derived from the records when they are read.
2. **A claim is fenced by its tip.** A push of the work branch or a tag
that abcd makes under a claim travels in one atomic push with a renewal
of the claim branch, so if the claim's tip moved the forge refuses the
whole push. A queue entry cannot share a push: abcd re-reads the claim's
tip immediately before it enqueues and names the pull request's head it
expects, which leaves a window of one call that this rule states rather
than claims closed. A push made outside abcd is not fenced, and the rule
does not claim it. A claim whose holder stopped
renewing is retaken only after its expiry plus a fixed delay.
3. **A reader refuses a coordination record newer than it knows.** From the
release that adds a version to the coordination records, a reader meeting
a version above its own refuses the record with a message naming both
versions, and never waits on it or guesses its meaning. Readers released
before that ignore the field, which is why the rule starts there.
4. **An autonomous session never acts under the person's own forge login.**
It pushes, opens pull requests and reads the forge with a short-lived
token issued through abcd's GitHub App for that session, limited to the
repository and the acts its work needs. The token reaches only the git
and forge commands abcd runs for the session, through a credential channel
of abcd's own, never the forge CLI's token variables, and the key or
refresh token that issues it is never readable by a session. An
unattended run starts only where the person's saved login cannot be
reached (a cloud session, a container or a separate user account). Before
one starts, and before its token is issued, abcd reads the signals that
carry no secret (the forge CLI's login status by exit code, whether the
token variables are set, whether a stored-credential file names the host)
and refuses to start, naming where it found a login. It never asks a
credential helper, so a login kept only in a keychain is out of its sight,
and the refusal says that rather than calling the machine clean.

## Alternatives Considered

- **These four as one trust rule (chosen).** They are the four ways
coordination by records fails silently (a wait that never ends, a stale
holder acting, a reader guessing, a session holding more than it needs),
and each is checkable at the act.
- **A coordinator process holding locks (etcd, a Temporal server).**
Rejected in the state-of-the-art pass: out of scale for one repository,
and a process that must stay alive is the failure 2026-10-16 showed.
- **Leave deadlines and fencing to each verb.** Rejected: the load gate's
eight-hour deadlock was exactly a verb that had none.
- **Say plainly that the person's login is still within reach, rather than
run where it is absent.** Offered to the product thinker on 2026-10-22 and
not chosen: a caveat is not a guarantee.

## Consequences

- Every coordination verb (land, build, drain, status, reserve, the load
gate) carries a stated deadline and a named refusal at its end; a test per
verb watches it end.
- abcd's pushes and tags carry the claim's renewal atomically, and its queue
entries a tip re-check; a session whose claim was taken over cannot push
or tag under it through abcd, and can enqueue only inside the one-call
window.
- The coordination records gain a version field in the release that ships
this; the refusal binds from then on.
- Unattended runs need a setup the person does once: the App registered in
the browser (C3), and a place to run without their login. abcd states that
as a step only the person can take.
- Stated in the brief's invariants once this ADR is accepted, which happens
with the product thinker's sign-off of the intent's plan.
Loading
Loading