fix: say why a sandbox failed to launch, and observe a run instead of an instant

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>
This commit is contained in:
DongHyeonka
2026-08-15 21:12:06 +09:00
co-authored by Claude Opus 5
parent 711d61e73f
commit 9ca5c3f668
7 changed files with 261 additions and 58 deletions
+42 -12
View File
@@ -491,21 +491,51 @@ below names the defect, not the symptom.
| `OPS-18` | architecture | `PARTIAL` | Generic presentation still reads the installed-feature registries. The rule freezes the exact set of modules doing so today; a new edge fails. Lifting the assembly into `bootstrap` is not done. |
| `OPS-19` | documentation | `FIXED` | README and the manual accessibility checklist both claimed six routes while ten were registered, leaving four screens outside the declared manual review scope. The list is now derived from the route registry by `verify:documentation`. |
### Product feature selection (2026-08-15, second pass)
| id | disposition | what changed |
| --- | --- | --- |
| `OPS-20` | `FIXED` | Which features a build contains is now a declared manifest rather than five registries spreading a literal. `VITE_PRODUCT_FEATURES` narrows it at build time; a test fails if a new registry forgets to consult it. |
| `OPS-21` | `FIXED` | `FEATURE_OVERRIDES` in the runtime document takes an installed feature out of service without a rebuild. The router refuses its routes, not just the navigation, so a typed deep link cannot still mount it. |
| `OPS-22` | `FIXED` | Both inputs are subtractive by vocabulary: the override enum has no `ENABLED`, and a build-time selection naming a feature the source tree does not declare is refused rather than ignored. |
| `OPS-23` | `FIXED` | A sandbox that fails to launch now reports why. The supervisor consumed the child's output only to enforce a byte cap and discarded it, so a host restriction surfaced as an unexplained `exit=1`. Lines the sandbox tooling itself emits are kept; provider output is still discarded. |
An env var does **not** shrink the bundle, and the code says so. A static import
cannot be undone by a value, and making the import graph depend on a
configuration string is what §3.5 exists to prevent. Measured: `none` changes
the output by 58 bytes. Physical removal is FE-GATE-020's job.
### Host restriction discovered during this pass
`bwrap --unshare-net` no longer works on this machine:
```
$ printf '%s\0' --unshare-net --ro-bind /usr /usr ... | bwrap --args 3 -- /bin/true
bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
$ sysctl kernel.apparmor_restrict_unprivileged_userns
kernel.apparmor_restrict_unprivileged_userns = 1
```
That reproduction contains none of this repository's code. Earlier in the same
session the identical sandbox ran to completion, so the restriction became
active partway through. While it holds, 16 of the 108 provider tests cannot run
here — they need a sandbox the kernel will not grant. They are not counted as
green and not counted as product defects; under a host that permits the
namespace the same file was 107/108.
### Still red after this pass
`tests/unit/ci-artifact-contract.test.ts`*applies effective aggregate cgroup
limits without exposing command or credentials*. It reads the live process tree
and cgroup of a running sandbox, and the supervisor now completes a whole run in
well under a second while `systemctl show` and `ps` each cost hundreds of
milliseconds, so the observation loses the race. It was already red before this
work and is not a product defect; the assertions it makes about cgroup limits
and credential exposure are not currently proven by an automated run.
*applies effective aggregate cgroup limits without exposing command or
credentials* was rewritten. It used to read the live process tree with one
`ps` per pid and assert mid-run, which lost a race against a sandbox that now
completes in a few hundred milliseconds; it records the tree from `/proc` every
5ms and asserts on the recording after the run. That restructuring is also what
revealed the host restriction above — the supervisor had been failing to launch
the sandbox and the test was dying on the observation first.
Two more time out at their 10s budget when the whole suite runs in parallel on
a loaded machine and pass in isolation, repeatedly: *expires an uncommitted
lease only after cleaning every owned object* and *requires every external
expected identity variable at the exact promotion CLI*. They are recorded as
environment-limited, not as green.
Tests that spawn processes, build archives and sign evidence were given a
30s budget instead of the 10s default sized for pure-JS unit tests. The default
was not raised: that would hide a genuinely hung test.
### FE-GATE-020 after this pass