Skip to content

feat(record): exec-time resolution record - #238

Open
michael-herwig wants to merge 4 commits into
mainfrom
feat/exec-resolution-record
Open

feat(record): exec-time resolution record#238
michael-herwig wants to merge 4 commits into
mainfrom
feat/exec-resolution-record

Conversation

@michael-herwig

@michael-herwig michael-herwig commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

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-name on ocx run and ocx package exec. ocx launcher exec carries neither flag and inherits the sink via forwarded env.
  • required is 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.
  • Published JSON schema at execution-record/v1.json, pinned in lockstep with the in-band schemaVersion.

What review changed

The branch went through a max-tier review (8 Claude perspectives + a gpt-5.6-sol cross-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=1 silently 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.
  • The sink guard made macOS unusable. Refusing any symlinked ancestor refused an ordinary /var/log/ocx/records, because macOS reaches /var through /private/var — permanently, under required = 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.
  • Windows parent_pid walked the entire process table on the launch path, behind a comment claiming it read one entry. The key is now omitted there.
  • The record was fully assembled even with no sink configured — the common path paid hostname, getcwd, the closure walk and purl construction on every invocation.
  • Eight fields did not mean what their names said. process.args is gone entirely (a command line can carry credentials, and this sink is operator-collected and routinely fleet-aggregated); resolution.registries reports 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/entrypoints are 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 wires current. Resolving a package already in the store did not record which platform resolution selected, so the host-runnable gate that already suppressed the symlink on ocx install saw nothing to act on. Both paths now agree.

Verification

task verify green: 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.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

Unit Test Results

3 760 tests  +193   3 760 ✅ +193   45s ⏱️ +7s
   10 suites ±  0       0 💤 ±  0 
    1 files   ±  0       0 ❌ ±  0 

Results for commit 602aca2. ± Comparison against base commit 57bcaff.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

Deep Test Results

     3 files      26 suites   10m 7s ⏱️
 3 672 tests  3 670 ✅ 0 💤 2 ❌
10 806 runs  10 804 ✅ 0 💤 2 ❌

For more details on these failures, see this check.

Results for commit dbaee1e.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

Acceptance Test Results

1 891 tests  +37   1 820 ✅ +45   4m 50s ⏱️ -17s
    1 suites ± 0      71 💤  -  8 
    1 files   ± 0       0 ❌ ± 0 

Results for commit 602aca2. ± Comparison against base commit 57bcaff.

♻️ 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
michael-herwig force-pushed the feat/exec-resolution-record branch from dbaee1e to 644945c Compare July 27, 2026 17:23
`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
michael-herwig force-pushed the feat/exec-resolution-record branch from 644945c to 0838bf3 Compare July 27, 2026 22:05
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
michael-herwig force-pushed the feat/exec-resolution-record branch from 0838bf3 to 602aca2 Compare July 28, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Managed configuration option to always log digest when package is invoked

1 participant