feat(record): exec-time resolution record - #238
Open
michael-herwig wants to merge 4 commits into
Open
Conversation
Deep Test Results 3 files 26 suites 10m 7s ⏱️ For more details on these failures, see this check. Results for commit dbaee1e. ♻️ This comment has been updated with latest results. |
michael-herwig
added a commit
that referenced
this pull request
Jul 27, 2026
The design landed in PR #238. The Negative list's "the emit is three call sites, not one seam" is struck: `ocx_lib::launch` is that seam — the `child_process` submodule is private, so `Launch::recording` / `Launch::exempt` are the only reachable spawn paths, `RecordingPolicy` is mintable only by `record::policy::resolve_records`, and sanctioned non-recording launches are a closed `ExemptionReason` enum.
michael-herwig
force-pushed
the
feat/exec-resolution-record
branch
from
July 27, 2026 17:23
dbaee1e to
644945c
Compare
`test_schema_generation.py` built `target/release/ocx_schema` from its own fixture whenever the binary was missing, with no timeout. The acceptance jobs download a prebuilt `ocx` and carry no Rust cache, so that was a cold full release compile of `ocx_lib` inside a job that has no business compiling anything — and under xdist every worker's fixture raced the same `target/` lock on a two-core runner. The failure mode is the worst kind: the run stalls just short of 100% with no failing test to point at, because the suite is not hung, it is waiting on a compiler. The only trace is an orphan `cargo` process in the runner's cleanup log. It survived on main by being merely marginal at ~8m30s. CI now supplies the binary instead of building it: the job that already runs `task schema:generate` uploads it, and the acceptance jobs download it alongside the `ocx` binary and the generated schema. Coverage is unchanged — the tests still run against a real binary, they just stop paying for it. The fixture keeps the local convenience build, where `target/` is normally warm, but bounds it so a cold build fails as a skip rather than a hang, and refuses to build at all under `CI`, where a missing artifact is a workflow bug and should say so. Also adds the `execution-record` variant, which was absent from the table.
`crates/ocx_lib/tests/fixtures/index_wire/cpython/` is generated locally from CPython's json module — the section 14 byte authority the bot repo implements — and is documented in that tree's README as explicitly not vendored from ocx-sh/index. It landed in 6d5270b without touching the drift gate, so `diff -r` has reported `Only in ...: cpython` on every run since, failing Verify Deep's Index Conformance Drift job on main. It joins README.md and SOURCE_COMMIT in the exclusion list for the same reason: ocx-authored, not vendored bytes. Excluding the name cannot hide an upstream addition — an upstream `cpython` path claims no leaf, so assert_every_upstream_path_is_claimed dies before the comparison runs.
Record the design as accepted and as shipped: sink shape, record scope, the four-layer config fold, fail-closed posture, and the launcher re-entry that emits a second frame joined on the content digest. States plainly what the sink guard does and does not catch — it pins the designated directory once and refuses a symlink found in that path before a write, which is an integrity control against accidental redirection, not a defence against an adversary racing it.
michael-herwig
force-pushed
the
feat/exec-resolution-record
branch
from
July 27, 2026 22:05
644945c to
0838bf3
Compare
Write one JSON record per launch frame — `ocx run`, `ocx package exec`, `ocx launcher exec` — to an operator-designated sink, immediately before the child starts. Each record names the resolved package closure with digests plus the resolved leaf executable, so a reporter can answer which bits actually ran without re-deriving them from the store. The sink is configured by `[records]`, `OCX_RECORDS_DIR` / `OCX_RECORDS_NAME`, or `--records-dir` / `--records-name`; `required` is config-only, and a SYSTEM-scope lock makes the invocation fail closed rather than run unrecorded. Records publish atomically under a no-clobber name, owner-only, and are not fsync'd. Every field is named for the value it actually carries: `process` has no `args` (a command line can carry credentials that a fleet-aggregated sink must not collect), `resolution.registries` reports the registry content came from rather than the namespace it was requested under, and a package's platform annotation reflects what resolution selected rather than what the caller asked for. Spawning is folded behind one seam so a launch cannot skip its record by construction, with the two escapes types cannot close policed by structural tests. Fail-closed means fail-closed, so the paths that could quietly opt out of a locked policy are closed with it rather than after it: - `OCX_NO_CONFIG=1` used to prune the system tier wholesale, dropping a SYSTEM-scope policy along with it. It now still loads the system file, filtered to its locked sections, matching the flag's documented hermetic-CI intent. - An explicitly written `required = true` naming no sink is a configuration error (exit 78) rather than a policy that records nothing: it is the plainest way to ask for recording and used to deliver none. A SYSTEM-locked block with no `dir` is left alone — that is an operator locking recording off for the host, and it still resolves to off. - A symlinked or unreadable `/etc/ocx/config.toml` is fatal (exit 78) instead of skipped with a warning, since skipping it drops every locked section with it. Absence stays silent, and the user and `$OCX_HOME` tiers keep best-effort discovery. - The maintainer-preview exemption (`ocx package test`, `ocx patch test`) is bounded by the resolved policy. It is claimed from a pkg-root under `$OCX_HOME/temp/`, which the invoking user owns, so it is forgeable by placement and no capability token can fix that at the same uid — what the operator does control is the posture, so a fail-closed policy over a live sink grants no exemption and refuses the launch (exit 74). The bound applies to the `--script` branches too, which reach the Starlark host's own spawn path rather than a `Launch`. Closes #214
michael-herwig
force-pushed
the
feat/exec-resolution-record
branch
from
July 28, 2026 11:14
0838bf3 to
602aca2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #214.
Writes one JSON record per launch frame —
ocx run,ocx package exec,ocx launcher exec— to an operator-designated sink, immediately before the child starts. Each record names the resolved package closure with digests plus the resolved leaf executable, so a reporter can answer which bits actually ran without re-deriving them from the store.Design record:
adr_exec_resolution_record.md. User docs:reference/execution-records.md.Interfaces added
[records]config (dir,name,required),OCX_RECORDS_DIR/OCX_RECORDS_NAME, and--records-dir/--records-nameonocx runandocx package exec.ocx launcher execcarries neither flag and inherits the sink via forwarded env.requiredis config-file-only at every tier. A SYSTEM-scope[records]lock clamps the whole block and makes the invocation fail closed — exit 74 for an unwritable sink, 78 for a configuration fault — rather than run unrecorded.execution-record/v1.json, pinned in lockstep with the in-bandschemaVersion.What review changed
The branch went through a max-tier review (8 Claude perspectives + a
gpt-5.6-solcross-model gate). Four blockers and eight wire-format defects came back, and the fixes are folded into the commits below rather than appended:OCX_NO_CONFIG=1silently defeated a SYSTEM-scope lock. The flag pruned the system tier wholesale, so an operator's policy stopped applying under it. Now the system file loads under the flag, filtered to only its locked sections. This reaches beyond[records]— a locked[registry],[registries.<name>],[mirrors]or[patches]also applies now, which is the same defect class and is called out in the changelog./var/log/ocx/records, because macOS reaches/varthrough/private/var— permanently, underrequired = true. The sink is now canonicalized and pinned once at designation; what is refused is substitution after that. The docs and the ADR state plainly what that does and does not catch: it is an integrity control against accidental redirection, not a defence against an adversary racing it.parent_pidwalked the entire process table on the launch path, behind a comment claiming it read one entry. The key is now omitted there.process.argsis gone entirely (a command line can carry credentials, and this sink is operator-collected and routinely fleet-aggregated);resolution.registriesreports the registry content actually came from rather than the namespace it was requested under; a package's platform annotation reflects what resolution selected, not what the caller asked for;binaries/entrypointsare arrays rather than comma-joined strings, which was lossy because commas are legal in those names.Behaviour change outside the feature
ocx package select -p <foreign-platform>no longer wirescurrent. Resolving a package already in the store did not record which platform resolution selected, so the host-runnable gate that already suppressed the symlink onocx installsaw nothing to act on. Both paths now agree.Verification
task verifygreen: 1827 passed, 42 skipped, 3 xfailed, 2 xpassed. Acceptance coverage includes the emitted record validating against the published schema, so the serializer and the schema can no longer drift with both green.