runward

RW™ · V0.43.0

Docs · Decisions · ADR-0088

ADR-0088.

ADR-0088-runwards-own-delivery-runs-under-a-delegation-charter-the-gate-reads.md

ADR-0088 — runward's own delivery runs under a delegation charter the gate reads: agents act under their own identity, the maintainer signs the policy and one weekly sample, not each act

Date: 2026-10-01 Status: accepted (2026-10-01) — the maintainer accepted this record as written: agents under their own identity, a charter signed once, a weekly signed acknowledgement, two stages, option (a) for runward's own regulated lock (decision 4), and the signed release tag as the authorization for a class I patch; stage 1 is the next work, and nothing is delegated before its pieces are in the gate Deciders: the maintainer Method: an inventory of every act in runward's delivery that requires the maintainer's hand, measured on origin/main@8a18489 with read-only gh api calls; the texts that bear on human approval read at source or at a named mirror on 2026-10-01; prior art from forges and large projects; the doctrine read against the code (src/lib/conformance.ts, src/commands/ratify.ts, src/commands/wire.ts, src/lib/harness.ts); a regulatory review and a security review of the draft the same day; then five platform measurements on a throwaway public repository, github.com/stranxik/runward-delegation-lab, with the GitHub App runward-steward (id 5150097) on 2026-10-01, quoted under "What the platform does, measured" Relates to: ADR-0039, ADR-0045, ADR-0050, ADR-0054, ADR-0065, ADR-0066, ADR-0080, ADR-0082, ADR-0084, ADR-0085, ADR-0086, ADR-0087 (proposed, PR #346) Would amend, by name, once accepted (none of these ADRs is edited by this one): ADR-0080, the "runward itself" paragraph; ADR-0082, decision 3 extended, for the single-accountable case only; ADR-0084, point 5, for advisory drafting; ADR-0085, decision 3 and its consequence "The only irreversible gesture of a release is the maintainer's", for patch releases only.

Context

What the maintainer said. On 2026-10-01: « c'est un système qui doit fonctionner sans ma main directe [...] c'est absurde de développer un système justement fait pour encadrer l'IA agentique et que je doive moi-même gérer cela manuellement » [it is a system that must work without my direct hand; it is absurd to build a system meant to frame agentic AI and then manage it all by hand myself]. This is, from the inside, the reevaluation trigger ADR-0080 names: ratification cost leading to bulk ratification without reading.

What the forge records today. All 277 merged pull requests not opened by Dependabot were opened and merged by the maintainer's account, with 0 reviews; median creation-to-merge 0.076 h. Since 2026-07-01, 823 commits carry the maintainer's name and 0 an agent's. 11 draft advisories were created in 8 seconds on 2026-10-01. .github/CODEOWNERS is * @stranxik. Every agent session on the maintainer's machine uses the same token. The forge cannot tell the maintainer's acts from an agent's: the attribution ADR-0082 forbids for ratification ("never under a human's name") already holds for merges, releases and advisories. While that token is within agent reach, no control in this ADR binds an agent: with it, an agent can give the CODEOWNERS approval, change rulesets, clear a stop, or bypass an environment wait.

What the human validation measured last week. runward's 46 rows were ratified on 2026-09-27 en bloc, "sample 18/46, sampled rows accepted 18/18": 28 rows were never displayed. A later entry (2026-09-30) is signed with a different spelling of the maintainer's name than the other five; by RWD-2026-0119 the trace cannot tell who ran it. By the same record, a script on a pseudo-terminal can produce a person-shaped trace: ratify's human path checks process.stdin.isTTY (ratify.ts:142), and agentRuntimeSignal(process.env) (harness.ts:110) is environment detection.

What ADR-0082 already allows, and its gap. ratify --agent <name> --for <person> records mode: agent, refused when the agent or its accountable person is the row's proposer. agentCause() (conformance.ts:756) compares free declared strings (declaredNameIn): --agent is any string, and --for thibaultsouris does not match a proposer Thibault Souris. Proposals do not record the proposer's accountable person. Under the regulated tier, ADR-0082 decision 3 extended counts agentRatification only "when its accountable person differs from the proposer" and "never relaxes ADR-0080 part 2: the forge approval still requires a human account, not a bot or an app". runward's own lock (runward/scaffold-lock.json) carries "regulated": true.

What ADR-0080 already refused. docs/operator-role.md: a "validated by" field in a mission artifact "would be re-signable by whoever writes the artifact — declarative, worth nothing to an assessor". A delegation charter is such an artifact. This ADR does not ask it to prove anything (decision 3).

What the platform does, measured (2026-10-01, github.com/stranxik/runward-delegation-lab, public, throwaway). The App runward-steward (id 5150097, private, owner stranxik) is installed on the lab repository only, with metadata: read, contents: write, pull_requests: write, repository_advisories: write, actions: read, checks: read, statuses: read, and no administration, workflows or variables. Its private key sits in the macOS keychain of the maintainer's user; installation tokens are minted per use and live one hour.

  • T1, a review. Branch ruleset on the default branch: pull request required, 1 approving review, no bypass. Pull request #1, opened by the maintainer's account, approved by runward-steward[bot] (REST; gh pr review over GraphQL errored with an App token), merged by the maintainer's account: merged, reviewDecision APPROVED, rule suite result pass. Control, pull request #2 with no review: merge refused, "At least 1 approving review is required by reviewers with write access." The App's approval satisfies a required review.
  • T2, a release. The App created a draft release for a tag v0.0.1 that did not exist, then published it (PATCH draft=false): release published, author runward-steward[bot]; the workflow on release: published ran with actor and triggering actor runward-steward[bot]. An installation token fires the event the GITHUB_TOKEN does not, so the App key alone can make a release's irreversible gesture and start everything ADR-0085 hangs on it, npm publication included.
  • T3, an advisory by the App. POST create: draft GHSA-vm9v-39qg-jf2c, state draft, author runward-steward[bot]; PATCH state=closed: closed. Publishing an advisory and requesting a CVE were not measured: either would write to the public Advisory Database.
  • T4, an advisory by the workflow token. A GITHUB_TOKEN with contents: read: list GET 200, create POST 403. A workflow token cannot create an advisory. (The lab note does not record the state filter or the content of the list call; ADR-0084's question on state=triage stays open.)
  • T5, a tag. Tag ruleset on refs/tags/v*, rule "creation", no bypass: the App creating refs/tags/v0.0.2 was refused ("Reference update failed"); control, the App creating refs/tags/lab-1 succeeded; the App publishing a draft whose tag v0.0.3 did not exist was refused ("Cannot create ref due to creations being restricted"); the maintainer's account creating v0.0.4 was refused too. A no-bypass tag ruleset stops the App and the maintainer alike.
  • A fact about one agent runtime. During the lab, Claude Code's own permission classifier refused, unprompted, an agent approving through the App a pull request the same agent had opened, a merge without review even as a control, reading the App key, and making a repository public; the maintainer ran T1's approval and its control by hand. This is a property of one harness's configuration, which runward neither ships nor can read, and no decision below relies on it.

What follows from the measurements. From T2: the custody of the App key is a precondition of any delegation, not a hardening step; whoever holds the key holds the release. From T1: if the author and the reviewer are two identities whose keys one process can read, the forge's "approved" is one actor approving itself; the reviewer's key must be under a custody separate from the author's, and even then, with one accountable person, the forge's "approved" is not independent and must not be presented as such. From T5: a no-bypass tag ruleset cannot serve a chain where the maintainer pushes the tag (ADR-0085 decision 1); an admin-role bypass keeps the App out but protects only once agents no longer hold the maintainer's credential, because with it an agent is the admin. From T3 and T4: advisory drafts come from the App, never from the GITHUB_TOKEN; App publication and CVE requests rest on documentation until measured.

What the texts require. Read 2026-10-01; EUR-Lex refused automated fetches, so DORA was read on an unofficial mirror and RTS art. 17 on springlex.eu.

What prior art does (read 2026-10-01). Prow (https://docs.prow.k8s.io/docs/components/plugins/approve/approvers/): for /lgtm, "Authors of the PR cannot give the label, but they can cancel it"; approve lets an author in OWNERS approve their own files by default (secondary source; the approve-plugin page is a placeholder). Chromium Rubber Stamper (https://chromium.googlesource.com/infra/infra/+/refs/heads/main/go/src/infra/appengine/rubber-stamper/README.md) approves narrow benign classes and "never provides OWNERS approval, by design". Copilot coding agent (https://docs.github.com/en/copilot/concepts/agents/coding-agent/risks-and-mitigations): commits authored by the agent, the requester co-author and barred from approving; Copilot reviews do not count toward required approvals by default, an administrator can let them count since 2026-09-01 (recorded in ADR-0080), approvals in public preview (https://docs.github.com/en/copilot/concepts/agents/code-review). Apache lazy consensus (https://community.apache.org/committers/lazyConsensus.html): intent stated "on a public email", "usually 72 hours", "Silence indicates consent". Anthropic (https://www.anthropic.com/engineering/claude-code-auto-mode): "Claude Code users approve 93% of permission prompts". OpenAI, "Practices for Governing Agentic AI Systems" (https://cdn.openai.com/papers/practices-for-governing-agentic-ai-systems.pdf): §4.2 opens "Some decisions may be too important for users to delegate to agents [...] (such as independently initiating an irreversible [...] transaction)" and lists the "rubber stamp" among its open questions; §4.4: "Actions that can only be reviewed after the fact should be more easily reversible than those that require approval." Class I below answers this with a signed prior authorization, never silence.

Not read. CRA primary text; ISO/IEC 42001 and 27001; ITIL 4; PCAOB AS 2201; GitHub's CNA review practice; npm's terms on automated publishing; npm trusted-publisher environment binding; GitHub environment admin-bypass documentation.

Decision

Agents act under their own identity, inside a charter the gate reads; the maintainer's recurring act is one signed weekly commit. It starts in two stages, and every surface says which stage holds.

  1. Two stages, displayed.

    • Stage 1, now. Delegated acts run under the App; the maintainer's credential and the App key are still within agent reach. Every surface (check, report, the charter header, README) prints « delegation: declared, not proved — the maintainer's credential is within agent reach ». Classes R and H stay the maintainer's, class I is not delegated (releases and advisories as ADR-0085 and ADR-0084 decide them today), class D is delegated. Stage 1 adds attribution; it adds no protection, and says so.
    • Stage 2, provable. Agent sessions run under a separate macOS user (or a container) that holds no maintainer gh token (at most a fine-grained read-only one) and cannot read the App keys; trust-root acts happen only from a 2FA web session or a commit signed by the maintainer's hardware key (sk-ssh-ed25519, touch required). The gate checks what it can: every commit on main after the cut-over is the App's (web-flow signature verified) or signed by a key in allowedSigners; an agent-side probe (gh api user fails or returns the App) is shown as self-declared. Only the hardware-key signature is evidence an agent cannot produce.
  2. Four classes of acts.

    • R, trust roots, the maintainer's: signing, renewing, widening and revoking the charter; forge and admin settings; allowedSigners; lock opt-ins; arming the gate (ADR-0065, unchanged); constitutional ADRs (verdict path or ADR-0054 crossing, AGENTS.md boundaries, SECURITY.md promises, the public-claim ceiling, the charter, superseding an accepted decision).
    • H, judgement owed to a third party, the maintainer's: a reported vulnerability, its acknowledgement, a CVE request, embargo coordination, and any CRA art. 14 notification if runward is in scope.
    • I, irreversible public acts, delegated in stage 2 only, covered by a signed prior authorization, never by silence: patch releases whose diff contains no constitutional ADR, no shipped-code merge not yet covered by a signed digest, and no dependency version younger than N days, with checks green, the ratchet held and the four assets verified. Advisory publication joins class I only after the CRA scope determination and the measurement of App publication (What would settle it); until then it stays human. Minor and major releases remain the maintainer's gesture.
    • D, reversible or internal, delegated: merges outside the pre-merge list (decision 7), agent ratification of rows, non-constitutional ADRs, Dependabot merges (with cooldown), defect-register entries, documentation syncs, advisory drafts by the App (T3; never by the GITHUB_TOKEN, T4), golden regeneration.
  3. Identities and key custody. Delegated acts run under runward-steward, installed on runward's repository with the lab's permission set and never administration, workflows or variables. Because the App key alone can publish a release (T2), its custody is part of the decision: in stage 2 it leaves the agents' reach (a signer the agents' OS user cannot read, preferably KMS/HSM with a per-App policy). Because the App's approval satisfies a required review (T1), a review counts only from an identity whose key is under a custody separate from the author's: preferably a third-party- hosted reviewer whose key no local process holds (Copilot review, if the administrator enables it), otherwise a second App under separate custody, outside any agent's filesystem, the operator-layer agent's included. In stage 1 no App review is presented as a review. Invariant: no App in any bypass list. Every act carries « by: (agent) · for: (accountable) ». README and GOVERNANCE state that the forge's "approved" is not independent with one accountable person: the forge UI, ruleset audit and SLSA tooling do not carry runward's disclosure.

  4. Independence fixed, and never claimed where it does not exist. propose records the proposer's accountable person; accountable persons are compared by canonical id (forge user id plus aliases); by: is bound to the git identity that committed the entry (the App bot, web-flow signature verified), not a free string. A ratification whose accountable person equals the proposer's is refused, except as agent (single accountable):

    • in a default mission it counts, disclosed on every surface as « single accountable person; agent ratifications are disclosed throughput, not independent approval »;
    • under the regulated tier it is refused by default (ADR-0082 decision 3 extended stands). For one case only this ADR would amend it: it counts when the lock names the exception ("singleAccountable": "<canonical id>") and the period is covered by a passing signed sample; otherwise it is a named strict gap. Every surface then prints « not a DORA change-approval control; ADR-0080 Part 2 unchanged ».
    • For runward's own mission, chosen: (a) the named amendment to ADR-0082 above and the exception declared in runward/scaffold-lock.json. The others: (b) leave the regulated tier on runward's own mission, losing the strictest dogfooding; (c) keep the agent rows as named strict gaps, which turns runward's own strict gate red or sends the maintainer back to ratifying in person. The maintainer chose (a) on 2026-10-01.
  5. A charter the gate reads, which proves nothing by itself: runward/delegation.md. Delegates, the accountable person as a canonical id, classes, scopes, budgets, sample size, expires: (at most 90 days). Guarded by CODEOWNERS with required code-owner review, the only file guard a public repository has, void while stage 1 holds. The answer to operator-role.md: the charter is declared and the gate says so; what carries evidence is the sample ledger, signed by a key only the maintainer holds and verified offline (decision 6). The gate prints « charter: declared, not proved; samples: signed by (presence shown, reading declared) ». A renewal is shown as a diff against the last signed charter; any widening (new scope, smaller sample, shorter window) needs its own signed commit and cannot ride a renewal.

  6. One weekly signed act: sample, digest, acknowledgement.

    • Population from git alone: first-parent merges on main, tags, ratification entries since the last signed sample; counts reconciled; population hash published.
    • Draw: fixed, stratified, risk-weighted, 5 acts plus 1 seeded defective item; randomness from a source fixed after the period closes that no agent can steer (a pre-announced drand round), never from the mission-state digest. The seed is committed as H(seed, nonce) before the sample and revealed after, so an auditor can re-perform the control.
    • One commit signed by the maintainer carries the sample outcome, the 100 % digest of shipped-code merges (decision 7) and, in stage 2, the authorization of that week's class I patches (decision 8). The gate verifies it offline against allowedSigners. This is the remaining human act and is named as such.
    • Missing the seed records control missed: the sample does not count. A rejected act suspends that class for that delegate. Seeds also measure the reviewer; its catch rate is a published counter.
    • In stage 1 the signing key is within agent reach: entries print « sample: declared human, not proved » and never unlock class I.
  7. The system comes to the maintainer; silence is never consent; escalation is deterministic.

    • Push notifications for: sample ready, pre-merge escalation, charter expiring within 14 days, minor release ready. A delivery receipt is shown as « delivered, not read », is an agent's attestation when the operator-layer agent sends it, and is never consent.
    • Before merge, the maintainer's pre-approval: src/lib/{conformance,evidence,verdict}*, src/commands/{wire,ratify,propose}.ts, src/lib/harness.ts, plugins/runward-gate/hooks/**, .github/** (CODEOWNERS and workflows included), package.json (scripts, prepublishOnly, dependencies), lock opt-ins, AGENTS.md, SECURITY.md, README claims, the charter, allowedSigners; disagreement between agents; a check red then green on re-run; a ratchet drop; any gate-bypass.log line.
    • After merge, 100 %, in the weekly digest: every other merge touching a file in the published tarball (package.json files, templates/, plugins/). No patch release may contain one before a signed digest covers it.
    • One missed week: « unsampled since » everywhere. Two missed periods: check --strict turns red on any class D act since the last signed sample, and agent ratifications stop counting.
  8. The release window, built on what was measured. For a class I patch (stage 2), the prior authorization is the annotated tag vX.Y.Z signed by the maintainer's hardware key and pushed in the weekly session. The v* tag ruleset carries an admin-role bypass, so the maintainer can push it and the App cannot (T5; a no-bypass ruleset would refuse the maintainer too). ADR-0085's prepare run makes the draft; the App may then publish it (T2: the published event fires for its token). The publish job verifies the tag's signature against allowedSigners before npm publish and refuses otherwise, so the App key alone no longer reaches npm. Minor and major releases keep ADR-0085 decision 3 as is.

  9. Stop. A repository variable or ruleset toggle only an administrator can change, read by a required check whose source is pinned to the GitHub Actions app; the escalation budget (3 consecutive refusals, 20 per period) is counted by that check, not by the agent. Revocation by uninstalling the App. Exercised on a schedule, result committed under signature. In stage 1 an agent holding the maintainer's credential can clear it; the surfaces say so.

  10. Public claims narrowed first (ADR-0050 claims guard), before any delegation: AGENTS.md:39 "The maintainer merges"; GOVERNANCE.md roles; README.md:166 "publishing to npm is a deliberate human gesture (creating the Release)" and "The deterministic executes; the human decides the crossing. The discipline is demonstrated, not claimed." Added: one accountable person; agent ratifications and forge approvals are disclosed throughput, not independent approval; not a DORA change-approval control; the SLSA Source level once measured, the two-person level out of reach with one person.

Implementation order. (1) This ADR decided, including decision 4's option. (2) Claims narrowed (decision 10). (3) CLI, each in its own pull request: canonical ids and the agentCause() fix, propose recording the accountable person, by: bound to the committing identity, the charter reader and its stage banner, the git-built population, the signed sample with commit-reveal. (4) The App installed on runward from a 2FA web session, CODEOWNERS, the stop, Dependabot cooldown; class D on, in stage 1, with its banner. (5) Stage 2: separate OS user, hardware key in allowedSigners, the App key and the reviewer's key under separate custody, the v* tag ruleset with admin-role bypass, the tag signature check in publish. (6) SLSA Source level measured; CRA scope determined. (7) Class I patches after four consecutive weeks of hardware-signed samples with no control missed.

Alternatives discarded

  • Keep the maintainer's hand on each act. Measured as a login, not a gesture; bulk ratification left 28 of 46 rows unseen.
  • Agents act under the maintainer's account (the status quo, never decided). The attribution ADR-0082 forbids; it also voids every human control, since the human identity is within agent reach.
  • Wait for stage 2 before delegating anything. Cleaner, and it keeps the attribution defect open for as long as the separate user and the hardware key take; stage 1 fixes attribution now and says it proves nothing.
  • Agent ratification without a sample (ADR-0082 as is). No human measurement, and the free-string independence check reads independence where one person answers for both sides.
  • Sampling a share of acts (20 %). At September's volume, 20 or more items a week at about 30 seconds each: the rubber stamp it claims to prevent.
  • Silence windows for irreversible acts. Counter to the cited OpenAI guidance; a delivery receipt does not show the notice was read.
  • An App review counted as independent approval. T1 shows the forge would accept it; with one accountable person and, in stage 1, one custody for every key, it is one actor approving itself.
  • A no-bypass tag ruleset. Measured to stop the maintainer as well (T5), which breaks ADR-0085 decision 1.
  • A second human approver staged for appearance. ADR-0080 Part 2 already refuses it.
  • Full automation including all releases and third-party security judgement. Irreversible acts and duties owed to a reporter or a CSIRT need a human.
  • A machine user with a long-lived token instead of an App. Allowed by the terms, but a long-lived secret; fallback only.
  • Pre-merge escalation of every shipped file. About 60 of 96 September merges; the bottleneck restored. Replaced by pre-merge on gate-critical paths plus a 100 % weekly digest gating the patch envelope.

Consequences

  • This is not a DORA change-approval control, and runward does not present it as one. Under the regulated tier ADR-0080 Part 2 (forge approval by a human account) is unchanged for adopters; the single-accountable exception counts only when named in the lock and covered by a signed sample.
  • Stage 1 changes who the forge names, not what anyone can do. Until stage 2, an agent can still act as the maintainer; the banner is the control, and it is a disclosure.
  • The maintainer's recurring work: one signed weekly commit (6 items, the shipped-code digest, the patch tags in stage 2), pre-merge escalations on gate-critical paths, about four minor-release gestures a month, constitutional ADRs, quarterly renewal, third-party and CRA duties. Weekly minutes are not estimated; they are published after four weeks.
  • Every delegated act is attributable to a named agent and a canonical accountable id; no surface presents an agent approval as a human one, nor the forge's approval as independent.
  • The evidence changes meaning, and says so: "it matched a policy the maintainer signed, a second declared identity under the same accountable person validated it, and a weekly sample signed by the maintainer's hardware key passed a seeded control an auditor can re-perform".
  • Adopters can use the charter, the signed sample and the disclosure in a default mission; under the regulated tier they gain nothing that relaxes ADR-0080 Part 2.
  • Cost: a separate OS user for agents; a hardware key; an App and a reviewer under separate custody; a notification channel; CLI changes to propose, ratify and the conformance reader.
  • Risk accepted: the charter is written by the audited party; it is declared, and the gate says so.
  • No CLI change and no runward/delegation.md ship with this ADR.

What would settle it

  • Measurements still missing, each on the lab repository first: an App publishing a repository advisory and requesting a CVE (both write to public databases, so a dedicated test advisory is needed); a v* tag ruleset with an admin-role bypass refusing the App and admitting the maintainer; whether a CODEOWNERS required review is satisfied by an App; whether GitHub refuses an App approving a pull request the same App authored; disabling admin bypass on the release environment; npm trusted publishing bound to an environment; a GITHUB_TOKEN on state=triage (ADR-0084's open item); runward's SLSA Source level.
  • Stage 2 holding: every commit on main after the cut-over attributable to the App or to an allowedSigners key, and the agents' OS user unable to read either App key.
  • Four weeks of class D with signed samples: share of control missed, rejected acts, escalation volume, measured weekly minutes; the reviewer's catch rate on seeds.
  • The CRA scope determination, before advisory publication enters class I.
  • Decision 4's option chosen by the maintainer for runward's own lock.

Reevaluation trigger (mandatory, dated)

Reopen on 2027-01-01, or earlier on: a sampled act rejected twice in one class; control missed twice in a row; stage 1 still in force three months after acceptance; a second maintainer joining (independence becomes possible); a supervisor statement on agents as the approving function under RTS art. 17(1)(b); GitHub changing whether App reviews count toward required approvals, or taking Copilot approval out of preview; the CRA scope determination concluding runward is in scope, or paid pilots starting.

Trigger set on: 2026-10-01 · Watched via: the weekly signed sample (its rejections and control missed), the stage banner on check, the GitHub changelog at each release's runbook step 2.

Note, 2026-10-02: the gate reads time from the tree, never from a clock

Recorded so the choice lives in the record, not only in the code. The verdict path reads no clock (ADR-0054, same working tree, same verdict), and two decisions above name dates:

  • Charter expiry (decision 5). check compares expires: only with the dates the tree declares: an agent ratification entry dated after expires: is a charter-expired gap. A charter that lapses with no act after it reads the same before and after the date; runward doctor, outside the verdict path, sets expires: beside today's date and warns 14 days ahead (docs/delegation-charter.md).
  • Missed periods (decisions 6 and 7). Periods are effective: plus multiples of sample-period-days:. The samples that count form a chain ending at the last one's end. The gate's only "now" is the latest date a ratification entry in the tree declares; every period ended on or before it after the chain's end is missed. One missed period prints « unsampled since »; two make agent ratifications dated on or after the chain's end strict gaps, and under the regulated tier stop them counting. runward doctor and runward sample read the same periods against today.

What this costs, said as it is: a tree in which nobody declares anything after a period ended shows no missed period, whatever the calendar says, and entry dates are declared by whoever writes them. Both are readable in the diff; neither is a proof. The randomness of the sample is recorded the same way: the drand round is recomputed from the period's end and its randomness from its signature, offline; the signature itself is not verified against drand's public key in the gate (a pairing library would enter the verdict path), so the gate prints « randomness: declared ».

References

← All ADRs