From a5b16834c8dbf9f2be322b46f7ce716697da82de Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 12 Oct 2026 13:26:56 -0500 Subject: [PATCH] fix: pin ARM qualification helper to workflow source Keep frozen product and reusable workflow tooling identities separate. Preserve clean-consumer and package integrity checks. Co-Authored-By: Paperclip --- .github/workflows/release-smoke.yml | 41 +++++++++++++- doc/plans/2026-10-06-default-agent-runner.md | 41 ++++++++++++++ .../release-verify-workflow.test.mjs | 54 ++++++++++++++++++- 3 files changed, 134 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-smoke.yml b/.github/workflows/release-smoke.yml index 8eaa07abe3..6301a4d1c1 100644 --- a/.github/workflows/release-smoke.yml +++ b/.github/workflows/release-smoke.yml @@ -308,10 +308,49 @@ jobs: path: ${{ runner.temp }}/public-runner-packs - name: Install only the existing verifier tooling run: pnpm install --ignore-scripts --no-frozen-lockfile + - name: Checkout immutable ARM qualification tooling + if: ${{ matrix.target == 'linux-arm64' }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + # job.workflow_* identifies this reusable workflow, not its caller. + repository: ${{ job.workflow_repository }} + ref: ${{ job.workflow_sha }} + path: .qualification-tooling + sparse-checkout: scripts/prepare-linux-arm64-qualification.sh + sparse-checkout-cone-mode: false + persist-credentials: false - name: Prepare Linux ARM sandbox prerequisites if: ${{ matrix.target == 'linux-arm64' }} timeout-minutes: 5 - run: bash scripts/prepare-linux-arm64-qualification.sh + env: + SOURCE_SHA: ${{ inputs.qualification_source_sha }} + TOOLING_SHA: ${{ job.workflow_sha }} + TOOLING_REPOSITORY: ${{ job.workflow_repository }} + TOOLING_WORKFLOW_REF: ${{ job.workflow_ref }} + TOOLING_WORKFLOW_PATH: ${{ job.workflow_file_path }} + run: | + set -euo pipefail + [[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]] || exit 1 + [[ "$TOOLING_SHA" =~ ^[a-f0-9]{40}$ ]] || exit 1 + [[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1 + [[ "$(git -C .qualification-tooling rev-parse HEAD)" == "$TOOLING_SHA" ]] || exit 1 + node --input-type=module <<'NODE' + import { createHash } from 'node:crypto'; + import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; + import { join } from 'node:path'; + const directory = join(process.env.RUNNER_TEMP, 'paperclip-arm-bwrap-prerequisite'); + mkdirSync(directory, { recursive: true }); + const helper = '.qualification-tooling/scripts/prepare-linux-arm64-qualification.sh'; + writeFileSync(join(directory, 'qualification-identity.json'), JSON.stringify({ + productSourceSha: process.env.SOURCE_SHA, + toolingSourceSha: process.env.TOOLING_SHA, + toolingRepository: process.env.TOOLING_REPOSITORY, + toolingWorkflowRef: process.env.TOOLING_WORKFLOW_REF, + toolingWorkflowPath: process.env.TOOLING_WORKFLOW_PATH, + helperSha256: createHash('sha256').update(readFileSync(helper)).digest('hex'), + }, null, 2) + '\n'); + NODE + bash .qualification-tooling/scripts/prepare-linux-arm64-qualification.sh - name: Verify host identity and actual clean npm consumer env: SOURCE_SHA: ${{ inputs.qualification_source_sha }} diff --git a/doc/plans/2026-10-06-default-agent-runner.md b/doc/plans/2026-10-06-default-agent-runner.md index f4925472a0..0c46731468 100644 --- a/doc/plans/2026-10-06-default-agent-runner.md +++ b/doc/plans/2026-10-06-default-agent-runner.md @@ -2,6 +2,47 @@ Updated 2026-10-12. +## Latest checkpoint: ARM qualification tooling provenance + +Production code remains frozen at `a200495cc7488f4d59da570407e516cf702e5af8`. +Subsequent changes are QA tooling and documentation. The verified 18-package +archive retains that literal source. All four hosted native producers, Linux x64 +clean installation, and both Mac installed startup/cleanup consumers pass. +The qualified ARM native startup passes with stock scoped OS prerequisites; +the original unprepared-host failure remains recorded below. + +At `0690801`, fresh review found one valid workflow compatibility defect: an +older frozen product source does not contain the new prerequisite helper. The +existing native qualification job now obtains only that helper through a sparse +checkout pinned to `job.workflow_repository` and `job.workflow_sha`. These +identify the workflow defining the current job, including reusable calls. The +product checkout, archive guards, and original native consumer remain unchanged. +Both checkout identities and the helper hash enter the existing diagnostic +artifact. No mutable branch or caller-source fallback is used. + +Five focused controls pass, including execution with a product source that lacks +the helper and rejection of mismatched checkout identities. Syntax/YAML checks +pass. The full affected workflow suite passes all 70 controls with localhost +fixture access; it runs no native or Docker builds. Receipt: +`/private/tmp/codex-default-arm-immutable-tooling-fix-receipt.json`. +The [committed-helper consumer](https://github.com/paperclipai/paperclip/actions/runs/38290234153) +checks tooling `0690801` against literal `a200495` packages with no diagnostic +overlay, no native rebuild, and no provider turns. Final results and latest-head +CI/review remain required before sign-off. + +The local preview serves `2026.1005.0+352.git.0690801f13` at +[3125](http://127.0.0.1:3125/COD/agents/all). All four complete agent configurations +and both historical native runs remain unchanged after restart. The downloaded +Apple Silicon daemon is verified; `needsBuild` and `sourceBuild` are false. +No local Dockerfile/native build or provider call ran. + +Next action: freeze the workflow-only correction, prove its actual sparse +checkout on hosted ARM against the unchanged verified package archive, and +require fresh CI/review. Refresh the preview and PR evidence at that candidate. +Human code-owner approval, managed staging onboarding, and actual post-merge +Cloud composition remain outstanding. No merge or production deployment is +authorized. Historical unknown costs and cleanup holds remain reserved. + ## Current amendment: remote build performance and Linux baseline The user requires Cargo dependency caching and overlapping JavaScript/native diff --git a/scripts/__tests__/release-verify-workflow.test.mjs b/scripts/__tests__/release-verify-workflow.test.mjs index 6d0ecedad4..2aa415cd20 100644 --- a/scripts/__tests__/release-verify-workflow.test.mjs +++ b/scripts/__tests__/release-verify-workflow.test.mjs @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; import { spawn, spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -967,13 +968,64 @@ test("native ARM qualification prepares its scoped distro sandbox before the exi const preparation = native.split(" - name: Prepare Linux ARM sandbox prerequisites\n")[1].split("\n - ")[0]; assert.match(preparation, /if: \$\{\{ matrix\.target == 'linux-arm64' \}\}/); assert.match(preparation, /timeout-minutes: 5/); - assert.match(preparation, /run: bash scripts\/prepare-linux-arm64-qualification\.sh/); + assert.match(preparation, /bash \.qualification-tooling\/scripts\/prepare-linux-arm64-qualification\.sh/); + const tooling = native.split(" - name: Checkout immutable ARM qualification tooling\n")[1].split("\n - ")[0]; + assert.match(tooling, /if: \$\{\{ matrix\.target == 'linux-arm64' \}\}/); + assert.match(tooling, /repository: \$\{\{ job\.workflow_repository \}\}/); + assert.match(tooling, /ref: \$\{\{ job\.workflow_sha \}\}/); + assert.match(tooling, /sparse-checkout: scripts\/prepare-linux-arm64-qualification\.sh/); + assert.match(tooling, /sparse-checkout-cone-mode: false/); + assert.match(tooling, /persist-credentials: false/); + assert.doesNotMatch(tooling + preparation, /github\.workflow_sha|github\.sha|ref: (?:master|main)|ref: \$\{\{ inputs\.qualification_source_sha/); + assert.ok(native.indexOf("Checkout immutable ARM qualification tooling") < native.indexOf("Prepare Linux ARM sandbox prerequisites")); assert.ok(native.indexOf("Prepare Linux ARM sandbox prerequisites") < native.indexOf("Verify host identity and actual clean npm consumer")); assert.match(native, /path: \|\n\s+\$\{\{ runner\.temp \}\}\/native-public-install\.json\n\s+\$\{\{ runner\.temp \}\}\/paperclip-arm-bwrap-prerequisite\//); assert.doesNotMatch(smoke.split(" qualification_daemons:\n")[0], /prepare-linux-arm64-qualification/, "ordinary service and Docker smoke keep their existing setup"); }); +test("ARM qualification binds separate product and reusable-workflow revisions even when the product lacks the helper", { skip: process.platform === "win32" }, () => { + const native = readWorkflow("release-smoke.yml").split(" qualification_native:\n")[1]; + const step = native.split(" - name: Prepare Linux ARM sandbox prerequisites\n")[1].split("\n - ")[0]; + const script = step.split(" run: |\n")[1].split("\n").map(line => line.startsWith(" ") ? line.slice(10) : line).join("\n"); + const root = mkdtempSync(path.join(tmpdir(), "arm-separate-tooling-")); + try { + const tools = path.join(root, "tools"); mkdirSync(tools); + const tooling = path.join(root, ".qualification-tooling", "scripts"); mkdirSync(tooling, { recursive: true }); + const helper = 'printf "%s\\n" "$TOOLING_SHA" > "$FIXTURE_PREPARED"\n'; + writeFileSync(path.join(tooling, "prepare-linux-arm64-qualification.sh"), helper); + writeFileSync(path.join(tools, "git"), '#!/bin/bash\nif [[ "$1" == -C ]]; then printf "%s\\n" "$FIXTURE_TOOLING_HEAD"; else printf "%s\\n" "$FIXTURE_PRODUCT_HEAD"; fi\n', { mode: 0o755 }); + const productSha = "a".repeat(40), toolingSha = "b".repeat(40); + for (const [productHead, toolingHead, expectedStatus] of [[productSha, toolingSha, 0], ["c".repeat(40), toolingSha, 1], [productSha, "c".repeat(40), 1], [productSha, "invalid", 1]]) { + const report = path.join(root, "report"); mkdirSync(report); + const prepared = path.join(root, "prepared"); + const result = spawnSync("/bin/bash", ["-c", script], { cwd: root, encoding: "utf8", env: { + PATH: `${tools}:${path.dirname(process.execPath)}:/usr/bin:/bin`, RUNNER_TEMP: report, + SOURCE_SHA: productSha, TOOLING_SHA: toolingSha, TOOLING_REPOSITORY: "paperclipai/paperclip", + TOOLING_WORKFLOW_REF: "paperclipai/paperclip/.github/workflows/release-smoke.yml@refs/heads/qualification", + TOOLING_WORKFLOW_PATH: ".github/workflows/release-smoke.yml", + FIXTURE_PRODUCT_HEAD: productHead, FIXTURE_TOOLING_HEAD: toolingHead, FIXTURE_PREPARED: prepared, + // Reusable calls retain the caller context; it must not select tooling. + GITHUB_REPOSITORY: "different/caller", GITHUB_SHA: "d".repeat(40), + } }); + assert.equal(result.status, expectedStatus, result.stderr); + assert.equal(existsSync(path.join(root, "scripts", "prepare-linux-arm64-qualification.sh")), false); + assert.equal(existsSync(prepared), expectedStatus === 0); + if (expectedStatus === 0) { + assert.equal(readFileSync(prepared, "utf8").trim(), toolingSha); + const identity = JSON.parse(readFileSync(path.join(report, "paperclip-arm-bwrap-prerequisite", "qualification-identity.json"), "utf8")); + assert.equal(identity.productSourceSha, productSha); + assert.equal(identity.toolingSourceSha, toolingSha); + assert.equal(identity.toolingRepository, "paperclipai/paperclip"); + assert.equal(identity.toolingWorkflowPath, ".github/workflows/release-smoke.yml"); + assert.equal(identity.helperSha256, createHash("sha256").update(helper).digest("hex")); + } + rmSync(report, { recursive: true, force: true }); + rmSync(prepared, { force: true }); + } + } finally { rmSync(root, { recursive: true, force: true }); } +}); + test("Linux ARM prerequisite setup rejects other hosts before invoking tools and preserves scoped security", { skip: process.platform === "win32" }, () => { const helper = path.join(repoRoot, "scripts/prepare-linux-arm64-qualification.sh"); const source = readFileSync(helper, "utf8");