The cgroup test read the live process tree with one `ps` per pid and asserted
while the provider was running. That was a race it used to win only because the
sandbox was slow; now a whole run finishes in a few hundred milliseconds and
`systemctl show` alone costs longer than the thing it describes. It records the
tree from `/proc` every 5ms and asserts on the recording once the run is over,
because the assertions were always about what the run contained.
That restructuring immediately paid for itself: the supervisor had been failing
to launch the sandbox at all, and the test was dying on the observation before
it ever checked the exit code.
It could not say why, because the supervisor consumed the child's output solely
to enforce a byte cap and then discarded it — `exit=1` and nothing else. It now
keeps the lines the sandbox tooling itself emits (`bwrap:`, `prlimit:`,
`systemd-run:`, `systemctl:`), which cannot carry provider credentials because
the provider command and its secrets travel in the args file. The failure now
reads:
sandboxed external provider failed: exit=1; sandbox reported:
bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
which is a host restriction — `kernel.apparmor_restrict_unprivileged_userns=1`
— reproducible in two lines of shell containing none of this repository's code,
and recorded in the ledger as such rather than carried as a product defect.
Suites that spawn processes, build archives and sign evidence were given a 30s
budget. The 10s default is sized for pure-JS unit tests; raising it globally
would hide a genuinely hung test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FE-GATE-020 proves a capability can be removed by rebuilding the whole project
without it. The fixture it built could not get that far, and the failures all
came from the fixture rather than from anything about removability.
It was not a repository. The supply-chain inventory is defined as the tracked
file set, so it asks `git ls-files` what the project contains; with no
repository to ask, generation failed and took every provider suite down with
it. It is now initialised on preparation and committed after the removal — not
before, or the index would still list the files the removal deleted.
It had no `.gitignore`, so once it did have a repository, every generated
artifact and every linked module landed in the index and the inventory refused
the fixture for tracked and generated paths colliding. It carries the ignore
rules now, and therefore records the same tracked set as the repository it was
copied from.
Each removal script kept its own copy-target list and they had drifted: the
reference-feature fixture omitted `playwright.capabilities.config.ts`, which the
inventory requires. There is one list now. It also gained the install and
workspace identity — `.npmrc`, the lockfile, the workspace file — without which
the fixture is a different project, and the release evidence a candidate is
assembled from, without which no candidate can be built at all.
A tracked root the removal deletes is no longer required of the result: the
optional-recipe fixture deletes `recipes/`, and the inventory policy demanded
it back. Roots that are gone are pruned from the fixture's policy.
Two smaller causes. A platform integration file asserted the reference feature's
own route ids, so removing the feature left it importing a deleted module —
typecheck, the test run, coverage and the residue scan all failed on that one
misplaced assertion, which now lives in the feature's test tree. And the
canonical exact-count authority was re-imposed on a contract the fixture
deliberately reduces, failing the fixture for the reduction it exists to prove;
`CI_CONTRACT_MODE` already marked those runs and is now honoured by default.
The reference-feature fixture goes from failing before its first assertion to
1,612 passing with one failure, and that one is the live process-tree
observation test already red on the main tree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three boundaries the layer contract declares had no executable rule behind
them, so the code drifted across all three while every gate stayed green.
`src/contracts` reached back up into `src/application` for the shared `Result`
carrier and the compatibility predicate. Neither package owned the shared
vocabulary and the dependency pointed both ways. Both now live in contracts —
the lower package — and application re-exports them, so no caller moves.
A concrete adapter was not supposed to depend on another concrete adapter, but
only adapter-to-presentation was enforced, and `diagnostics` imported a guard
out of `telemetry`. The guard belongs to neither, so it moved to the adapter
kernel. Stating the rule needed the checker to resolve `$1` in a `to` pattern
against the importing module's own directory; the alternative is one rule per
adapter group, which silently stops covering a group the moment one is added.
Product assembly leaks out of bootstrap: generic presentation reads the
installed-feature registries. That is a real refactor, so the rule freezes the
exact set of modules doing it today rather than pretending it is fixed — a new
edge fails. The two remaining open edges are named in the config, not silent.
Each rule was verified by introducing the violation it forbids and confirming
the gate rejects it.
The documentation drifted the same way. README and the manual accessibility
checklist both said six routes while ten were registered, which left the
platform overview and three reference-resource screens outside the declared
manual review scope without anyone deciding they should be. The scope is now
derived from the route registry by `verify:documentation`, so the sentence
cannot outlive the registry again. The review ledger also named a canonical
path that does not exist in this tree; it is upstream provenance, and it now
says so instead of looking like a broken repository reference.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four browser-capability specs never reached the code they were named for. The
`PRESIGNED_TRANSFER_V1` envelope gained a top-level `protocol` field, and the
fixtures kept answering without it, so every capability was refused before any
object request was made: the download and part-upload success paths were
asserting against an empty transcript rather than exercising a real GET or PUT.
The fixtures now speak the protocol they claim to, and the part-deletion
expectation carries the physical effect the adapter reports.
A refused capability document also answered `recovery: NONE`, telling the
caller there was nothing to be done. The design record fixes this class of
refusal as re-issuable and the vault already answers `REISSUE_CAPABILITY` for
it, so the HTTP decoder disagreed with both. It now agrees.
Lab performance produced no evidence at all. Playwright matches accessible
names by substring, so the navigation entry "플랫폼 구성" also matched the home
page's "플랫폼 구성 보기" call to action; the locator resolved to two links and
the run died on a strict-mode violation before the first measurement. With an
exact match the metrics are collected, and they show the named-interaction
budget is missed on this machine — a real signal that was previously invisible.
The platform overview baseline was captured before the reference routes moved
from `integration-defined` to `session-required` and was never regenerated, so
the only visual gate that could catch a regression on that page was failing for
its own staleness. Regenerated after confirming the diff is exactly that label.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The provider sandbox never ran. bubblewrap 0.9.0 stops parsing an `--args`
file at the first non-option and never hands the remainder back, so the
command written into that file was silently dropped: bwrap printed its usage
text, exited 1, and the provider produced no evidence at all. The options
still travel in the args file — that is what keeps host paths and credentials
out of `/proc/<pid>/cmdline` — but the command now rides on real argv, and
`encodeProviderBwrapInput` refuses a `--` so the drop cannot come back.
The scope wrapper then could not exit. It read the supervisor's liveness pipe
through `fs`, which runs a blocking `read(2)` on a threadpool thread; the
supervisor holds that pipe open for the scope's whole life, so the read never
returned and closing the descriptor did not interrupt it. Once bubblewrap
finished the wrapper deadlocked in `process.exit`, the scope outlived the
provider, and a completed run was reported as a timeout kill. The channel is
now read through the event loop, so teardown is observable and terminal.
Creation modes were left to the ambient umask. `mkdir(mode)` and `open(mode)`
are requests the kernel subtracts the umask from, so a runner exporting a
restrictive umask produced directories it could not enter and handed `tar` a
file it could not re-open. Private modes are pinned instead of inherited.
Promotion cleanup deleted before it checked. Removals run through a pinned
descriptor, so a leaf substituted after validation had this promotion's exact
five destroyed first and the substitution reported afterwards, leaving a
half-emptied directory a retry could not tell from a completed one. The name
is re-bound to the inode before anything is removed, so the failure is total.
Separately, release coherence proved the artifacts agreed with each other but
never that they belonged where they were going: a build whose runtime document
said `APP_ENV: local`, `AUTH_MODE: demo` and a loopback API is coherent with
itself and passed every gate. `public/` is copied verbatim into `dist/`, so
that local document shipped with every build regardless of what the build was
for. Runtime configuration now comes from a declared profile, and FE-GATE-027
refuses to admit an artifact to an environment it does not match — including
refusing an undeclared destination, so nothing is admitted by omission.
`REQUEST_TIMEOUT_MS` and `VITE_ROUTER_BASE_PATH` were validated and then
dropped: the V3 executor ran every operation on its contract's own deadline,
and Vite emitted root-absolute assets for a sub-path deployment. The timeout is
now a ceiling that may tighten a contract but never loosen one, and one base
path feeds the router, the Service Worker scope and the asset base together.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adversarial re-verification of the gate itself found three holes.
The disposition check matched the finding's verdict anywhere in the table
row, and every row also carries the prior verdict — so `| NS-01 | PARTIAL
| FIXED |` satisfied a receipt that said either. It now reads the
disposition from its own column, which is the check the gate was supposed
to be performing all along.
An evidence path only had to exist. A row could point at an unrelated
suite and look substantiated, so each evidence file must now name the
finding it is evidence for; rows the review labelled differently declare
their own markers rather than the check being loosened.
The file-transfer bundle budget was a number typed into prose next to a
number in config, which is exactly the evidence drift the cross-audit
raised. The gate now compares them.
Each hole was confirmed by breaking the input and watching the gate fail:
a disposition disagreement, a budget changed to 60,000, and NS-07 pointed
at the public cache suite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ledger declared "All 38 are now FIXED" while six of those rows were
reproducibly partial. A summary sentence is cheap and a reviewer reads it
as evidence, so the claim is now derived from a machine-readable record:
`docs/operations/adapter-remediation-dispositions.json` carries each
finding's disposition and the test paths that hold it, and
`check:remediation-ledger` joins that file to the prose, verifies every
evidence path exists, and refuses a blanket closure sentence while any row
is still open.
The shared-abort gate had the same weakness in miniature: it passed when
at least one production file imported the primitive, so an unrelated
import satisfied it while Image and Resumable kept their own diverging
copies. It now requires the four named consumers to resolve their import
to the primitive itself, and prints the exact importer set rather than a
count.
`check:optional-recipes:source` was already failing before this work
(52,078 against a 52,000 budget) and the correctness code above pushed it
further. Duplicate abort mechanics were consolidated first — Image and
Resumable onto the shared primitive, four decoders onto one snapshot
helper — and the remainder is code the review asked for, so the budget is
reset to 54,600 against a measured 53,810 with that reasoning recorded,
rather than the failure being carried forward as if it were green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The marker read added a whole chunk to a running total and compared the
total afterwards, so a corrupt body could hand activation a 1 MiB chunk
against a 257-byte ceiling. It now reads at most the remaining allowance —
through a BYOB reader where the source offers one, and by refusing an
oversized chunk before copying it otherwise. A declared oversize cancels
the body it refuses instead of leaving the stream open, and the reader
lock is released on every path.
The build generator and the runtime decoder shared only the extension
table, not the path grammar. The generator happily emitted
`/assets/bad@name-abcdefgh.js`, which the decoder then refused — a correct
build failing at install time. Both now use one exported canonical path
predicate and the generator decodes its own output before returning it.
The notification click handler emitted its terminal record from inside
`process` and again from the `waitUntil` wrapper, so an ordinary click was
counted twice. Worse, a late rejection downgraded `MAYBE_APPLIED` to
`NOT_APPLIED` — telling operators the click had definitely not been
applied when nobody knew that — and the late observation ran outside
`waitUntil`, so a worker shutdown lost the evidence. There is one
observation authority per click now, certainty is monotone, only an
explicit null window confirms `NOT_APPLIED`, and the bounded tail is owned
by `waitUntil` without extending the public deadline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TR-RR-06. dispose() now bounds its drain with a cleanupDeadlineMs from policy
and returns the result, so a non-cooperative mutation lock or provider can no
longer make teardown unbounded and an unproved drain is reported as still
CLOSING instead of closed over. The checkpoint store stays open in that case,
because something can still write to it. An abort is admitted physical work
like an upload, so it joins the tracked set rather than being stepped over.
TR-RR-07. The verification slot belongs to the raw verifier, not the wrapper.
Releasing it when the caller's wait expired let an abandoned verification keep
running while a new one was admitted, so repeated aborts produced more
concurrent physical work than the configured cap allows. The slot is now
released only once the raw tasks settle.
TR-RR-04. A presigned byte source owns a fetch reader and a capability lease and
its port requires close(); the delivery consumer never called it. The closeable
subtype is lost in the FileByteSource projection, so a holder keeps it from the
moment the lease exists and the outermost finally closes it exactly once — on
success, validation failure, writer failure and abort alike.
check:adapter-inventory now also fails if the shared abortable-operation
primitive has no production importers. It was safe to add only once the
presigned subsystems actually migrated onto it; a gate that fails CI for a
documented, unfixed defect reports the wrong thing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four fixtures linked the installed dependencies into a throwaway root with a
single directory symlink at <fixture>/node_modules, then ran pnpm inside that
root. pnpm does not recognise the modules directory it finds there and purges
it; with CI=true it does so without a prompt. The purge followed the symlink and
deleted the repository's own node_modules mid-run, so a test suite uninstalled
the workspace it was running in. That is what produced the cascading,
file-unrelated failures a full test:unit run reported, and it happened twice
while running the suites for the adapter re-review.
scripts/lib/fixture-node-modules.ts replaces all four sites: node_modules is a
real directory whose entries are individual symlinks, so a recursive delete
unlinks the fixture's own links instead of walking through one link into the
shared tree. Resolution is unchanged.
tests/unit/fixture-node-modules.test.ts performs the exact recursive delete pnpm
performs and asserts the source tree survives, and check:adapter-inventory now
fails on any reintroduction of the directory-symlink form — verified by putting
the old line back and watching the gate reject it.
A full tests/unit + tests/integration run now leaves the dependencies intact.
removal-fixture, supply-chain and security-followup-archive, the three suites
that had to be excluded before, pass in that run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GOV-01. The adapter inventory claimed 118/118 while the tree held 119 files, so
src/adapters/platform/abortable-operation.ts sat outside every review's
coverage without anything failing. The row is restored and
scripts/check-adapter-inventory.ts now diffs the document against
git ls-files src/adapters, so the count is an equality rather than a number
someone has to remember. The same gate pins that the Service Worker asset
generator reads the shared extension table instead of declaring its own.
GOV-02. The previous ledger closed rows as FIXED_NOT_RELEASED that the
re-review found partial. The new section is written the other way round: a row
reads FIXED only where a named adversarial test failed on the pre-fix source
and passes on the landed one, and the twelve findings this pass did not reach —
RPC-RR-01, RT-RR-01 through RT-RR-04 and TR-RR-01 through TR-RR-07 — are
recorded as NOT_STARTED with the reason each needs a lifecycle change rather
than a contained edit. None of them may be treated as closed and no capability
they cover may be promoted without its own evidence row.
The structural gate for the shared abortable-operation primitive is
deliberately not added yet: it still has zero production importers, and a gate
that fails CI for a documented, unfixed defect would report the wrong thing.
Also records the destructive test hazard found while running the suites:
scripts/lib/removal-fixture.ts and scripts/check-supply-chain-provider-fixtures.ts
symlink the real node_modules into a temp fixture root and run pnpm there, which
purges the repository's own dependencies through the symlink mid-run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SW-RR-01. The activation marker was read with response.text() whenever no
Content-Length was present, so a large or non-terminating body could consume
the whole activation step. It now reads through a bounded reader that stops one
byte past the ceiling, cancels its reader, applies a read deadline and decodes
UTF-8 fatally.
SW-RR-02. A matching nonce is not identity. An activation or reset result whose
event.source is null can no longer stand in for the expected worker; only a
strict identity match is admitted.
SW-RR-03. The build generator and the shared manifest decoder now read one
exported extension table, so .mjs and .png stop being emitted-then-refused.
.json is deliberately outside it: every JSON file in a build output is a control
document the generator already excludes, not a cacheable asset.
SW-RR-04. Both caches.open and cache.match are closed as a miss. Letting a
match rejection propagate rejected respondWith itself, so the entry never
reached its network fallback.
WP-RR-01. focus and openWindow now carry the certainty phase showNotification
already had — NOT_APPLIED, MAYBE_APPLIED, CONFIRMED — and an effect that lands
after the handler deadline is observed exactly once. The evidence never
authorizes a retry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SW-URL-01: canonicalize each generated root-relative manifest URL against the
registration scope once, re-check same-origin, and share that absolute identity
across install cache keys, fetch classification and cache lookup or delete.
Previously every verified asset fell through to the network.
SW-01: serve verified static requests only from the current release cache. A
CacheStorage-wide match could return a previous release's response for the same
URL while the delete targeted a cache that was never read. The worker scope
facade no longer exposes a wide match at all.
SW-02: cache reset deletes only names that parse as owned, so a foreign cache
sharing the ca-static-v1- prefix survives.
SW-03: unregister() resolving to false is a FAILED unregister, not UNREGISTERED.
SW-04: staged removal reports what happened - ABSENT, UNREGISTERED and PURGED
map to DISABLED, OWNERSHIP_MISMATCH to INCOMPATIBLE and FAILED to FAILED - so a
later release cannot delete the worker while a registration or owned cache is
still present.
SW-05: add the runtime-neutral service-worker-static-manifest codec that owns
exact row keys, the extension and content-type allowlist, the root-relative URL
rule and the length-prefixed canonical bytes. The generator and the build gate
hash those same bytes, and the build gate now decodes and recomputes the set
digest instead of type-casting the manifest.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>