ADR-0093.
What runward shows is assembled from files the CLI writes and never raises; a customer-run satellite verifies and displays; a hosted product is a separate product
Date: 2026-10-13 (revised the same day after two adversarial reviews)
Status: accepted (2026-10-13) — the maintainer accepted the umbrella architecture and its four
decisions, each as recommended: (1) two declared populations, the sample's unchanged and the
landings', with a published reconciliation; (2) the hosted product is a separate product, with its own
repository, record and licence, built in D6, while the customer-run satellite runward-fleet (MIT, no
editor key) stays; (3) a mission snapshot as an optional subject of each record, measured in D2 before
any public sentence on offline re-derivation; (4) OSCAL keeps component-definition. Constitutional,
class R under ADR-0088 decision 2: it names new reads against ADR-0054's crossings, adds a fourth git
reader, and amends ADR-0011, 0039, 0041, 0054, 0064, 0080, 0088, docs/spec/runward-signing.md and
docs/spec/runward-oscal-mapping.md by dated note, each note landing one delivery ahead of the code
that needs it. An agent wrote this record; the maintainer decided (ADR-0088 decision 4, applied to
decisions as to rows)
Deciders: the maintainer
Method: a French synthesis of three architecture studies, a forge-landscape note and a GitLab gap
analysis (all of 2026-10-13, outside this repository), revised against a doctrine review and a product
review whose findings were each checked on a fresh clone at main@958d068 (runward 0.43.1):
src/lib/attestation.ts (missionStateDigest), src/commands/verify.ts, src/commands/sample.ts,
src/lib/delegation-sample.ts (populationHash), src/commands/gate-hook.ts,
test/unit/runtime-boundary.test.js, eslint.security.config.js (SPAWN_ALLOWED), the ADRs named
above, the signing, OSCAL mapping and witness specifications, and the actual output of
check --strict --json. Kosli's attest custom and create attestation-type reference pages were
read on 2026-10-13. Every week figure below is an estimate (plus or minus 30 %), not a measurement
Relates to: ADR-0011,
ADR-0021,
ADR-0039,
ADR-0041,
ADR-0054,
ADR-0055,
ADR-0064,
ADR-0065,
ADR-0080,
ADR-0088,
ADR-0089,
ADR-0090,
ADR-0092;
the three D0 records it opens, ADR-0094,
ADR-0095
and ADR-0096,
all proposed
Context
runward emits, for one instant, almost everything a steering committee, an auditor or a security
operations team asks about: check --strict --json, the witness, the unsigned attestation and
verify, SARIF, the HTML report, the OSCAL component definition. Missing are time (no verdict is kept
from one commit to the next), the population (the complete list of changes that landed in a period,
which an IT change-controls auditor starts from) and agent activity (gate-hook records only its
releases, in runward/gate-bypass.log).
The target is the full product: pull-request and merge-request summary, population export, period report, event stream, organisation view, connectors, and a hosted offering with long history, cross-repository search, alerts, role-based access and real-time agent activity. Zero human time by default. Finished and demonstrable before any customer feedback. Never overclaim.
Four constraints shape it. runward is self-sufficient: it never assumes Kosli, GitHub, internet access or any third-party tool; connectors are optional. GitLab is first-class, often self-managed and possibly offline, at parity with GitHub; organisations are multi-forge. The change request in the ITSM (ServiceNow, Jira Service Management) is the incumbent, so runward deposits evidence on it and replaces nothing. The verdict path stays as ADR-0054 draws it.
A first draft of this record made four claims the code does not support:
implemented: machineby equality between a record's subject digest and the landed commit. Measured:missionStateDigesthashesrunward/and the evidence files the manifests cite, nothing else; it is not a commit identity, and the record's commit fields are unsigned CI variables.- A vendor-operated multi-tenant service holding vendor-managed keys, called an ADR-0039 satellite. ADR-0039's amendment of 2026-08-24 says an adjacent product that makes runward operate a process, or whose aggregation is not the operator's, "is not a satellite: it is a different company's product".
- "One population" shared with
sample draw. Measured:sample.tsbuilds its population fromgit log --first-parent --mergesby committer date, plus tags and ratification entries, with no patch id;populationHashis re-checked bydelegation-sample.ts, whichverdict.tsimports. - Offline re-performance of a row without the repository. Measured:
verifyre-derives the verdict from the current working tree.
This revision corrects all four and the major findings that came with them.
Decision
The MIT CLI writes files under versioned contracts and never raises a cell above ci-declared,
never signs, never verifies a signature and never reads a clock for a state. It reads git in exactly
one new module, ledger, outside the verdict path. A customer-run satellite, runward-fleet, verifies
signatures against the operator's trust policy, raises cells it can prove, and displays; it never
re-judges and is never the only copy. A vendor-operated hosted product, if built, is a separate
product under its own record, not a satellite, and nothing in the core depends on it.
The four questions put to the maintainer, and the answers (2026-10-13, each as recommended).
| # | Question | Answer | Where it lives below |
|---|---|---|---|
| 1 | One population re-unified by a class R record, or two declared populations | two declared populations; the sample's stays unchanged in the verdict closure; the reconciliation is published | decision 4 |
| 2 | The hosted product as a separate product, built in D6 | yes: its own repository, record and licence; runward-fleet stays a MIT satellite and suffices alone |
decision 10 |
| 3 | The mission snapshot as an optional subject of each record | yes, optional, measured in D2 before any public sentence on offline re-derivation | decisions 6 and 8 |
| 4 | OSCAL: keep component-definition or move to assessment-results |
keep component-definition; reopen on a GRC tool that reads only assessment results |
decision 9, the OSCAL amendment |
The self-sufficient core. Every function of the CLI and the static satellite works with no forge API, no network, no account and no third-party tool. A deliverable that needs a third-party tool is a connector and is never a prerequisite of another deliverable.
The forge adapter boundary. The verdict and the file renderings (SARIF, Code Quality JSON, a JUnit XML of the verdict, Markdown) are forge-neutral. Fact readers, posters and packaging are per forge and live in CI-step scripts outside the verdict's import closure, behind one port (
readChangeFacts,postSummary,setExternalStatus,identifyChange) with documented closed behaviour. GitHub and GitLab ship at parity in every delivery; Azure DevOps and Bitbucket ship a pinned pipeline template and a summary poster. "Parity" describes what runward ships, never what a forge tier allows: on GitLab Free, approval independence is only detected after merge, and every surface says so.The ITSM deposit. A CI step attaches a
runward-change-evidence/1file (row, records, Markdown summary, HTML report, non-scope sentence, re-derivation command) to a ServiceNow change request or a Jira Service Management issue and adds a work note or comment. It never writes an approval state and never closes a request. The join key is a change-request identifier declared in the PR or MR description or aChange-Request:trailer; absent, the row readschange_request: not-declared. Offline, the file alone is the deposit.Two populations, declared. The charter's sample population (ADR-0088 decision 6) is unchanged and stays in the verdict closure. The landing population,
runward-landing-population/1, is new: every first-parent landing between two commits (merge, fast-forward, squash), each withsha,parents,patch_idand a declared forge change identifier. A published reconciliation lists everymergeact of a sample period that is absent from the landing population of the same range, with its reason. They are not claimed to share a hash.ledger, the fourth git reader. One module, fifth entry ofSPAWN_ALLOWEDafterhooks.ts(which runs operator commands) and the three git readerscharacterize.ts,doctor.ts,sample.ts.ledger populationrefuses a shallow clone, pins git configuration (core.quotepath=false,diff.renames=false,core.abbrev=40, diff attributes neutralised), passes--no-ext-diff --no-textconv, defines a merge'spatch_idasgit patch-id --stableover the diff between its first parent and itself, and writes the git version, the bounds, the list, its hash and the command that reproduces it.ledger replayjudges past trees of a commit range with today's runward, read-only from git, and writes records labelledorigin: replayed,judgedBy: runward@<version>. It works on any repository on the day runward is installed, with no CI change. A replay never fills a verb.ledger record,ledger export,ledger verifyandperiodare pure functions of files.
The contracts. All borrow the canonical encoding of
docs/spec/witness.md§3 and read an unknown value asnot-observed; they borrow nothing else, and are not witnesses.runward.dev/record/v1(unsigned in-toto statement):originisundeterminedfor every record the CLI writes andreplayedfor a replay; repository, ref,commitand itscommitSource,run_id,run_attempt, pipeline source; subjects: payload, witness, attestation and, opt-in, a mission snapshot (the exact filesmissionStateDigesthashes, at their paths); frozen tree facts:gate-bypass.logline count and digest,wire_journal_last(the last line ofadapters/installed.log, declared, never "armed"), charter digest,delegation.stage, and the assurance mode anddegradedflag copied fromcheck --json(not-observeduntil ADR-0090 surfaces them there; measured absent today);implementedat mostci-declared;signatureVerified: false.runward-change-row/1: stable join keys for the customer's DORA metrics (landing commit, PR or MR identifier, landing date with its source, declared change request); every record of the landing withrun_id,run_attempt,originand signature presence (a missing earlier attempt is the findingrecord-attempt-missing); six verbs withstate,source,ref; emergency; releases; replay kept apart; afaceand aprovenanceblock.runward-period/1: bounds in commits, dates as labels; counts per verb and state, each with its denominator and the list it counts; findings withclass,impactand an emptyentity_severity, never combined;effectiveness: not-assessed-by-runward;management_approvalempty; previous period by digest.runward-event/1:stream.{id,seq,prev}, closedtypelist,assurance: "declared",hash.- Cell states:
machine-established(set by the satellite only),ratified,ci-declared,declared,refused,no-record,not-observed,not-available. The earlier namemachineis dropped because it collides with ADR-0090's modemachine. Under that mode, zero ratification displays "machine mode in force", never a gap. - Emergency: a landing with no passing pre-merge record. A harness gate release is not an emergency change; it is shown as a release.
- Prerequisite I0: the per-row ratification list and
causesByKindincheck --json, re-derived byverify. Additive, but it changescheck --jsonand touches files on the charter's pre-merge paths, so it goes through the pre-approval path, not a delegated merge. Writtenrows[]in the studies; measured onmain@958d068,ratification.rowsis already the count of ratified rows (48 on runward's own mission), so the list takes another key, named in ADR-0095.
The event stream. Off unless
--events <file>orRUNWARD_EVENTSis given. The path is refused when, symlinks resolved for the file and its parent, it lies in the verdict's read set or in the working tree. One file per stream id, created exclusively, then appended by the same writer, which reads only its last line. The side-channel test covers both import rings (dist/commands/check.js,dist/commands/gate-hook.js) and the five exit paths ofgate-hook(misconfigured, no mission, clean, release, refusal): standard output and exit code byte-identical with and without the stream, and with a planted writer that throws.events verifydetects interior deletions only; tail truncation is detected only once the head is shipped to a store the agent's credentials do not administer. Expected streams: one perrun_idcited by records; harness sessions are not expected, and views say so. Prometheus textfile is a second output format of the same writer, declared outside the view rules (an operational signal, never evidence).gen_ai.evaluation.resultis never produced, by the CLI or by a recipe.Integrity, honest per level and charter stage. N0, unsigned, is the default and complete. N1, signature by the operator's CI identity, is opt-in. The CLI never verifies; the satellite verifies against the operator's
trust-policy.json. The satellite raisesimplementedtomachine-establishedonly when the certificate's commit and ref claims equal the landing SHA and protected ref, and only when the trust policy pinsrefs/heads/mainand the workflow file digest. Under ADR-0088 stage 1 every artifact is declared; in stage 2 CI-signed artifacts become evidence an agent cannot produce only under those pins (decision 1). End-of-turn events stay declared at every stage. The strongest public phrase is "unalterable after shipping to a store the agent's credentials do not administer". Without the tree, a third party performs offline inspection; re-derivation requires the tree at the cited commit or the mission snapshot. No reference to a strongest level of audit evidence is made.Connectors never make a customer tool say "compliant". The GitHub check run is an option of the Action, carrying the gate's own exit code under the name "runward gate" with the non-scope. GitLab is one module with two modes:
step(outbound external-control update) andendpoint(external status checks, served by the satellite container); it publishes the raw verdict, and the mapping topassorfailis a policy file the customer writes; without it the module refuses to post. Kosli:kosli attest customwith a runward attestation type shipped with--schemaonly and no--jqrule; any rule is the customer's, and Kosli's compliance result is then Kosli's evaluation of the customer's rule;attest decision --compliantis not used. How Kosli displays a type with no rule is to be checked before the recipe ships. SIEM ingestion is a file and a tested Collectorfilelogrecipe. OSCAL stays acomponent-definition.The satellite and the hosted product.
runward-fleetis a satellite under ADR-0039 as amended: its own repository, MIT, run in the operator's CI (static site) or installed by the customer as a signed OCI image with an SBOM and no outbound call (ingestion, append-only store, rebuildable index, alerts, RBAC, cross-repository search, real-time agent activity from the stream, the GitLab endpoint). It holds no vendor key, imports no runward module, reads no repository, invents no field and can be rebuilt from the customer's export. A vendor-operated multi-tenant hosted product with vendor-managed keys is not a satellite; it gets its own repository, its own record and its own licence, consumes the same exports, and the CLI knows neither its URL nor its existence.View rules. Two display layers: a face (one sentence and
n / Nper state) and a provenance appendix (digest, signature state, date and its source, age, re-derivation command), tied by a golden test. Eighteen rules, each with a test a planted mutation reddens: no state computed by a view; face as a function of fields; states never summed, no composite percentage; gaps and silent repositories not filterable by default; silence is a finding;replayednever mixed with other origins; no cross-version comparison without a marker; no strong state carried by time; the CLI verifies no signature;machine-establishedonly by the satellite under pinned policy; a repository cannot sign for another; management-body approval never set by software; the side channel never touches the verdict; machine mode shows "machine mode in force"; a harness release is never an emergency; bypass review is disclosure by default and strict only when opted in; join keys stable; no connector posts "compliant" or "pass" without the customer's rule.Build order, in deliveries, each ADR landing one delivery ahead of its code (weeks are estimates):
Delivery Content (GitLab woven in) Weeks Acceptance D0 this record; forge adapter boundary; per-row ratification contract; Code Quality and JUnit renderings; offline GitLab CE test bench 1.5 records proposed; bench starts with network offD1 I0; report --format md;--codequality,--junit; Action summary and check-run option; GitLab component, catalogue and integrity-pinned file; MR note; docs, harness and charter sweep; per-tier enforcement guide; Azure and Bitbucket templates8.5 byte-identical Markdown on both forges; every number a JSON path of the payload; install on offline GitLab CE D2 ledger population,replay,record(N0 default, snapshot and signing opt-in),export,verify; ITSM deposit8 shallow clone refused; replay fills no verb; deleted attempt surfaces; offline re-derivation from the snapshot measured D3 periodand HTML; bypass review; event writer,events verify, Prometheus format; SIEM recipes; GitLab audit recipes; Pages8 byte-identical period; planted throwing writer leaves output identical; third-party checker agrees D4 static runward-fleet; trust policy; signature verification;machine-establishedon GitHub and GitLab; cosign andactions/attestrecipes, offline verification6 rules in tests; no runward import; unpinned policy raises nothing D5 satellite container; GitLab two-mode module; Kosli type; forge-neutral independence rule with GitHub and GitLab readers; Azure and Bitbucket posters 10 no outbound call; rebuilt from export; GitLab module refuses without policy D6 hosted product, separate repository and record 7 tenant isolation; export replayed in a customer container gives the same site About 49 agent-weeks in total, of which about 11 are GitLab parity spread over D0 to D5.
Amendments, each by dated note, no accepted decision rewritten.
- ADR-0011. Writing a file in a standard's format (JSONL, Prometheus textfile, SARIF, Code Quality, JUnit, OSCAL) is a gate output; embedding an SDK, opening a socket or pushing stays forbidden. runward's own decisions as events are distinct from the observability port of the delivered system. Conventions pinned by version on delivery.
- ADR-0039.
runward-fleetis named as a voluntary satellite and its conditions become tests (no import of the runward package, no URL or upload flag in the CLI, run or installed by the operator, rebuildable from the export). The hosted product is recorded as outside the satellite definition. - ADR-0041. By name: "runward never computes the change set" gains one exception,
ledger populationandledger replay, outside the verdict path, read-only, shallow clones refused, configuration pinned.rulesandcheckstill take no base ref. - ADR-0054.
ledgerjoins the git readers outside the verdict path (crossing 4) andSPAWN_ALLOWED; the event writer's read of the last line of its own stream file (crossing 3) and of the harness payload (crossing 4) are named as permitted outside the verdict path; the boundary test asserts that neither module is in the verdict closure and runs the side-channel test in both rings. The fleet corollary loses "summoned only on a real demand signal", aligned with ADR-0039 as amended; "assembled from artifacts each repo already emitted" stands. - ADR-0064. "Computes nothing" means derives no verdict: a grouping inside runward is permitted as a pure function of emitted fields, under a versioned contract, publishing the list it counts; it is forbidden in any view. The report family includes the Markdown summary and the period report. The sentence on GitLab ingesting SARIF is narrowed to Ultimate from 19.2, where findings are filed as vulnerabilities; Code Quality and JUnit are the recommended GitLab channels.
- ADR-0080. Part 2 becomes one forge-neutral pure rule with per-forge fact readers, GitHub and GitLab in the same delivery, replacing "GitLab when a user asks"; on GitLab Free it is a post-merge detection control and says so, on Premium and above the forge prevents and runward records.
- ADR-0088. Decision 6: the sample population is distinct from the landing population and keeps
its construction; the reconciliation is published. Decision 1: a CI-signed artifact counts as
evidence an agent cannot produce only when the trust policy pins
refs/heads/mainand the workflow file digest. docs/spec/runward-signing.md. By name: the CLI still never signs and never verifies; a verifying party (the satellite, an auditor) verifies against the operator'strust-policy.json, and the satellite alone may raise a cell on a verified certificate.docs/spec/runward-oscal-mapping.md. By name: the document stays acomponent-definition; the landing population and period report may be cited as back-matter resources by digest. Anassessment-resultsoutput is not adopted (it would reverse §2 and require animport-apassessment plan runward does not write).
Where each note lands. In D0, with this record: ADR-0011 (the file renderings only), ADR-0054 (its
stale fleet corollary), ADR-0064 (the report family and the SARIF sentence), and a pointer on ADR-0090
from ADR-0095. In D1, with the records for D2: ADR-0041, ADR-0088 decision 6 and the ledger crossing
of ADR-0054. In D2: the rest of ADR-0011, the event-stream crossings of ADR-0054 and the grouping
clause of ADR-0064. In D3: ADR-0039, ADR-0088 decision 1 and the signing specification. In D4: ADR-0080
and the OSCAL mapping specification. A note never rewrites an accepted decision.
New records, and the delivery during which each lands (numbers from ADR-0094 on are the ones
taken; the class of a proposed record is proposed in it and decided by the maintainer):
| Record | Class | Lands during | For |
|---|---|---|---|
| ADR-0093, this umbrella | R | D0 | all |
| ADR-0094, forge adapter boundary (port, closed behaviours, parity by tier) | R, proposed | D0 | D1 |
ADR-0095, per-row ratification contract, causesByKind, assurance block |
R proposed (studies: D, pre-approval path) | D0 | D1 |
| ADR-0096, Code Quality and JUnit renderings of the verdict, position on SARIF in GitLab | R proposed (studies: D) | D0 | D1 |
Landing population and the ledger family (two populations, replay, record, snapshot) |
R | D1 | D2 |
| ITSM change-request deposit | D | D1 | D2 |
| Event stream and its emission points | R (touches check and gate-hook) |
D2 | D3 |
| Ex-post review of gate releases (disclosure by default, strict on opt-in) | R (the gate reads a new block) | D2 | D3 |
| Period report and the finding-class table | D | D2 | D3 |
runward-fleet satellite, trust policy, cell raising |
R (satellite conditions, signing spec) | D3 | D4 |
| Forge-neutral independence rule (ADR-0080 part 2) | R | D4 | D5 |
| Connectors (GitLab module modes, Kosli type, SIEM recipes) | D | D4 | D5 |
| Hosted product, in its own repository | its own register | D5 | D6 |
Alternatives discarded
implemented: machinefrom digest equality or the merge queue in the CLI. The digest does not identify a commit and the commit fields are unsigned; only a verified certificate binds the two.- The hosted product as a satellite. Contradicts ADR-0039's definition and ADR-0054's invariant.
- One population for the sample and the export. Re-unifying would change a construction the verdict re-checks (class R on ADR-0088 decision 6) for no gain an auditor sees.
- Signing as the default path. It would make the first demo depend on key material; N0 is complete.
- OTLP/JSON written by the CLI;
gen_ai.evaluation.result. The event hash is on native bytes; the evaluated object is a repository tree, not a model output. - A hash chain over records. Parallel jobs race on the head; reconciliation against the landing population and the listing of every attempt detect what matters.
- Kosli
attest decision --compliant, or a pass/fail chosen by runward in GitLab. Either makes a customer tool say "compliant" on runward's word. - OSCAL
assessment-results. Reverses the mapping spec and needs an assessment plan. - GitLab after GitHub. The target segment's most likely primary forge would wait for a request it will never phrase in runward's terms.
Consequences
- Positive. Every function works offline with no third-party tool; the auditor gets a landing population with its reproduction command; the change manager finds the evidence on the change request; the security team gets an append-only line; the organisation view never states more than it verified. The verdict path is untouched except by I0 (additive, pre-approved) and the ex-post review record, argued separately.
- Negative, accepted. Six contracts to keep in step, each expand-only; a fourth git reader; two
populations to explain; a second repository to govern, and a third if the hosted product is built;
several cells read
not-observedorci-declaredfor a long time and say so; GitLab Free users get detection, not prevention, of approval independence. - Words. "machine-established" (satellite, pinned policy), "declared", "ci-declared", "unalterable after shipping to a store the agent's credentials do not administer". Never "certified", "compliant" or "guaranteed".
What would settle it
- Side channel: in both rings and on the five
gate-hookexits, output and exit code identical with and without the stream and with a throwing writer; a planted import ofledgeror the event module into the verdict closure reddensruntime-boundary.test.js. - Population: the same landing population on two clones; a shallow clone refused; the published reconciliation with the sample population empty or explained on runward's own history.
- Cell raising: a record whose certificate names another commit is never raised; an unpinned trust policy raises nothing.
- Offline: a third party re-derives a row from the mission snapshot with no repository, measured on runward's own mission; without the snapshot the page says "inspection only".
- Connectors: no connector posts "pass" or "compliant" without a customer rule file (test).
- Self-sufficiency: the full D1 to D4 demonstration runs on an offline GitLab CE instance.
- Reader: an auditor briefed on IT change controls draws a row and re-performs it from the snapshot; a committee reader answers from the period page alone what is blocking at close.
Reevaluation trigger (mandatory, dated)
Reopen on 2027-01-13, or earlier on: an assessor rejecting the landing as population unit; a pipeline that can only read OTLP/JSON; an OpenTelemetry convention for control decisions reaching Release Candidate; a GRC tool that only reads OSCAL assessment results; the satellite found holding a vendor key or reading a repository; ADR-0088 stage 2 holding; the ADR-0054 re-evaluation of 2026-11-05 concluding otherwise on a crossing named here.
Trigger set on: 2026-10-13 · Watched via: runtime-boundary.test.js, the satellite's closure
and golden tests, the claims guard on rendered HTML, the offline GitLab CE bench.
References
- ADR-0011, 0021, 0039 (amendment of 2026-08-24), 0041, 0054, 0064, 0065, 0080, 0088 (decisions 1, 2, 4,
6), 0089, 0090, 0092;
docs/spec/witness.md§3;docs/spec/runward-signing.md(invariant, §3);docs/spec/runward-oscal-mapping.md§2;src/lib/attestation.ts,src/commands/verify.ts,src/commands/sample.ts,src/lib/delegation-sample.ts,src/commands/gate-hook.ts,eslint.security.config.js,test/unit/runtime-boundary.test.js. - Regulation (EU) 2022/2554 art. 9(4)(e); Delegated Regulation (EU) 2024/1774 art. 12, 17, 27.
- GitLab documentation on CI/CD components, merge request approvals, external controls and status
checks, Code Quality,
id_tokensand Sigstore signing; Kosliattest customandcreate attestation-type(read 2026-10-13). - The French studies of 2026-10-13, outside this repository.