Skip to content

Commit 7779cb0

Browse files
ci(host-proofs): run the binary-gated real-host install proofs on every PR with pinned CLIs (#395)
* ci(host-proofs): run the binary-gated real-host install proofs on every PR with pinned CLIs The host-install, packed host-install, and packed Claude plugin-validation proofs skip their Claude/Codex legs when the CLIs are absent, which let #364 break both proofs on main unnoticed (#367/#368 repaired them from local runs). - Pin @anthropic-ai/claude-code@2.1.250 and @openai/codex@0.147.0 as `hostCli` in each adapter's schema PROVENANCE.json; scripts/host-cli-pins.mjs reads the pins, refuses a pin that differs from observedCliVersion, installs them (re-running Claude's postinstall when npm blocks it), and fails closed with one diagnostic line when the CLI on PATH is not the pin. - Add the host-install-proofs CI job (cached CLI prefix, version gate, build, test:host-install, test:host-install:packed, test:packed:native) honoring the docs-only skip. No secrets or login are needed. - Harden the Codex interface proof: one shared pinned snapshot for both proof suites, installed manifest must equal the built artifact, validate against the pinned plugin schema, and reject fields outside the adapter's exported codexInterfaceFields. - Document the job and the local commands in docs/local-ci.md and README. * docs(local-ci): list dev-live-host among the CI-run host proofs * fix(ci): key host CLI cache on package names and resolve npm global bin via prefix Address the automated review on #395: the actions/cache key now includes each pinned hostCli.package as well as its version, so a re-pin to a different package with the same version misses the cache instead of reusing stale binaries; and the installer derives npm's global bin directory from `npm prefix -g` (<prefix>/bin on POSIX, the prefix itself on Windows) instead of walking up from `npm root -g`. * fix(ci): hash exact host CLI pins into the cache key Sanitising package names for the actions/cache key can map distinct packages (`@foo/bar`, `foo-bar`) to the same text; append a 16-hex SHA-256 of the exact `package@version` pairs so any re-pin misses the cache. * fix(ci): probe installed host CLIs from the prefix only and reject bare --prefix The post-install probe now runs with PATH limited to the prefix bin directory plus the running Node, so a pre-existing claude/codex elsewhere on the inherited PATH cannot mask an incomplete prefix. `install --prefix` without a directory operand is rejected instead of silently installing into the default global npm prefix.
1 parent 56b77db commit 7779cb0

16 files changed

Lines changed: 809 additions & 31 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"agent-bundle": patch
3+
---
4+
5+
Pin the Claude Code and Codex CLI versions the real-host install proofs run
6+
against beside each adapter's schema provenance (`hostCli` in
7+
`src/adapters/schemas/{claude,codex}/PROVENANCE.json`, kept equal to
8+
`observedCliVersion`), and export the Codex adapter's declared
9+
`codexInterfaceFields` so the host-install proofs can reject an undeclared
10+
`interface` emission before comparing against their single pinned snapshot.
11+
Repository CI now installs the pinned CLIs and runs the host-install, packed
12+
host-install, and packed Claude plugin-validation proofs on every change,
13+
signed out and without secrets. No runtime behavior changes.

‎.github/workflows/ci.yml‎

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,66 @@ jobs:
175175
# the nightly packed-matrix job and in pre-publish `pnpm check:release`.
176176
- run: pnpm check:release:ci
177177

178+
# Binary-gated real-host install proofs. The proof suites skip their Claude
179+
# and Codex legs when the CLIs are absent, which is how #364 changed the
180+
# Codex `interface.logo` emission and broke both proofs on main without CI
181+
# noticing (#367/#368 repaired them from local runs). This job installs the
182+
# exact CLI versions pinned in each adapter's schema PROVENANCE.json
183+
# (`hostCli`, kept equal to `observedCliVersion` by scripts/host-cli-pins.mjs),
184+
# fails closed if `claude`/`codex --version` differs from the pin, and runs
185+
# the source-built proofs, the packed-tarball proofs, and the packed Claude
186+
# plugin validation. Nothing here needs a login: every proof runs against an
187+
# isolated HOME / CLAUDE_CONFIG_DIR / CODEX_HOME and no secrets are passed.
188+
# Signed-in smokes (`claude -p`, `codex exec`, the Eval harnesses) stay in
189+
# the opt-in native-host-smoke workflow.
190+
host-install-proofs:
191+
needs: changes
192+
if: >-
193+
${{ !cancelled() && github.event_name != 'schedule' &&
194+
(github.event_name != 'pull_request' || needs.changes.outputs.docs_only != 'true') }}
195+
name: Host install proofs (Node 22.19)
196+
runs-on: ubuntu-latest
197+
timeout-minutes: 30
198+
steps:
199+
- uses: actions/checkout@v7
200+
- uses: pnpm/setup@v2
201+
with:
202+
cache: true
203+
install: false
204+
runtime: node@22.19.0
205+
- run: pnpm install --frozen-lockfile
206+
- name: Read pinned host CLI versions
207+
id: pins
208+
run: node scripts/host-cli-pins.mjs print
209+
# The global npm prefix holding both CLIs (Claude Code and Codex ship
210+
# platform-native binaries), keyed by OS, arch, and the exact pins
211+
# (package name and version per host) so a deliberate re-pin of either
212+
# misses the cache and installs fresh.
213+
- name: Restore pinned host CLIs
214+
id: host-cli-cache
215+
uses: actions/cache@v6
216+
with:
217+
path: ${{ runner.temp }}/host-cli
218+
key: host-cli-${{ runner.os }}-${{ runner.arch }}-${{ steps.pins.outputs.pins }}
219+
- name: Install pinned host CLIs
220+
if: steps.host-cli-cache.outputs.cache-hit != 'true'
221+
run: node scripts/host-cli-pins.mjs install --prefix "$RUNNER_TEMP/host-cli"
222+
# ubuntu runner: npm's global executables live in <prefix>/bin (the
223+
# script's globalBinDirectory rule; Windows would be the prefix itself).
224+
- name: Expose host CLIs on PATH
225+
run: echo "$RUNNER_TEMP/host-cli/bin" >> "$GITHUB_PATH"
226+
- name: Verify host CLI versions match the schema pins
227+
run: node scripts/host-cli-pins.mjs verify
228+
- run: pnpm build
229+
- name: Host install proofs (source-built bundle)
230+
run: pnpm test:host-install
231+
- name: Host install proofs (packed tarball)
232+
run: pnpm test:host-install:packed
233+
- name: Packed Claude plugin validation proof
234+
env:
235+
AGENT_BUNDLE_PACKAGE_PREBUILT: '1'
236+
run: pnpm test:packed:native
237+
178238
# Release-boundary scaffolder template matrix (mcp-server, cli-tool) plus
179239
# the full packed pool — the nightly form of pre-publish `check:release`.
180240
packed-matrix:
@@ -207,7 +267,8 @@ jobs:
207267
rsc-runtime-micro-eval:
208268
# Deterministic end-to-end spot-check of the built RSC runtime artifacts
209269
# (hook -> RSC worker -> shared kernel state -> MCP tool lowering) without
210-
# any real Claude/Codex host. Real native-host smokes stay skip-gated in
270+
# any real Claude/Codex host. Login-free real-host install proofs run in
271+
# the host-install-proofs job; signed-in native smokes stay skip-gated in
211272
# the manually dispatched native-host-smoke workflow on purpose.
212273
needs: changes
213274
if: >-

‎.github/workflows/native-host-smoke.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ concurrency:
2020
jobs:
2121
native-host-smoke:
2222
# This runs only on a trusted runner with existing CLI login state; no workflow secrets are passed.
23+
# The login-free subset (host install proofs, packed host install proofs, packed Claude plugin
24+
# validation) also runs on every PR and main push in ci.yml's host-install-proofs job against the
25+
# CLI versions pinned in the adapter schema PROVENANCE files; this workflow adds the signed-in
26+
# `claude -p` / `codex exec` evidence on top, against whatever CLI the trusted runner has.
2327
runs-on: self-hosted
2428
strategy:
2529
fail-fast: false

‎docs/local-ci.md‎

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ Verify-equivalent leg on whatever Node is currently active, with the repo's
2121
normal local worker derivation. It skips the Node matrix and the
2222
examples/release/micro-eval gates, so it is a fast signal, not a merge gate.
2323

24-
Docs-only PRs skip the hosted Verify, examples, release-gates, and micro-eval
25-
jobs. Docs-only means changes under `docs/` or `agent-patterns/`, changeset
24+
Docs-only PRs skip the hosted Verify, examples, release-gates, micro-eval, and
25+
host-install-proofs jobs. Docs-only means changes under `docs/` or `agent-patterns/`, changeset
2626
markdown (`.changeset/*.md`), or top-level markdown. Nested markdown elsewhere
2727
is treated as code. Pushes to `main` never use this skip. The allowlist and
2828
fail-open listing checks are implemented by `scripts/classify-docs-only.mjs`
@@ -75,6 +75,66 @@ duration × test census) is printed and written to
7575
`.worktrees/local-ci/summary.md` (plus `summary.json`); per-step logs land in
7676
`.worktrees/local-ci/logs/`. The command exits non-zero if any step fails.
7777

78+
## Real-host install proofs
79+
80+
The host-install proof suites (`host-install-proof.test.ts`,
81+
`packed-host-install-proof.test.ts`, `dev-host-install.test.ts`,
82+
`dev-live-host.test.ts`, and the packed Claude plugin validation in
83+
`packed-native-smoke.test.ts`) skip their
84+
Claude and Codex legs whenever the `claude` or `codex` CLI is not on PATH.
85+
That skip is what let #364 change the Codex `interface.logo` emission and
86+
break both proofs on `main` without CI noticing. Hosted CI therefore runs a
87+
dedicated `host-install-proofs` job (Node 22.19) on every PR and `main` push:
88+
89+
1. `node scripts/host-cli-pins.mjs print` reads the pins — the `hostCli`
90+
block in `packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json`
91+
(`@anthropic-ai/claude-code`) and `.../schemas/codex/PROVENANCE.json`
92+
(`@openai/codex`). Each pin must equal that file's `observedCliVersion`,
93+
so bumping the CLI CI runs is the same deliberate edit as re-pinning the
94+
schemas observed against it.
95+
2. `node scripts/host-cli-pins.mjs install --prefix "$RUNNER_TEMP/host-cli"`
96+
installs exactly those versions into a global npm prefix (cached by OS,
97+
architecture, and pin), then re-runs Claude Code's `install.cjs` when npm
98+
blocked its postinstall, which is what links the native binary.
99+
3. `node scripts/host-cli-pins.mjs verify` fails closed with one diagnostic
100+
line per host when `claude --version` or `codex --version` on PATH is not
101+
the pin.
102+
4. `pnpm build`, `pnpm test:host-install`, `pnpm test:host-install:packed`,
103+
and `AGENT_BUNDLE_PACKAGE_PREBUILT=1 pnpm test:packed:native`.
104+
105+
None of it needs a login or a secret: every proof runs against an isolated
106+
`HOME` / `CLAUDE_CONFIG_DIR` / `CODEX_HOME`, and the subcommands involved
107+
(`claude plugin marketplace add`, `plugin install`, `plugin list --json`,
108+
`plugin details`, `plugin validate --strict`; `codex plugin marketplace add`,
109+
`plugin add`, `plugin list`) work signed out. The job passes no workflow
110+
secrets.
111+
112+
To run the same proofs locally:
113+
114+
```sh
115+
pnpm check:host-cli # your PATH claude/codex must match the pins
116+
pnpm build
117+
pnpm test:host-install
118+
pnpm test:host-install:packed
119+
AGENT_BUNDLE_PACKAGE_PREBUILT=1 pnpm test:packed:native
120+
```
121+
122+
If your machine tracks a newer CLI, install the pinned versions into a
123+
throwaway prefix and put it first on PATH for the run:
124+
125+
```sh
126+
node scripts/host-cli-pins.mjs install --prefix /tmp/host-cli
127+
PATH=/tmp/host-cli/bin:$PATH pnpm check:host-cli
128+
PATH=/tmp/host-cli/bin:$PATH pnpm test:host-install
129+
```
130+
131+
What stays binary-gated *and* login-gated, and therefore outside this job:
132+
the `claude -p` session proofs (`AGENT_BUNDLE_HOST_INSTALL_CLAUDE_SESSION`,
133+
`AGENT_BUNDLE_NATIVE_CLAUDE_SMOKE`), the Codex `exec` smoke
134+
(`AGENT_BUNDLE_NATIVE_CODEX_SMOKE`), and the packed Eval smokes
135+
(`AGENT_BUNDLE_PACKED_NATIVE_{CLAUDE,CODEX}_SMOKE`). Those still run only in
136+
the opt-in `native-host-smoke` workflow on a signed-in runner.
137+
78138
## Node provisioning
79139

80140
The runner introduces no new tooling. For each hosted runtime line
@@ -116,6 +176,11 @@ never lets it drop below what its own pool shape requires.
116176
hosted-only, PR-time check.
117177
- **package-preview** (pkg.pr.new) and the **release publish** workflow are
118178
publish-side effects, not checks; nothing about them gates a merge.
179+
- **host-install-proofs** needs the pinned `claude` and `codex` CLIs on PATH
180+
(see [Real-host install proofs](#real-host-install-proofs)). The local gate
181+
does not install host CLIs into its legs, so run those proofs by hand with
182+
the commands above when a change touches adapter emission, the installers,
183+
or the proof suites.
119184
- **native-host-smoke** needs signed-in Claude/Codex CLIs and is opt-in even
120185
on hosted CI.
121186
- **Environment skew**: hosted runners are `ubuntu-latest`, and hosted jobs

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"typecheck": "tsc --noEmit && tsc --project packages/workbench/tsconfig.json && tsc --project packages/create-agent-bundle/tsconfig.json",
2626
"check": "pnpm build && pnpm test:unit && pnpm test:route-unit && pnpm test:projection && pnpm test:integration:run && pnpm lint && pnpm typecheck",
2727
"check:local-ci": "node scripts/local-ci.mjs",
28+
"check:host-cli": "node scripts/host-cli-pins.mjs verify",
2829
"docs:runtime-topology": "node scripts/rsc-runtime-topology.mjs --root . --output docs/architecture/rsc-runtime-workbench.md",
2930
"eval:spot": "pnpm build && pnpm --filter @agent-bundle/rsc-agent-runtime-demo build && pnpm --filter @agent-bundle/rsc-agent-runtime-demo exec rstest run tests/micro-eval.spot.test.ts --config rstest.config.ts",
3031
"check:runtime-topology": "node scripts/rsc-runtime-topology.mjs --root . --output docs/architecture/rsc-runtime-workbench.md --check",

‎packages/agent-bundle/README.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,12 @@ prove native-host install or dispatch, or an install mode that copies the
437437
artifact elsewhere. `host-install` is separate installed-layout process
438438
evidence: its deterministic adapter-simulator lane is unconditional, available
439439
Claude and Codex binaries also prove their public install paths, and Cursor
440-
records its unavailable non-interactive host-session surface explicitly.
440+
records its unavailable non-interactive host-session surface explicitly. The
441+
Claude and Codex legs skip when those binaries are absent, so the repository's
442+
CI runs them on every change against the exact CLI versions pinned beside each
443+
adapter's schema provenance (`hostCli` in `src/adapters/schemas/*/PROVENANCE.json`),
444+
signed out and with no secrets; only the `claude -p` session and Eval smokes
445+
remain login-gated.
441446

442447
### Contract matrix (`runContractMatrix` / `runPackedContractMatrix` / `runInstalledHostContractMatrix`)
443448

‎packages/agent-bundle/src/adapters/codex.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,12 @@ const isAbsoluteUrl = (value: unknown): value is string => {
241241
}
242242
};
243243

244-
const codexInterfaceFields = Object.freeze([
244+
/**
245+
* Every `interface` field the adapter can emit — generated or authored through
246+
* the `codex.interface` extension. The host-install proofs check installed
247+
* manifests against this declared set so an undeclared emission cannot land.
248+
*/
249+
export const codexInterfaceFields = Object.freeze([
245250
'brandColor',
246251
'capabilities',
247252
'category',

0 commit comments

Comments
 (0)