feat(runner): assemble target-owned Claude Linux provider packs

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-12 12:37:57 -05:00
1 parent 18626f677f
commit d0da37c221
11 files changed
+262 -33

No files matched your search

+13 -1
View File
@@ -242,6 +242,13 @@ jobs:
pattern: ${{ needs.qualification_daemons.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
merge-multiple: true
- name: Download target-owned Linux provider-pack identities
if: needs.qualification_daemons.outputs.provider_pack_artifact_prefix != ''
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: ${{ needs.qualification_daemons.outputs.provider_pack_artifact_prefix }}-*
path: ${{ runner.temp }}/release-provider-pack-inputs
merge-multiple: true
- name: Bind the immutable Linux producer source
env:
SOURCE_SHA: ${{ inputs.qualification_source_sha }}
@@ -259,9 +266,14 @@ jobs:
env:
SOURCE_SHA: ${{ inputs.qualification_source_sha }}
run: |
pack_args=()
if [[ -d "$RUNNER_TEMP/release-provider-pack-inputs" ]]; then pack_args=("$RUNNER_TEMP/release-provider-pack-inputs"); fi
node packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs \
assemble-daemons "$RUNNER_TEMP/release-runner-inputs" "$SOURCE_SHA"
assemble-daemons "$RUNNER_TEMP/release-runner-inputs" "$SOURCE_SHA" "${pack_args[@]}"
cp -R packages/paperclip-runner/dist/bin/. server/dist/vendor/paperclip-runner/bin/
if [[ -d packages/paperclip-runner/dist/remote-provider-packs ]]; then
cp -R packages/paperclip-runner/dist/remote-provider-packs server/dist/vendor/paperclip-runner/
fi
- name: Verify clean public installation and retain the same package bytes
env:
PAPERCLIP_RUNNER_PUBLIC_PACKS_DIR: ${{ runner.temp }}/public-runner-packs
+32
View File
@@ -15,6 +15,8 @@ on:
outputs:
runner_artifact_prefix:
value: ${{ jobs.runner_binaries.outputs.artifact_prefix }}
provider_pack_artifact_prefix:
value: ${{ jobs.runner_binaries.outputs.provider_pack_artifact_prefix }}
native_consumer_matrix:
value: ${{ jobs.runner_binary_targets.outputs.native_consumers }}
@@ -513,6 +515,7 @@ jobs:
matrix: ${{ fromJSON(needs.runner_binary_targets.outputs.matrix) }}
outputs:
artifact_prefix: ${{ steps.identity.outputs.artifact_prefix }}
provider_pack_artifact_prefix: ${{ steps.identity.outputs.provider_pack_artifact_prefix }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
@@ -535,6 +538,9 @@ jobs:
[[ "$(git rev-parse HEAD)" == "$SOURCE_SHA" ]] || exit 1
[[ "$(node -p 'process.platform + "-" + process.arch')" == "$TARGET" ]] || exit 1
echo "artifact_prefix=runner-release-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
if node --input-type=module -e 'const m = await import("./packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs"); process.exit(m.RELEASE_PROVIDER_PACK_TARGETS ? 0 : 1)'; then
echo "provider_pack_artifact_prefix=runner-provider-packs-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
fi
- name: Pin native Cargo workspace and toolchain
id: native_rust_workspace
working-directory: packages/paperclip-runner
@@ -618,3 +624,29 @@ jobs:
path: ${{ runner.temp }}/runner-release/
if-no-files-found: error
retention-days: 14
- name: Build target-owned Linux provider pack
id: provider_pack
if: startsWith(matrix.target, 'linux-') && steps.identity.outputs.provider_pack_artifact_prefix != ''
env:
SOURCE_SHA: ${{ inputs.ref }}
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
pack="$RUNNER_TEMP/provider-pack"
PAPERCLIP_RUNNER_SOURCE_REVISION="$SOURCE_SHA" node packages/paperclip-runner/scripts/build-provider-pack.mjs "$pack"
node --input-type=module - "$pack/provider-pack.json" <<'NODE'
import { readFileSync } from 'node:fs';
import { verifyReleaseProviderPack } from './packages/paperclip-runner/scripts/release-provider-pack.mjs';
verifyReleaseProviderPack(JSON.parse(readFileSync(process.argv[2], 'utf8')), process.env.SOURCE_SHA, process.env.TARGET);
NODE
mkdir -p "$RUNNER_TEMP/release-provider-packs/$TARGET"
cp "$pack/provider-pack.json" "$RUNNER_TEMP/release-provider-packs/$TARGET/provider-pack.json"
tar -C "$pack" -czf "$RUNNER_TEMP/release-provider-packs/$TARGET/provider-pack.tar.gz" .
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: startsWith(matrix.target, 'linux-') && steps.identity.outputs.provider_pack_artifact_prefix != ''
with:
name: ${{ steps.identity.outputs.provider_pack_artifact_prefix }}-${{ matrix.target }}
path: ${{ runner.temp }}/release-provider-packs/
if-no-files-found: error
retention-days: 14
+50
View File
@@ -432,6 +432,16 @@ jobs:
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
merge-multiple: true
- name: Download target-owned Linux provider-pack identities
if: needs.release_runner_source.outputs.required == 'true' && needs.release_runner_binaries.outputs.provider_pack_artifact_prefix != ''
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: ${{ needs.release_runner_binaries.outputs.provider_pack_artifact_prefix }}-*
path: ${{ runner.temp }}/release-provider-pack-inputs
merge-multiple: true
- name: Bind provider-pack inputs for release assembly
if: needs.release_runner_source.outputs.required == 'true' && needs.release_runner_binaries.outputs.provider_pack_artifact_prefix != ''
run: echo "PAPERCLIP_RELEASE_PROVIDER_PACK_ARTIFACTS_DIR=$RUNNER_TEMP/release-provider-pack-inputs" >> "$GITHUB_ENV"
- name: Bind daemon inputs for release assembly
if: needs.release_runner_source.outputs.required == 'true'
run: echo "PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR=$RUNNER_TEMP/release-runner-inputs" >> "$GITHUB_ENV"
@@ -729,6 +739,16 @@ jobs:
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
merge-multiple: true
- name: Download target-owned Linux provider-pack identities
if: needs.release_runner_source.outputs.required == 'true' && needs.release_runner_binaries.outputs.provider_pack_artifact_prefix != ''
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: ${{ needs.release_runner_binaries.outputs.provider_pack_artifact_prefix }}-*
path: ${{ runner.temp }}/release-provider-pack-inputs
merge-multiple: true
- name: Bind provider-pack inputs for release assembly
if: needs.release_runner_source.outputs.required == 'true' && needs.release_runner_binaries.outputs.provider_pack_artifact_prefix != ''
run: echo "PAPERCLIP_RELEASE_PROVIDER_PACK_ARTIFACTS_DIR=$RUNNER_TEMP/release-provider-pack-inputs" >> "$GITHUB_ENV"
- name: Bind daemon inputs for release assembly
if: needs.release_runner_source.outputs.required == 'true'
run: echo "PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR=$RUNNER_TEMP/release-runner-inputs" >> "$GITHUB_ENV"
@@ -1008,6 +1028,16 @@ jobs:
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
merge-multiple: true
- name: Download target-owned Linux provider-pack identities
if: needs.release_runner_source.outputs.required == 'true' && needs.release_runner_binaries.outputs.provider_pack_artifact_prefix != ''
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: ${{ needs.release_runner_binaries.outputs.provider_pack_artifact_prefix }}-*
path: ${{ runner.temp }}/release-provider-pack-inputs
merge-multiple: true
- name: Bind provider-pack inputs for release assembly
if: needs.release_runner_source.outputs.required == 'true' && needs.release_runner_binaries.outputs.provider_pack_artifact_prefix != ''
run: echo "PAPERCLIP_RELEASE_PROVIDER_PACK_ARTIFACTS_DIR=$RUNNER_TEMP/release-provider-pack-inputs" >> "$GITHUB_ENV"
- name: Bind daemon inputs for release assembly
if: needs.release_runner_source.outputs.required == 'true'
run: echo "PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR=$RUNNER_TEMP/release-runner-inputs" >> "$GITHUB_ENV"
@@ -1389,6 +1419,16 @@ jobs:
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
merge-multiple: true
- name: Download target-owned Linux provider-pack identities
if: needs.release_runner_source.outputs.required == 'true' && needs.release_runner_binaries.outputs.provider_pack_artifact_prefix != ''
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: ${{ needs.release_runner_binaries.outputs.provider_pack_artifact_prefix }}-*
path: ${{ runner.temp }}/release-provider-pack-inputs
merge-multiple: true
- name: Bind provider-pack inputs for release assembly
if: needs.release_runner_source.outputs.required == 'true' && needs.release_runner_binaries.outputs.provider_pack_artifact_prefix != ''
run: echo "PAPERCLIP_RELEASE_PROVIDER_PACK_ARTIFACTS_DIR=$RUNNER_TEMP/release-provider-pack-inputs" >> "$GITHUB_ENV"
- name: Bind daemon inputs for release assembly
if: needs.release_runner_source.outputs.required == 'true'
run: echo "PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR=$RUNNER_TEMP/release-runner-inputs" >> "$GITHUB_ENV"
@@ -1447,6 +1487,16 @@ jobs:
pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-*
path: ${{ runner.temp }}/release-runner-inputs
merge-multiple: true
- name: Download target-owned Linux provider-pack identities
if: needs.release_runner_source.outputs.required == 'true' && needs.release_runner_binaries.outputs.provider_pack_artifact_prefix != ''
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: ${{ needs.release_runner_binaries.outputs.provider_pack_artifact_prefix }}-*
path: ${{ runner.temp }}/release-provider-pack-inputs
merge-multiple: true
- name: Bind provider-pack inputs for release assembly
if: needs.release_runner_source.outputs.required == 'true' && needs.release_runner_binaries.outputs.provider_pack_artifact_prefix != ''
run: echo "PAPERCLIP_RELEASE_PROVIDER_PACK_ARTIFACTS_DIR=$RUNNER_TEMP/release-provider-pack-inputs" >> "$GITHUB_ENV"
- name: Bind daemon inputs for release assembly
if: needs.release_runner_source.outputs.required == 'true'
run: echo "PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR=$RUNNER_TEMP/release-runner-inputs" >> "$GITHUB_ENV"
+9
View File
@@ -597,6 +597,15 @@ Use the matching Paperclip Daytona image: the controller verifies the image's
pack manifest, executable hashes, bridge tree, and pinned versions before launch.
The controller's operating system does not determine the remote daemon target.
Native Claude remote execution also selects the Linux x64 or ARM64 daemon and
pack identity for its actual selected host. For SSH, install that exact release's
pack at `$HOME/.local/share/paperclip-runner/provider-pack` or
`/opt/paperclip-runner/provider-pack`; the runtime discovers and verifies it.
The package's identities and the native producer archives share one source.
No development pack override is needed for this installed-host path. Other
provider qualification boundaries and the current Linux x64 image path remain.
ARM64 cloud-image production is deferred.
A plain Daytona sandbox image does not contain this provider pack. Custom images
and source-development launchers must supply a verified Linux provider pack and
daemon through the existing remote artifact overrides; see
@@ -94,3 +94,30 @@ Executable identities: daemon source `5f0b898e9ecef77afefe309a230e04d7fef66cde`,
## Approved prerequisite split and current CI
The behavior PR now targets the protocol branch and has 64 changed files. Protocol #15994 targets the latest Codex dependency and adds the shared v7 effort/resume contract with historical readers. Cloud Docker-context verification found that `.dockerignore` omitted the new Pi v23 and Copilot v18 identity inputs; explicit exceptions retain them without broadening the fixture context. The old 9788c05 package campaign passed all four daemon producers and Apple Silicon clean install, but the Linux ARM64 consumer failed durable-suspend settlement. Retain that failure; current-source package qualification and ARM diagnosis remain required. No local Docker or Rust compilation occurred.
## Platform candidate implementation
The isolated platform checkout is `/private/tmp/claude-runner-platform-pr` on
`codex/claude-runner-platforms`. It preserves behavior candidate `8d07b40907`.
The pinned Linux ARM64 Claude SDK 0.3.286 archive passed registry SHA512
integrity, ELF aarch64 inspection and an actual no-prompt version launch on
the authorized cloud ARM host. Its executable SHA256 is
`0292fa22ac2fd43e16be9d0e511ddd8347280d6e0ebaca744ef5b27e05d8d0f8`.
Native admission retains descriptor/no-follow, package and architecture checks.
Release producers now retain target-owned Linux x64 and ARM64 provider-pack
identities and relocatable archives separately from daemon receipts. Assembly
binds both target identities to its daemon source and vendors the manifests.
ARM inventories cannot claim additional Pi, Cursor or Copilot qualification.
Mac controllers probe the selected Claude remote target and select its packaged
identity without a development override. Installed SSH hosts use the standard
pack locations. Additional automatic defaults remain gated pending evidence.
Focused platform checks: pack integrity11, release workflow67, native remote
manifest/selection12, executable/profile/path86 passed (six platform skips).
The first new ARM selection test failed from shared durable fixture identity;
a unique workspace/session per case fixed the fixture. No product guard was
removed. Platform TypeScript build passed. Cloud package and actual Linux task
pairs remain required. Current behavior package campaign:
https://github.com/paperclipai/paperclip/actions/runs/38284813521 .
+16 -1
View File
@@ -170,7 +170,7 @@ the task waits for the next instruction. See the
A release includes all four platform daemons and the Linux provider-pack identity
from its matching Daytona image. Run `stage:release-binaries` with a manifest that
binds each daemon path and SHA-256, plus `remoteProviderPack: {path, sha256}` for
binds each daemon path and SHA-256, plus `remoteProviderPacks: {"linux-x64": {path, sha256}, "linux-arm64": {path, sha256}}` for
the actual image's `provider-pack.json`. Assemble these assets after the normal
build and include them in the server's vendored Runner output before npm packing.
Assembly requires the provider pack's source revision to match `sourceRevision`
@@ -682,3 +682,18 @@ canonical event retained for that command. The header marker `🖇️ v0.1.2`
identifies the current console iteration.
`create_task` accepts an optional initial `status` of `backlog` or `todo`. Use `backlog` when the user wants a saved task or plan without execution: assignment and the initial plan are committed without scheduling a wake, even when dependencies are already complete. Omitting status preserves immediate delegation (`todo`, or `blocked` for unresolved dependencies).
Linux provider-pack producers run on the corresponding native Linux target and
retain `provider-pack.json` with a relocatable `provider-pack.tar.gz`. Release
assembly binds both identities to the same source as its four daemons. The
historical `remoteProviderPack` field remains the Linux x64 identity. Linux
ARM64 packs do not qualify Pi, Cursor or Copilot on that target.
For an installed SSH host, install the same release producer's pack at
`$HOME/.local/share/paperclip-runner/provider-pack` or
`/opt/paperclip-runner/provider-pack`. A Mac controller selects its packaged
Linux target identity after probing the selected host. The runtime verifies the
installed pack before prompting. No development pack override is needed. A
missing or mismatched pack fails clearly. Existing explicit overrides remain
authoritative. ARM64 cloud-image production remains deferred.
@@ -7,11 +7,12 @@ const canonical = value => Array.isArray(value) ? `[${value.map(canonical).join(
: JSON.stringify(value);
/** Release assembly must reject a self-consistent pack built for another release. */
export function verifyReleaseProviderPack(identity, sourceRevision) {
export function verifyReleaseProviderPack(identity, sourceRevision, target = "linux-x64") {
const payload = identity?.payload;
if (identity?.schema !== "paperclip-runner/remote-provider-pack/v1" || !payload
|| payload.target?.platform !== "linux" || payload.target?.architecture !== "x64"
|| payload.runnerSourceRevision !== sourceRevision) throw new Error("Remote provider pack must match the release source and Linux x64 target");
|| !["linux-x64", "linux-arm64"].includes(target)
|| payload.target?.platform !== "linux" || `linux-${payload.target?.architecture}` !== target
|| payload.runnerSourceRevision !== sourceRevision) throw new Error(`Remote provider pack must match the release source and ${target === "linux-x64" ? "Linux x64" : target} target`);
if (identity.digest !== `sha256:${createHash("sha256").update(canonical(payload)).digest("hex")}`) {
throw new Error("Remote provider-pack payload digest mismatch");
}
@@ -19,6 +20,12 @@ export function verifyReleaseProviderPack(identity, sourceRevision) {
|| ["grok", "claude", "codex"].some(agent => payload.acpxProfileDigests?.[agent] !== profiles.profiles[agent].commandDigest)) {
throw new Error("Remote provider-pack ACPX profiles do not match this release");
}
if (target === "linux-arm64") {
if (Object.keys(payload.providers ?? {}).length || Object.keys(payload.candidateProviders ?? {}).length) {
throw new Error("Linux ARM64 pack must not expand other providers qualification");
}
return identity;
}
const current = profiles.profiles.cursor;
for (const cursor of [payload.providers?.cursor, ...(Object.hasOwn(payload.candidateProviders ?? {}, "cursor") ? [payload.candidateProviders.cursor] : [])]) {
if (!cursor || cursor.version !== current.agentServerVersion || cursor.profileDigest !== current.commandDigest
@@ -9,7 +9,7 @@ import test from "node:test";
import profiles from "../acpx-profiles.json" with { type: "json" };
import distributions from "../cursor-distributions.json" with { type: "json" };
import { verifyReleaseProviderPack } from "./release-provider-pack.mjs";
import { existingReleaseProviderPack } from "./stage-release-runner-binaries.mjs";
import { existingReleaseProviderPack, releaseProviderPackManifest } from "./stage-release-runner-binaries.mjs";
const revision = "a".repeat(40);
const digest = "sha256:" + "b".repeat(64);
@@ -94,3 +94,37 @@ test("daemon-only assembly retains an existing same-source Linux provider-pack i
assert.throws(() => existingReleaseProviderPack(root, revision), /Linux x64 target/);
} finally { rmSync(root, { recursive: true, force: true }); }
});
function armFixture() {
const payload = fixture();
payload.target.architecture = "arm64";
delete payload.providers;
return payload;
}
test("ARM pack binds its own target and preserves other providers qualification limits", () => {
const pack = seal(armFixture());
assert.equal(verifyReleaseProviderPack(pack, revision, "linux-arm64"), pack);
assert.throws(() => verifyReleaseProviderPack(pack, revision), /Linux x64 target/);
assert.throws(() => verifyReleaseProviderPack(seal(fixture()), revision, "linux-arm64"), /linux-arm64 target/);
for (const inventory of ["providers", "candidateProviders"]) {
const payload = armFixture(); payload[inventory] = { pi: {} };
assert.throws(() => verifyReleaseProviderPack(seal(payload), revision, "linux-arm64"), /must not expand/);
}
});
test("both remote pack identities bind one source and remain separated during assembly", () => {
const root = realpathSync(mkdtempSync(join(tmpdir(), "release-provider-targets-")));
try {
for (const [target, payload] of [["linux-x64", fixture()], ["linux-arm64", armFixture()]]) {
const file = join(root, target, "provider-pack.json"); mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, JSON.stringify(seal(payload)));
}
const entries = releaseProviderPackManifest(root, revision);
assert.deepEqual(Object.keys(entries), ["linux-x64", "linux-arm64"]);
assert.notEqual(entries["linux-x64"].sha256, entries["linux-arm64"].sha256);
assert.throws(() => releaseProviderPackManifest(root, "c".repeat(40)), /release source/);
writeFileSync(join(root, "linux-arm64", "provider-pack.json"), JSON.stringify(seal(fixture())));
assert.throws(() => releaseProviderPackManifest(root, revision), /linux-arm64 target/);
rmSync(join(root, "linux-arm64"), { recursive: true });
assert.throws(() => releaseProviderPackManifest(root, revision), /exactly both/);
} finally { rmSync(root, { recursive: true, force: true }); }
});
@@ -10,6 +10,7 @@ import { runnerBinaryTarget } from "../src/live/runner-binary.ts";
import { verifyReleaseProviderPack } from "./release-provider-pack.mjs";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
export const RELEASE_RUNNER_TARGETS = Object.freeze(["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64"]);
export const RELEASE_PROVIDER_PACK_TARGETS = Object.freeze(["linux-x64", "linux-arm64"]);
const required = RELEASE_RUNNER_TARGETS;
const digest = bytes => "sha256:" + createHash("sha256").update(bytes).digest("hex");
function regularFile(path, maximumSize) {
@@ -55,10 +56,23 @@ export function releaseDaemonManifest(artifactDirectory, sourceRevision, verifyM
return { sourceRevision, platforms };
}
export function existingReleaseProviderPack(packageRoot, sourceRevision) {
const file = join(packageRoot, "dist/remote-provider-packs/linux-x64/provider-pack.json");
export function existingReleaseProviderPack(packageRoot, sourceRevision, target = "linux-x64") {
if (!RELEASE_PROVIDER_PACK_TARGETS.includes(target)) throw Error("Unsupported release provider pack target");
const file = join(packageRoot, `dist/remote-provider-packs/${target}/provider-pack.json`);
if (!existsSync(file)) return undefined;
return verifyReleaseProviderPack(JSON.parse(regularFile(file, 512 * 1024)), sourceRevision);
return verifyReleaseProviderPack(JSON.parse(regularFile(file, 512 * 1024)), sourceRevision, target);
}
export function releaseProviderPackManifest(artifactDirectory, sourceRevision) {
if (readdirSync(artifactDirectory).sort().join() !== [...RELEASE_PROVIDER_PACK_TARGETS].sort().join()) {
throw Error("Release provider packs require exactly both Linux target directories");
}
return Object.fromEntries(RELEASE_PROVIDER_PACK_TARGETS.map(target => {
const path = join(resolve(artifactDirectory), target, "provider-pack.json");
const bytes = regularFile(path, 512 * 1024);
verifyReleaseProviderPack(JSON.parse(bytes), sourceRevision, target);
return [target, { path, sha256: digest(bytes) }];
}));
}
async function main() {
@@ -79,11 +93,12 @@ async function main() {
return;
}
const daemonOnly = process.argv[2] === "assemble-daemons";
if (daemonOnly ? process.argv.length !== 5 : process.argv.length !== 3) throw Error("Usage: stage-release-runner-binaries.mjs MANIFEST.json | assemble-daemons ARTIFACT_DIRECTORY SOURCE_SHA");
if (daemonOnly ? ![5, 6].includes(process.argv.length) : process.argv.length !== 3) throw Error("Usage: stage-release-runner-binaries.mjs MANIFEST.json | assemble-daemons ARTIFACT_DIRECTORY SOURCE_SHA");
const manifest = daemonOnly
? releaseDaemonManifest(process.argv[3], process.argv[4], await metadataContract())
: JSON.parse(regularFile(resolve(process.argv[2]), 512 * 1024));
if (!/^[a-f0-9]{40}$/.test(manifest.sourceRevision) || !manifest.platforms || Object.keys(manifest.platforms).sort().join() !== [...required].sort().join()) throw Error("Release daemon manifest must bind one source and all four supported targets");
if (daemonOnly && process.argv[5]) manifest.remoteProviderPacks = releaseProviderPackManifest(process.argv[5], manifest.sourceRevision);
const verified = required.map(target => {
const artifact = manifest.platforms[target];
if (!artifact || !isAbsolute(artifact.path) || !/^sha256:[a-f0-9]{64}$/.test(artifact.sha256)) throw Error(`Invalid ${target} release artifact`);
@@ -91,18 +106,26 @@ async function main() {
if (sha256 !== artifact.sha256 || runnerBinaryTarget(bytes) !== target) throw Error(`Release daemon ${target} digest or architecture mismatch`);
return { target, source: artifact.path, sha256 };
});
if (!manifest.remoteProviderPack && !daemonOnly) throw Error("Release assembly requires the matching Linux remote provider pack");
let packBytes, packIdentity;
if (manifest.remoteProviderPack) {
const entry = manifest.remoteProviderPack;
if (!isAbsolute(entry.path) || !/^sha256:[a-f0-9]{64}$/.test(entry.sha256)) throw Error("Invalid remote provider-pack release artifact");
packBytes = regularFile(entry.path, 512 * 1024);
if (packBytes.length > 512 * 1024 || "sha256:" + createHash("sha256").update(packBytes).digest("hex") !== entry.sha256) throw Error("Remote provider-pack release manifest digest mismatch");
packIdentity = verifyReleaseProviderPack(JSON.parse(packBytes), manifest.sourceRevision);
} else if (daemonOnly) {
// Preserve a qualified same-source Linux pack already materialized by the
// normal producer. It never becomes a Mac provider-pack qualification.
packIdentity = existingReleaseProviderPack(root, manifest.sourceRevision);
if (!manifest.remoteProviderPack && !manifest.remoteProviderPacks && !daemonOnly) throw Error("Release assembly requires the matching Linux remote provider pack");
if (manifest.remoteProviderPacks && Object.keys(manifest.remoteProviderPacks).sort().join() !== [...RELEASE_PROVIDER_PACK_TARGETS].sort().join()) {
throw Error("Release provider packs require exactly both Linux targets");
}
const packs = {};
for (const target of RELEASE_PROVIDER_PACK_TARGETS) {
const entry = manifest.remoteProviderPacks?.[target] ?? (target === "linux-x64" ? manifest.remoteProviderPack : undefined);
if (entry) {
if (!isAbsolute(entry.path) || !/^sha256:[a-f0-9]{64}$/.test(entry.sha256)) throw Error("Invalid remote provider-pack release artifact");
const bytes = regularFile(entry.path, 512 * 1024);
if (digest(bytes) !== entry.sha256) throw Error("Remote provider-pack release manifest digest mismatch");
packs[target] = { bytes, identity: verifyReleaseProviderPack(JSON.parse(bytes), manifest.sourceRevision, target) };
if (target === "linux-x64" && manifest.remoteProviderPack && manifest.remoteProviderPacks &&
(manifest.remoteProviderPack.path !== entry.path || manifest.remoteProviderPack.sha256 !== entry.sha256)) {
throw Error("Conflicting legacy Linux x64 provider-pack identity");
}
} else if (daemonOnly) {
const identity = existingReleaseProviderPack(root, manifest.sourceRevision, target);
if (identity) packs[target] = { identity };
}
}
const platforms = {};
for (const artifact of verified) {
@@ -116,20 +139,22 @@ async function main() {
const sha256 = "sha256:" + createHash("sha256").update(readFileSync(destination)).digest("hex");
platforms[artifact.target] = { path: relative, sha256, sourceSha256: artifact.sha256 };
}
if (packBytes) {
const destination = join(root, "dist", "remote-provider-packs", "linux-x64", "provider-pack.json");
for (const [target, pack] of Object.entries(packs)) {
if (!pack.bytes) continue;
const destination = join(root, "dist", "remote-provider-packs", target, "provider-pack.json");
mkdirSync(dirname(destination), { recursive: true });
// Replace the inode so repeat assembly can preserve a read-only manifest.
const staged = `${destination}.staging-${process.pid}`;
try {
writeFileSync(staged, packBytes, { mode: 0o444, flag: "wx" });
writeFileSync(staged, pack.bytes, { mode: 0o444, flag: "wx" });
renameSync(staged, destination);
} finally {
rmSync(staged, { force: true });
}
}
writeFileSync(join(root, "dist", "bin", "release-manifest.json"), JSON.stringify({schema:"paperclip.runner.release-binaries.v1",sourceRevision:manifest.sourceRevision,platforms,
...(packIdentity ? { remoteProviderPack: { target: "linux-x64", digest: packIdentity.digest, sourceRevision: packIdentity.payload.runnerSourceRevision } } : {})},null,2)+"\n");
...(packs["linux-x64"] ? { remoteProviderPack: { target: "linux-x64", digest: packs["linux-x64"].identity.digest, sourceRevision: packs["linux-x64"].identity.payload.runnerSourceRevision } } : {}),
...(Object.keys(packs).length ? { remoteProviderPacks: Object.fromEntries(Object.entries(packs).map(([target, pack]) => [target, { target, digest: pack.identity.digest, sourceRevision: pack.identity.payload.runnerSourceRevision }])) } : {})},null,2)+"\n");
console.log("Staged verified release daemons for " + required.join(", "));
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();
+8 -1
View File
@@ -288,9 +288,16 @@ pnpm build
# receipts. Assemble after the build (which stages its host binary), before
# public packages are packed. The server vendors this same compiled runner.
if [ -n "${PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR:-}" ]; then
pack_args=()
if [ -n "${PAPERCLIP_RELEASE_PROVIDER_PACK_ARTIFACTS_DIR:-}" ]; then
pack_args=("$PAPERCLIP_RELEASE_PROVIDER_PACK_ARTIFACTS_DIR")
fi
node "$REPO_ROOT/packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs" \
assemble-daemons "$PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR" "$(git rev-parse HEAD)"
assemble-daemons "$PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR" "$(git rev-parse HEAD)" "${pack_args[@]}"
cp -R "$REPO_ROOT/packages/paperclip-runner/dist/bin/." "$REPO_ROOT/server/dist/vendor/paperclip-runner/bin/"
if [ -d "$REPO_ROOT/packages/paperclip-runner/dist/remote-provider-packs" ]; then
cp -R "$REPO_ROOT/packages/paperclip-runner/dist/remote-provider-packs" "$REPO_ROOT/server/dist/vendor/paperclip-runner/"
fi
elif [ "$dry_run" = false ]; then
release_fail "Public releases require the four hosted runner daemon artifacts; use the supported Release workflow."
fi
@@ -234,7 +234,7 @@ vi.mock("../../vendor/paperclip-runner/index.js", async (importOriginal) => {
>();
return {
...original,
bundledRemoteProviderPackManifestPath: () => state.bundledPackManifestPath ?? original.bundledRemoteProviderPackManifestPath(),
bundledRemoteProviderPackManifestPath: vi.fn((moduleUrl?: string, target?: "linux-x64" | "linux-arm64") => state.bundledPackManifestPath ?? original.bundledRemoteProviderPackManifestPath(moduleUrl, target)),
bundledRemoteRunnerBinary: () => state.bundledRunnerBinary ?? original.bundledRemoteRunnerBinary(),
createNativeSessionBackend: state.createBackend,
createRunnerdCodexTransport: state.createTransport,
@@ -1136,7 +1136,9 @@ describe("remote provider pack manifest", () => {
return JSON.stringify(value);
};
it.each(["opencode", "cursor"] as const)("uses the bundled Linux image authority for remote %s without controller overrides", async provider => {
it.each(["opencode", "cursor", "claude-x64", "claude-arm64"] as const)("uses the bundled Linux image authority for remote %s without controller overrides", async provider => {
const architecture = provider === "claude-arm64" ? "arm64" : "x64";
const claude = provider.startsWith("claude-");
const root = await mkdtemp(join(tmpdir(), "paperclip-bundled-image-"));
const digest = "sha256:" + "a".repeat(64);
const artifacts = Object.fromEntries(Object.entries({
@@ -1146,7 +1148,7 @@ describe("remote provider pack manifest", () => {
opencodeProxy: "dist/cli/opencode-app-server-proxy.cjs", acpxSidecar: "dist/cli/acpx-runtime-sidecar.cjs",
}).map(([key, path]) => [key, { path, sha256: digest }]));
const payload = {
target: { platform: "linux", architecture: "x64" }, runnerSourceRevision: "a".repeat(40),
target: { platform: "linux", architecture }, runnerSourceRevision: "a".repeat(40),
pins: { nodeMinimum: "24.11.0", codex: QUALIFIED_ACPX_PROFILES.codex.agentRuntimeVersion,
opencode: "1.18.34", acpx: "0.13.1", grok: QUALIFIED_ACPX_PROFILES.grok.agentRuntimeVersion,
claudeAcp: QUALIFIED_ACPX_PROFILES.claude.agentServerVersion, codexAcp: QUALIFIED_ACPX_PROFILES.codex.agentServerVersion },
@@ -1160,10 +1162,14 @@ describe("remote provider pack manifest", () => {
state.bundledPackManifestPath = join(root, "provider-pack.json");
state.bundledRunnerBinary = join(root, "linux-paperclip-runnerd");
await writeFile(state.bundledPackManifestPath, JSON.stringify(manifest));
await writeFile(state.bundledRunnerBinary, "verified Linux daemon fixture");
const daemon = Buffer.alloc(64); daemon.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]);
daemon.writeUInt16LE(architecture === "arm64" ? 183 : 62, 18);
await writeFile(state.bundledRunnerBinary, daemon);
if (claude) state.resolveRemoteRunnerBinary.mockResolvedValue(state.bundledRunnerBinary);
const remoteExecution = { ...execution, provider: provider === "opencode"
? { kind: "opencode", model: null } : { kind: "acpx", agent: "cursor", model: null },
session: { ...execution.session, driverKind: provider === "opencode" ? "opencode_server" : "acpx_runtime" },
? { kind: "opencode", model: null } : { kind: "acpx", agent: claude ? "claude" : "cursor", model: null },
workspace: { ...execution.workspace, cwd: root },
session: { ...execution.session, normalizedSessionId: `bundled-${provider}-${randomUUID()}`, driverKind: provider === "opencode" ? "opencode_server" : "acpx_runtime" },
} as unknown as NativeExecutionInputV1;
const syncIn = vi.fn(async () => undefined);
const execute = vi.fn(async (command: { command: string; args?: string[] }) => {
@@ -1182,6 +1188,11 @@ describe("remote provider pack manifest", () => {
controlPlaneRegistration: (authority: unknown) => Promise<unknown>;
};
expect(transport.runnerBinary).toBe(state.bundledRunnerBinary);
if (claude) {
expect(state.resolveRemoteRunnerBinary).toHaveBeenCalledWith(expect.objectContaining({ remoteCwd: "/workspace", configuredPath: undefined }));
const { bundledRemoteProviderPackManifestPath } = await import("../../vendor/paperclip-runner/index.js");
expect(bundledRemoteProviderPackManifestPath).toHaveBeenCalledWith(undefined, `linux-${architecture}`);
}
await expect(transport.controlPlaneRegistration({})).rejects.toThrow("reached-staged-Linux-daemon-verification");
expect(syncIn).toHaveBeenCalledWith([expect.objectContaining({ files: [expect.objectContaining({
sourcePath: state.bundledRunnerBinary, targetPath: "/workspace/.paperclip-runtime/paperclip-runner/bin/paperclip-runnerd",