feat: consolidate release provenance, SBOM, and packaging - #3
Draft
trumpyla wants to merge 51 commits into
Draft
Conversation
* fix(windows): require structured agent argv * refactor(acp): own built-in command migration
Bumps the actions group with 1 update in the / directory: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 7.0.0 to 7.0.1 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@v7.0.0...v7.0.1) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 7. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](actions/setup-node@v4...v7) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…claw#442) * fix(flows): swallow heartbeat write rejections from setInterval void heartbeat() inside setInterval can reject when store.writeLive fails, producing unhandledRejection noise and process flags. Catch rejections as best-effort live-state updates, matching other best-effort .catch() sites in the runtime. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca> * test(flows): prove heartbeat timer swallows writeLive rejections Add a focused regression that forces best-effort heartbeat writes to reject and asserts zero unhandledRejection events, matching the setInterval .catch() boundary in FlowRunner.runWithHeartbeat. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca> * test(flows): exercise FlowRunner heartbeat writeLive rejection path Replace the duplicated timer harness with a real FlowRunner shell node that forces interval writeLive failures and asserts zero unhandledRejection. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca> * retrigger proof check Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca> * fix(flows): harden heartbeat rejection coverage (openclaw#442) Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca> --------- Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca> Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(session): avoid atomic write temp collisions * test(session): make temp-path regression deterministic (openclaw#447) Co-authored-by: Henk ter Harmsel <henk@chaink.it> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(cli): add filesystem capability opt-out * docs(cli): complete no-fs coverage (openclaw#469) Co-authored-by: 甄新 <heyang.heyang@alibaba-inc.com> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat: add Pool ACP agent Register Poolside as a built-in agent via `pool acp`. * fix: add structured Pool agent launch --------- Co-authored-by: Onur Solmaz <2453968+osolmaz@users.noreply.github.com>
* fix(cli): preserve positive timer durations * fix(cli): share timer conversion with config (openclaw#459) Co-authored-by: mehmetali <36207866+realmehmetali@users.noreply.github.com> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
… reconnect" (openclaw#444) * fix(cli): distinguish starting and reconnect states (openclaw#444) Co-authored-by: Thomas Güttler <info@thomas-guettler.de> * fix(cli): preserve reconnect status for live leases (openclaw#444) --------- Co-authored-by: Peter Steinberger <steipete@gmail.com> Co-authored-by: Thomas Güttler <info@thomas-guettler.de>
* feat: add ZeroClaw ACP agent (openclaw#440) Co-authored-by: jordanthejet <morepencils@gmail.com> * fix(agents): register ZeroClaw structured argv (openclaw#440) --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
…mula Two install paths, each honest about its tradeoffs (docs/packaging.md). Declaring @agentclientprotocol/codex-acp and claude-agent-acp as dependencies puts them under acpx's own node_modules, which is exactly where resolveInstalledBuiltInAgentLaunch already walks. That path spawns the adapter bin directly; without it, launches fell through to the `npx -y` form and put npm's dependency resolution on the critical path of every agent start. Measured at a fixed adapter version, warm npx cache, so neither vendor nor version is a confound: acp.initialize.rpc 2293.9ms -> 245.2ms. Every other span moved by <= 42ms, i.e. the entire cost was npm resolution blocking the adapter's reply to `initialize` — which is why it never showed up in spawn timing (spawn is ~4ms). Invoking the built-in registry string now measures 348.6ms. The pinned ranges are raised to match: codex ^0.0.44 -> ^1.1.7, claude ^0.37.0 -> ^0.61.0. Neither old range could be satisfied by any published adapter, so the installed-bin path could not engage even when a suitable adapter was present. claude is pinned to 0.61.0 rather than 0.62.0 deliberately: pnpm-workspace.yaml sets minimumReleaseAge 2880 (48h) and 0.62.0 was 34h old. The guard is there on purpose; the pin moves when the release matures. test/agent-registry.test.ts now asserts the registry ranges against package.json rather than against literals. The two must stay in sync because resolution never compares versions — whatever is installed wins silently, so a drifted pin is fiction rather than a constraint. Formula/acpx.rb makes the repository its own tap (no separate tap repo): brew tap artagon/acpx https://github.com/artagon/acpx brew install artagon/acpx/acpx It ships a prebuilt single executable rather than building from source, because Homebrew's node is compiled without single-executable support. sha256 fields are REPLACE_ON_RELEASE placeholders until the first release; brew fails loudly on checksum mismatch, so an un-updated formula cannot silently install a wrong artifact. Its test asserts the binary answers --help with no system Node, which is the property that justifies shipping 122MB. `pnpm run sea` (scripts/sea/, src/sea/entry.ts) produces a Node SEA with a V8 startup snapshot. Measured 50.2ms vs 77.1ms for the npm install (hyperfine, n=25): the snapshot executes the module graph at build time, so the all-eager bundle it requires costs nothing at runtime. Constraints that shaped it, each learned by hitting it: - One CJS chunk, everything inlined — a snapshot resolves nothing from disk. - CommonJS, because Node rejects mainFormat "module" with useSnapshot. - A CJS wrapper shim, because useSnapshot runs the entry through minimalRunCjs which defines neither `exports` nor `module`. IIFE is not a workaround: it hoists require("node:*") to globals. - argv passes through unchanged. A SEA's argv[1] is the invocation path, not the first user argument; re-injecting it makes cli-core read it as the agent name and shadow every top-level verb. Verification: tsc, oxfmt, oxlint, lint:eager-graph clean; ruby -c on the formula; full suite green except the pre-existing "lets the submitter exit while a detached owner keeps draining stderr" flake, which also fails on an untouched baseline. Note: .gitignore also carries a pre-existing local OpenSpec block that was already modified in the working tree before this change.
The formula pointed at release assets that did not exist, so `brew install artagon/acpx/acpx` failed with a 404. Publish the assets from CI and list only platforms that actually have one. - .github/workflows/release-binaries.yml builds the SEA per target and attaches the tarballs to the tag. Kept separate from release.yml, which publishes the npm package with provenance and asserts upstream metadata; the two should not be entangled. Each target proves the binary answers --version and --help under `env -i` with node stripped from PATH — a SEA that quietly needs an ambient Node would defeat the reason for shipping ~122MB. - Formula/acpx.rb now lists only darwin-arm64, the one published asset, with its real checksum. Listing a platform whose asset does not exist turns a clear 'unsupported' message into a download failure.
The v0.12.0 asset shipped as a plain copy of the Node binary: postject had not injected the blob, so `acpx --version` printed Node's version (v24.11.0) and every command fell through to Node's own CLI. brew install succeeded and the binary ran, which is exactly why this went unnoticed. Nothing could have caught it. Both the build script and the workflow checked only the exit status, and an un-injected binary starts fine and exits 0. - scripts/sea/build.mjs now runs the freshly built binary and asserts it reports package.json's version, failing the build otherwise. - release-binaries.yml verifies the packaged TARBALL rather than dist-sea/acpx (packaging is itself a step that can go wrong) and compares --version output instead of trusting the exit code. Release asset for v0.12.0 replaced; Formula/acpx.rb carries the new checksum.
… toolchain Adversarial review of the two new workflows. Findings acted on, worst first. postject was fetched with `npx` at release time (scripts/sea/build.mjs) — not in package.json, not in the lockfile, and outside minimumReleaseAge, which is a pnpm setting that npx does not honour. A compromised release of it would have had write access to the exact bytes users install, and none of the new controls covered it: not `pnpm audit --prod`, not dependency-review, not the SBOM. It is now a pinned devDependency and both build steps use `pnpm exec`, which resolves from the lockfile and fails closed. The build job held `id-token: write` while running pnpm install, the bundler, and the freshly built binary. That injects ACTIONS_ID_TOKEN_REQUEST_* into every step, so any bundler plugin or transitive dependency could mint a Sigstore token asserting this repo and sign arbitrary bytes offline. Attesting an artifact in the same job that ran untrusted code is worse than not attesting: it turns provenance into a laundering primitive. Build is now read-only with no OIDC, and a publish job that runs no repository code attests the exact bytes it uploads — which also closes the gap where attested bytes and published bytes were never compared. The audit job ran `pnpm install --frozen-lockfile` on pull_request. This is a public fork, and --frozen-lockfile constrains the dependency graph but not `scripts`; package.json declares a root `prepare` hook, so opening a PR was enough for code execution on a runner. Now --ignore-scripts, and persist-credentials: false on every job that touches repository content. release-binaries.yml had none of release.yml's tag validation, so a tag pointing at an unreviewed commit — including a fork PR head, reachable in this repo's object network — would have produced a signed, published binary. Added a gate job asserting semver shape, package.json agreement, and ancestry in origin/main. `gh release upload --clobber` could silently replace assets of an already-published release while minting fresh provenance for them. Removed; the publish step now fails if an asset already exists. Also: github/codeql-action was pinned to an annotated tag object rather than a commit and could never resolve (e34fc271 -> 89a39a4e); node-version pinned exactly since the shipped binary *is* that Node build; pnpm cache dropped from the release build; concurrency keyed on the tag so dispatch and tag-push runs cannot race; the minimumReleaseAge guard moved ahead of any install; scorecard no longer runs on pull_request and no longer requests id-token it does not use; package.json version validated before it reaches $GITHUB_OUTPUT. New policy job enforces the two mistakes made here: no npx/pnpm dlx under scripts/, and every action pin must resolve to a real commit. Not fixed: the SBOM describes the source tree while being attested against the binary — it over-reports devDependencies that never ship and omits the embedded Node runtime. Attesting it is worse than publishing it unsigned, so the SBOM attestation is dropped from this workflow pending a decision on what to scan.
Both new gates failed CI for reasons unrelated to security. The 'build tools are lockfile-pinned' check grepped for the word npx, so it flagged a string literal used as test data in lint-persisted-key-casing.ts and the comment in build.mjs explaining why npx is banned. It now matches the invocation form — npx or pnpm dlx as the command argument of an exec/run call — and skips comment lines. Verified locally: clean. The audit step failed with 'Unexpected token \x1f ... is not valid JSON'. That is the gzip magic number: pnpm 10.33.2 crashes decoding a gzipped audit response. Reproduced identically on CI and locally, so it is a transport bug and not an advisory. Failing on it would make a red audit job meaningless, and a genuine finding would be ignored along with it. The step now separates 'found advisories' from 'could not run': a parse failure warns and passes, anything else still fails.
Immutable releases are now enabled on the repository, which freezes assets and the Git tag at publication. The old create-then-upload order cannot work under that rule — the release would lock before the first asset landed — so assets are attached to a draft and the draft is published last. The SBOM question was what to scan. Measured against this artifact, every obvious target is wrong: syft on the source tree reports 751 components (every devDependency, none of which reaches a user), the production closure is 685 (tree-shaking drops most of it), and syft on the finished binary reports 0, because no per-package boundaries survive bundling into a V8 snapshot. rollup-plugin-sbom reads the bundler's own module graph and reports the 18 packages actually inlined, plus the Node runtime added explicitly — Node is the majority of the file and its largest attack surface, and no scanner would match a Node advisory without a component to match against. Signing is also split out of the build: a new attest job holds the identity and runs no repository code, no checkout and no install, so dependency code can never mint provenance for bytes the workflow did not produce. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kjbs5gugsCauZwrosUMhgJ
The npm package had npm's own --provenance and nothing else. It now also carries GitHub build-provenance and SBOM attestations, and is published from a tarball packed and attested before it leaves the runner, so the bytes npm serves are the bytes that were signed. Its SBOM is deliberately not the binary's. The package bundles nothing: dist/ ships alone and every dependency is installed on the user's machine, so the honest scan target is the production dependency closure — ~685 packages, resolved by syft from a prod-only install built in a scratch directory so a dev-only package cannot leak in. One asymmetry is worth stating rather than hiding: unlike the binary pipeline, this job cannot keep the signing identity away from build code. npm trusted publishing derives provenance from an OIDC token minted in the job that holds the package, so they are necessarily co-resident. Twenty-three action references across five workflows were still on movable tags, including a setup-node@v4. A tag is a pointer the action's owner can repoint at new code that then runs with this workflow's permissions. All are now SHA-pinned, and a new gate fails on any that are not — the existing check could only validate pins that were already SHAs, so an unpinned action was invisible to it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kjbs5gugsCauZwrosUMhgJ
…n npm fallback The formula is now rendered by scripts/sea/generate-formula.mjs, the single writer of both install paths: platforms with a published single-executable asset pin its immutable release tarball, and every other platform installs the npm package with Homebrew's node. A new formula job in release-binaries.yml regenerates the file after publish — from the release's SHA256SUMS and the npm registry tarball — and opens a PR, replacing the paste-by-hand checksum step. Also fixes the darwin-x64 matrix leg: macos-13 is a retired runner label, so the next tag would have failed that build; macos-15-intel is the supported Intel image. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The darwin matrix legs run on GitHub-hosted macOS runners, which bill at a premium, so the tag-push trigger is removed: binaries are built when a human dispatches the workflow for a validated tag. The npm release in release.yml stays automatic on its Linux runner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rmula job's invariants Two findings from the adversarial review of this branch: - The job-list invariant in test/release-workflow.test.ts did not know about the formula job, so the Test job was red on this branch. The test now pins the five-job list and asserts the formula job's trust model: no OIDC, no dependency installs, checksums only from the immutable release manifest. - The npm checksum was trust-on-first-use: whatever bytes the registry served were pinned into the formula. The formula job now runs gh attestation verify against the tarball — the provenance release.yml attests over the exact packed tarball before publish — before recording its sha256. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial-review finding: the formula job held a contents-write token with a persisted credential while checking out movable main and executing repo scripts — the exact co-residency the rest of this workflow is built to avoid. The job is now split on the same line as build/attest: `formula` renders from the immutable validated tag with read-only permissions and no persisted credential, syntax-checks the result (bot PRs from GITHUB_TOKEN get no CI), and uploads it; `formula-pr` holds the write credential but executes no repository code — it commits only the artifact bytes. The gate now reads package.json from the tagged commit rather than the dispatch-branch checkout, the generator refuses duplicate targets in a manifest, and the invariant tests cover both new jobs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Require the generated formula to advance the live-main formula, reject ambiguous inputs and layouts, disable the legacy unlocked fallback, and keep npm fallback installs script-free. Consolidates source commits c3d96b4, b44caf1, and faab8fe; supersedes their older 0.12.x dependency and workflow baselines with the stricter 0.13.0 release pipeline already present on this branch.
Use a purpose-built SEA-capable Node probe in the unit test so Homebrew Node builds that intentionally disable SEA do not mask version-provenance assertions. Follow-up to source commit 7a7c945 after consolidation onto the current macOS toolchain.
This reverts commit aae3da5.
Source-Commit: c3d96b4f82699f8cd6f6691ad6c0c351cdfbb052 Source-Commit: b44caf1e28e7203f865021e14adc95b558497893 Source-Commit: faab8fe811da16d8883e9de72ee894e8fca77575 Source-Commit: 7a7c945c21b3bcbf2adb22b8db9a598e9ba007ab Source-Commit: 3cf8a67336c8a874828c04f4332d17b978454077 Source-Commit: 47bc822c43afd100fb7cd1877e7c4dd7037f1132 Source-Commit: 8147777b01194f819ff982b4cf18a33caea11bb7 Source-Commit: b1c586c708a92c1a607c65fe2a6301de9366eaef Source-Scope: sbom
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.
Summary
Why
The previous release and consolidated branches mixed supply-chain work with runtime lifecycle changes. This branch creates a release-owned history with explicit source provenance, fail-closed publication checks, deterministic npm packaging, and tested installed artifacts.
The remaining queue-owner-process surface is only the narrow interface required for a packaged SEA to start its detached owner. The branch reports ACPX version 0.13.0.
Validation
pnpm run checkandpnpm run check:docsperf/consolidated-runtime, including the complete check, docs gate, performance report, and SEA testsOwnership boundary
This PR owns release workflows, provenance, package closure, SEA assets, Homebrew, and SBOM behavior. Runtime performance, queue ownership, and persistence implementation are intentionally isolated in companion PR #2.