main is branch-protected on the ci check, with the sole maintainer exempt on purpose (enforce_admins: false)concept-analysis-v2.md §3, row CD-7
(consolidates AD7, SD8, QA-D3/D4, G-D3, H-SEQ); docs/ai/DESIGN.md §8Verdict: 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.
127.0.0.1,
and then re-verifies every actually-bound address after listening: any
non-loopback address logs a secret-free FATAL and hard-exits the process. That
second check exists because a config-level bind constant is only a claim about
intent; the socket is the fact./api/ routes are gated.Origin both with and
without a valid token — so neither response distinguishes “wrong token” from
“no token,” nor “bad origin” from “bad credential.”gate:spawner (scripts/check-no-spawner.mjs) scans apps, packages,
scripts, hooks and the repo-root config files for subprocess spawners, wide
binds (0.0.0.0, host: true, host: ''), WebSocket servers, and dynamic
evaluation including indirect eval — and exits 1 with the offenders listed. It
runs in CI on every push. Its allowlist contains exactly one file: the gate itself,
which necessarily contains the patterns it forbids.gate:licenses runs the CD-9 allowlist scan (ADR-0011).journal_mode = WAL and
then reads the pragma back, throwing if SQLite did not honour it. Foreign keys are
enforced the same way.integrity_check =
ok, WAL mode preserved, and the substrate rows readable through the port.events_raw has no UPDATE/DELETE path, enforced by SQLite triggers rather
than by discipline (ADR-0004).apps/server,
apps/web, packages/core and packages/shared each run vitest run --coverage
with 90% thresholds on lines/branches/functions/statements.
packages/test-fixtures is a deliberate, documented exclusion — it is fixture data
consumed by other packages’ tests, not shipped logic.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:
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.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.)
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.
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.
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.
Verbatim from concept-analysis-v2.md §6 (“Security, build-failing, from Phase 1”) and
(“Delivery bar”):
127.0.0.1 only and FAILS startup when DASHBOARD_TOKEN is unset
(never “auth disabled”); token compare is timing-safe; SSE rejects cross-origin; no
wildcard CORS.events_raw exposes no UPDATE/DELETE path (enforced by test); SQLite runs WAL; a
backup is taken and a restore is exercised at least once per release candidate.docs/ai/DESIGN.md §0), not a bolt-on appendix.development-plan.md §7 notes that
WP-F5…WP-F7 (static no-spawner/no-SSRF/license gates, security-contract tests) plus
WP-F8 (backup/restore) all land before any ingest feature code, and WP-F7’s contract
tests are intentionally red from wave 8 until WP-U0 wires the real primitives at wave 9.development-plan.md Track F (WP-F1…WP-F8), WP-U0 (server bootstrap
that turns WP-F7 green). See the security model (flagship page)
and backup & restore.