runward

RW™ · V0.36.1

Docs · Décisions · ADR-0054

ADR-0054.

the runtime boundary is explicit

Date: 2026-08-14 Status: accepted 2026-08-14 (the consolidated boundary test exists and passes on the built binary; record below; this document crosses nothing)

Context

A technical-roadmap investigation on 2026-08-14 (six agents, counter-expertised) asked what runward would need to BE, in code and infrastructure, to become a credible enterprise-grade governance base for an agentic factory — without becoming a runtime. It produced a set of buildable objectives (a verdict attestation, committed-tool evidence adapters, a pinnable rule corpus, a fleet rollup) and one recurring finding: every one of them is one design slip away from turning runward from a frame into a runtime, and that slip destroys the moat.

The moat is four properties, and each is a direct consequence of runward NOT running: independent (the judged party does not manufacture the judge), survivable (a verdict re-runs months later on the repo alone), agent-agnostic, and deterministic (no LLM, no network in the verdict path). A hosted service, a daemon, a held key, a live cross-repo read — each dissolves one of the four.

The boundary already exists, but scattered across a dozen decisions, never stated as one line:

  • No HTTP/registry endpoint. "runward is never a runtime; the CLI is the seam, scripts compose it" (ADR-0024:23).
  • No daemon, watcher, or auto-installer. "the cure for 'the gate doesn't run automatically' is not for runward to become the thing that runs it. A daemon, a file-watcher, a runward-managed hook runtime … would each turn runward from a frame into a runtime — the one thing the doctrine forbids it to become" (ADR-0012:14,40).
  • No reading beyond the mission tree. "runward never computes the change set … the CLI reads your mission repo, never your behavior — the moment runward runs git against a base ref, it stops reading its own data and starts reading the state of the harness" (ADR-0041:29), backed by the load-bearing contract "Same working tree ⇒ same verdict and same machine outputs".
  • No LLM or network in the verdict path (ADR-0007, ADR-0011).
  • No key or identity the operator does not hold (ADR-0021 threat model; ADR-0011 no identity broker).
  • The operator layer stays outside the CLI (ADR-0039).

A reader assembling the roadmap has to reconstruct the boundary from six ADRs. The three thesis-risk objectives (opt-in signing, a shared corpus, a fleet rollup) each need it stated as a single testable invariant BEFORE they are built, not litigated one PR at a time. This ADR writes the line down and makes it the check every future objective passes.

Decision

The invariant, in one sentence. runward stays a delivery gate — never a runtime or hosted service — for exactly as long as the verdict, the rules, and any fleet rollup are computed in-repo, on demand, and carried as artifacts the operator owns, never produced, held, watched, served, or aggregated by a process runward operates. Emitting a deterministic file is a gate output; operating a process that stores, serves, watches, or aggregates it is a runtime.

The crossings, enumerated. The boundary is crossed by ANY one of:

  1. A network-reachable endpoint that serves rules, verdicts, or aggregated outcomes — an HTTP/rules-registry API or a hosted compliance portal (ADR-0024).
  2. A long-lived process between gate invocations — a daemon, a file-watcher, an auto-installer, a drift monitor (ADR-0012).
  3. runward holding state, a key, or an identity the operator does not — signing on the operator's behalf, brokering agent identity, or storing fleet state (ADR-0011/0021).
  4. Reading beyond the single mission tree it judges — a second repo's working tree, git history, a base ref, or the harness's behaviour — which also breaks "same working tree ⇒ same verdict" (ADR-0039/0041).
  5. Any data flow, telemetry export, LLM call, or network call in the verdict path (ADR-0007/0011).

The corollary for the fleet. Everything beyond the per-repo gate — cross-repo aggregation, adoption metrics, an org-wide dashboard — lives in the deferred operator satellite (ADR-0039), outside the MIT CLI, summoned only on a real demand signal, and even there it is assembled from artifacts each repo already emitted, never from a live pipe into the repos. Distribute governance into each repo; the moment it is centralized into something that runs, the four-way moat is gone.

How new objectives use this. Every objective on the technical roadmap declares which side of the line it sits on — thesis-consistent CLI+artifacts, operator satellite (ADR-0039), or not code. The two objectives that touch the boundary most (opt-in signing, a shared corpus) ship ONLY with a blocking test of the specific invariant they risk: the key stays the operator's, the corpus resolves with no live fetch. This ADR is the reference those tests cite.

Alternatives considered

  • Leave the boundary implicit, as it is today. It held for 53 ADRs because the surface was small. The roadmap's attestation, corpus and fleet work each proposes something that emits toward a standard (in-toto, cosign, a registry-shaped corpus), and the distance from "emit a file" to "operate a service" is one design decision. An implicit boundary is re-argued every time; an explicit one is checked once.
  • Enforce the whole boundary with one linter/test. Parts are already mechanized and stay so — the CI zero-LLM / zero-network guard, the no-exec offline test, the "same working tree ⇒ same verdict" contract. But "a decision to serve rules over HTTP" is an architecture choice a test cannot pre-empt; the boundary is doctrine first, with tests enforcing the mechanizable crossings (network, exec, cross-tree reads, verdict-path purity). This ADR names both halves.
  • Fold the line into ADR-0039. ADR-0039 is about the operator LAYER (adoption self-audit, cost recipes) staying out of the CLI. The runtime boundary is broader — it governs the CLI's own surface too (no daemon, no key, no cross-tree read), not only the satellite. It deserves its own statement that ADR-0039 becomes one instance of.

Consequences

  • The roadmap gets a single gate. Each new objective is checked against five enumerated crossings before it is built, not after it ships. A proposal that needs runward to host, watch, or hold state is rejected on this ADR by name, the way ADR-0041 rejected --changed.
  • The mechanizable crossings become a named test surface. The existing guards (CI zero-network / zero-LLM, offline no-exec, same-tree determinism, the ADR-0045 audit corpus) are consolidated as the boundary's enforcement floor, so a future PR that adds a socket, a spawn, or a base-ref read to the verdict path reddens.
  • The satellite is quarantined by construction. A fleet rollup that reads emitted artifacts in the operator's own infra is inside the line; the same rollup as a hosted always-on dashboard, or one that reads a repo's working tree, is outside it. The category label does load-bearing work, and this ADR is what makes the label checkable.
  • Nothing ships or changes for a user. This ADR consolidates existing doctrine and adds a test surface; node dist/cli.js check --strict on this repository must read exit 0 before and after.

What this does not claim

  • It does not forbid the OPERATOR from building a service around runward's emitted artifacts — a dashboard over check --json, a registry that vendors the rule package. That is the operator's runtime, on the operator's side of the seam; the boundary is about what runward operates.
  • It does not add a command, a flag, or a behaviour. It is doctrine plus a consolidated test.
  • It does not make the boundary un-crossable by a future decision; it makes crossing it a visible, named decision — a superseding ADR, argued in the open — rather than a silent slide.

Ratification

Accepted 2026-08-14. The consolidated boundary test is test/unit/runtime-boundary.test.js, green on the built binary. The audit of 2026-08-14 named the debt this ratification closes: this ADR — the anchor of the thesis — sat proposed under two accepted dependents (ADR-0055, ADR-0057) while its consolidated tests did not exist. Evidence against each criterion:

  1. No network / no exec in the verdict path — proven structurally: the TRANSITIVE import closure of dist/lib/verdict.js contains no socket module and no process spawner (http/https/net/tls/ dgram/child_process/worker_threads/cluster/repl), with a require()-blindness guard on every closure file, and a negative control asserting the crossing modules DO exist elsewhere in the CLI (hooks.ts — the operator's own checks, outside computeVerdict; characterize.ts — local git archaeology) so the boundary test cannot pass vacuously. The CI unshare -n block remains the dynamic network half (.github/workflows/ci.yml), consolidated by citation.
  2. Same working tree ⇒ same verdictcheck --strict --json is byte-identical across two runs on an unchanged tree, and a grep-level guard asserts no command source speaks --changed/base-ref (mirroring ADR-0041's own guard). Both in runtime-boundary.test.js.
  3. The verdict ignores advisory LLM output — the existing test/unit/verify-findings-out-of-verdict.test.js (verdict byte-identical whether the verify-findings file is present, absent, empty, or adversarial) is the proof; the boundary test pins the citation so it cannot be silently deleted while this ADR stays accepted.
  4. Global invariantnode dist/cli.js check --strict exits 0 before and after; no-overclaim green.

The criteria as originally set, kept as the record of the bar:

  1. No network / no exec in the verdict path. A test (consolidating the CI ci.yml zero-LLM / zero-network guard and the offline no-exec guard) asserts that check and check --strict open no socket and spawn no process while computing the verdict.
  2. Same working tree ⇒ same verdict. A test asserts check --strict --json is byte-identical across two runs on an unchanged tree (extending the port-contract invariant), and that no command accepts a --changed/base-ref argument (grep-level, mirroring ADR-0041's own guard).
  3. The verdict ignores advisory LLM output. The existing "verdict byte-identical whether the verify-findings file is present, absent, empty, or adversarial" test is cited here as the verdict-path-purity proof (ADR-0007).
  4. Global invariant. node dist/cli.js check --strict on this repository exits 0 before and after, and no-overclaim stays green (this ADR argues the boundary, it asserts no capability).

Every objective ADR that follows (ADR-0055, ADR-0056, ADR-0057) cites this one for the side of the line it is on.

Reevaluation trigger (mandatory, dated)

Trigger set on: 2026-11-05.

The decision is wrong and must be revisited if any holds: a capability operators genuinely need turns out to require a crossing, and refusing it costs more than the boundary buys (the honest form of "the thesis was too strict"); the boundary test starts passing vacuously (a crossing enters through a path the import closure does not see, e.g. a dynamic import or a spawned helper); or the seam to the satellite proves unusable in practice, which would mean the line is drawn in the wrong place rather than that there should be no line.

Watched via: runtime-boundary.test.js, the CI network-cut run, and any capability request.

References

  • ADR-0012 — no daemon/watcher; the frame does not become the thing that runs it
  • ADR-0024 — no HTTP/registry endpoint; the CLI is the seam
  • ADR-0039 — the operator layer (and the fleet satellite) stays outside the MIT CLI
  • ADR-0041 — no --changed; reads the mission tree, never the harness's behaviour; same working tree ⇒ same verdict
  • ADR-0007 — no LLM in the exit-code path
  • ADR-0011 — no identity broker; versioned ports, not a service
  • ADR-0021 — the threat model: never a key the operator does not hold
← Tous les ADR