agenthropic

ADR-0009: CD-7 — Security + the coverage gate are boundary conditions from commit one

As-built update — 2026-07-30

Verdict: holds, and it is the part of this project that was built exactly as decided. Every acceptance criterion below is enforced by something that fails a build or kills a process — not by a policy sentence.

Two honesty defects found in this area and fixed rather than shipped, recorded here because a security ADR that only lists its successes is not evidence of anything:

  1. apps/web ran its tests without --coverage. The thresholds were configured, looked enforced in review, and silently never executed — a gate that cannot fail is not a gate. Now vitest run --coverage, same as every other package.
  2. The events table was created by a migration and never written to — a lie by omission in a shipped schema. Now wired (ADR-0006).

One criterion is vacuously satisfied, and should be read that way. There is no SSRF test, because there is no outbound network call anywhere in apps/server — no fetch, no http.request, no HTTP client dependency. Webhook targets do not exist yet (ADR-0008: AlertSink has no adapter; alerts are post-1.0). “No outbound dial to a payload-supplied URL” is currently true because there is no outbound dial at all. When alerting is built, this criterion needs a real test; today it has nothing to test. (As-built addendum 2026-09-26: the static half of WP-F5’s no-SSRF gate now exists — scripts/check-no-spawner.mjs fails CI on an outbound network primitive or HTTP client in server-process source or manifests — so the absence is enforced rather than merely observed. The dynamic SSRF test is still the future dispatcher’s Done-when.)

As-built update — 2026-08-15

Verdict: the coverage bar strengthened; its enforcement clause is still unmet. (Verdict re-amended 2026-08-25: the second clause held when written and holds no longer — main is now branch-protected on the ci check, so the enforcement clause is met for anyone who is not the repository owner, enforce_admins: false being deliberate for a single-maintainer repository. See the “Blocks merges” paragraph below and the standing correction.) Two things changed since the 2026-07-30 reading, and they point in opposite directions. Recording only the first would be exactly the kind of success-list-as-evidence this ADR already refuses.

The bar is 100, not 90, and it covers five packages, not four. Every package — apps/server, apps/web, packages/core, packages/shared and packages/test-fixtures — runs vitest run --coverage with lines, branches, functions and statements all set to 100, and on a clean run at this date all five hold it. Two of the 2026-07-30 statements are therefore superseded: the threshold is no longer 90, and packages/test-fixtures is no longer an exclusion. Its config records why the original reasoning was revisited — getFixture, listFixtures and makeRawEventEnvelope are real code, and “a defect in a fixture builder does not fail loudly; it silently weakens every downstream parser and ingest test that consumes it.” The reasoning for the raised bar is stated in the same place: a 90% bar on a package sitting at 100% licenses a ten-point regression to pass in silence, which is the opposite of a gate. These are measured figures on one dated run, not a constant — see testing & quality §6.1 for the per-package numbers, the three ways a coverage figure can be bought, and the static guards that read the config as text to stop each of them.

“Blocks merges” was false until 2026-08-25, and is now true with one exception. The Decision below says CI-blocking, and the fifth acceptance criterion says the gate “blocks merges at or below 90%.” Until 2026-08-25, gh api repos/IvanBBaev/agenthropic/branches/main/protection returned 404 Branch not protected: CI ran the gate on every push and pull request and failed correctly when a threshold was missed — the mechanism was real and stricter than specified — but nothing physically prevented a merge over a red run.

The rule now exists. main requires the ci check, and refuses force-pushes and deletion. What it does not do is stop the repository owner: enforce_admins is deliberately off, because this is a single-maintainer repository whose normal mode is a direct push to main. So the criterion reads: a coverage regression withholds the merge button from a contributor, and does not withhold it from Ivan.

So this criterion is satisfied for the case it was written about, with the single-maintainer exemption stated rather than assumed: the measurement side exceeds what was asked, and the enforcement side binds everyone the project can be defended against. It is not an override — nobody decided to proceed without it — but the owner-bypass half is still not a pass, and no commit inside this repository can close it: it is a setting on github.com, kept off on purpose.

One criterion remains vacuous. The no-SSRF position is unchanged: still no outbound network call anywhere in apps/server, still nothing to test, still owed a real test the moment alerting exists.

Context

Every one of the six audited rival projects binds 0.0.0.0 and/or ships no-op auth in practice (simple10: 0.0.0.0 + zero auth; cast: 0.0.0.0 unauth GET reads; hoangsonww: token is a no-op when unset, and its /api/run spawner — accepting permission-mode from the request body with bypassPermissions in its allow-list — is an outright RCE). EXPANDED, one of the two external parallel reports, sequenced security to Phase 6 and backup to Phase 8; the Holistic and Architect lenses both call this “architecturally wrong, self-contradictory” (§4.1, §4.6) — a cross-origin-vulnerable socket cannot be a “later polish” item on a system whose entire positioning is security-by-default.

Decision

Security and the coverage gate are boundary conditions from commit one, CI-blocking: loopback-or-fail bind; mandatory DASHBOARD_TOKEN-or-fail-startup (timing-safe compare); SSE same-origin; no-spawner grep/static gate; no-SSRF (webhook targets operator-configured, never dialed from a payload); WAL + tested restore; >90% coverage blocks merges. This rejects EXPANDED’s security→Phase 6 / backup→Phase 8 sequencing outright — in the roadmap, Slice 8 is polish only, never the first appearance of these guarantees.

Acceptance criteria

Verbatim from concept-analysis-v2.md §6 (“Security, build-failing, from Phase 1”) and (“Delivery bar”):

Consequences

Alternatives considered