Align staging runtime packaging and lifecycle with current master

Preserve active ACPX child snapshots through reusable lease close. Align the canonical provider lock and CLI pins with the current qualified profiles, retain local Git fallback, and preserve narrowed heartbeat values across diagnostics.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-11 14:32:15 -05:00
parent 398a82646e
commit d2ecec3359
9 changed files with 1046 additions and 839 deletions

View File

@ -296,7 +296,7 @@ COPY packages ./packages
COPY server/package.json ./server/package.json
COPY ui/package.json ./ui/package.json
COPY cli/package.json ./cli/package.json
ARG PAPERCLIP_RUNNER_LOCK_SHA256=84409576c7cbd2bec50b535c6df6acf3691bdec7c7697e6c50b2fb834b56f203
ARG PAPERCLIP_RUNNER_LOCK_SHA256=21aa3df50da53c338660b9f5f2611e9d46c896ad05c0e7a9976185ebce340f52
RUN printf '%s pnpm-lock.yaml\n' "${PAPERCLIP_RUNNER_LOCK_SHA256}" > /tmp/provider-lock.sha256 \
&& sha256sum -c /tmp/provider-lock.sha256 \
&& pnpm install --frozen-lockfile --filter '@paperclipai/paperclip-runner...'
@ -306,7 +306,7 @@ RUN test -n "${PAPERCLIP_BUILD_COMMIT}" \
&& PAPERCLIP_RUNNER_SOURCE_REVISION="${PAPERCLIP_BUILD_COMMIT}" \
node packages/paperclip-runner/scripts/assemble-provider-pack.mjs /provider-pack \
&& node packages/paperclip-runner/scripts/verify-pi-provider-launch.mjs /provider-pack \
&& PATH=/provider-pack/node_modules/.bin:$PATH sh -ec 'for cli in node acpx claude-agent-acp codex-acp pi-acp pi claude codex opencode; do test -x "/provider-pack/node_modules/.bin/$cli"; done; test "$(acpx --version)" = "0.13.1"; test "$(claude-agent-acp --version)" = "0.70.0"; test "$(codex-acp --version)" = "@agentclientprotocol/codex-acp 1.6.2"' \
&& PATH=/provider-pack/node_modules/.bin:$PATH sh -ec 'for cli in node acpx claude-agent-acp codex-acp pi-acp pi claude codex opencode; do test -x "/provider-pack/node_modules/.bin/$cli"; done; test "$(acpx --version)" = "0.13.1"; test "$(claude-agent-acp --version)" = "0.73.0"; test "$(codex-acp --version)" = "@agentclientprotocol/codex-acp 1.6.2"' \
&& node --input-type=module -e "import {verifyProviderPack} from './packages/paperclip-runner/scripts/provider-pack-integrity.mjs'; verifyProviderPack('/provider-pack', {revision: '${PAPERCLIP_BUILD_COMMIT}', lockSha256: '${PAPERCLIP_RUNNER_LOCK_SHA256}'});"
FROM production AS cloud

View File

@ -29,7 +29,7 @@ COPY cli/package.json ./cli/package.json
# The complete resolved lock (including transitive integrity hashes) is reviewed.
# Reject registry-time drift BEFORE installing packages or running lifecycle code.
# Refresh this digest together with source/provider dependency changes.
ARG PAPERCLIP_RUNNER_LOCK_SHA256=84409576c7cbd2bec50b535c6df6acf3691bdec7c7697e6c50b2fb834b56f203
ARG PAPERCLIP_RUNNER_LOCK_SHA256=21aa3df50da53c338660b9f5f2611e9d46c896ad05c0e7a9976185ebce340f52
RUN printf '%s pnpm-lock.yaml\n' "${PAPERCLIP_RUNNER_LOCK_SHA256}" > /tmp/provider-lock.sha256 \
&& sha256sum -c /tmp/provider-lock.sha256 \
&& pnpm install --frozen-lockfile --filter '@paperclipai/paperclip-runner...'
@ -39,7 +39,7 @@ RUN pnpm --filter @paperclipai/paperclip-runner build:typescript \
&& PAPERCLIP_RUNNER_SOURCE_REVISION="${PAPERCLIP_RUNNER_SOURCE_REVISION}" \
node packages/paperclip-runner/scripts/assemble-provider-pack.mjs /provider-pack \
&& node packages/paperclip-runner/scripts/verify-pi-provider-launch.mjs /provider-pack \
&& PATH=/provider-pack/node_modules/.bin:$PATH sh -ec 'for cli in node acpx claude-agent-acp codex-acp pi-acp pi claude codex opencode; do test -x "/provider-pack/node_modules/.bin/$cli"; done; test "$(acpx --version)" = "0.13.1"; test "$(claude-agent-acp --version)" = "0.70.0"; test "$(codex-acp --version)" = "@agentclientprotocol/codex-acp 1.6.2"' \
&& PATH=/provider-pack/node_modules/.bin:$PATH sh -ec 'for cli in node acpx claude-agent-acp codex-acp pi-acp pi claude codex opencode; do test -x "/provider-pack/node_modules/.bin/$cli"; done; test "$(acpx --version)" = "0.13.1"; test "$(claude-agent-acp --version)" = "0.73.0"; test "$(codex-acp --version)" = "@agentclientprotocol/codex-acp 1.6.2"' \
&& node --input-type=module -e "import {verifyProviderPack} from './packages/paperclip-runner/scripts/provider-pack-integrity.mjs'; verifyProviderPack('/provider-pack', {revision: '${PAPERCLIP_RUNNER_SOURCE_REVISION}', lockSha256: '${PAPERCLIP_RUNNER_LOCK_SHA256}'});"
# The trusted qualification entry exports the identical canonical provider stage.

File diff suppressed because it is too large Load Diff

View File

@ -177,7 +177,9 @@ describe("sandbox adapter execution targets", () => {
kind: "remote", transport: "sandbox", providerKey: "test", environmentId: "env-1",
leaseId: "lease-1", remoteCwd: root, timeoutMs: 30_000, runner: { execute },
};
return { runId: "launcher-retry", target, cwd: root, env: {} };
// Pin this local fixture PATH so these tests inject failures into writes,
// independently of the execution-target PATH discovery tests.
return { runId: "launcher-retry", target, cwd: root, env: { PATH: "/usr/bin:/bin" } };
}
it("hash-skips an accepted upload after its provider reply is lost", async () => {
@ -3600,7 +3602,7 @@ process.stdin.resume();setTimeout(()=>process.exit(2),20000);`);
"x-paperclip-github-capability": "current-github-capability",
"content-type": "application/json",
},
body: "{}",
body: Buffer.from("{}"),
});
expect(credentials.status).toBe(200);
expect(api.requests[1]).toMatchObject({
@ -3609,7 +3611,7 @@ process.stdin.resume();setTimeout(()=>process.exit(2),20000);`);
auth: "Bearer real-run-jwt",
runId: "run-http2",
headers: { "x-paperclip-github-capability": "current-github-capability" },
body: "{}",
body: Buffer.from("{}"),
});
} finally {
sessionRef.current?.close();

View File

@ -141,7 +141,10 @@ async function main() {
}
}
} else { diagnostic('capability_missing'); }
} catch (error) { diagnostic(error && error.credentialCategory === 'denied' ? 'capability_rejected' : 'broker_transport_unavailable'); }
} catch (error) {
if (program === 'gh') throw error;
diagnostic(error && error.credentialCategory === 'denied' ? 'capability_rejected' : 'broker_transport_unavailable');
}
}
// Only this invocation and its children inherit the captured credential.
// Its Git children use the real binary, so steering cannot split a gh operation.
@ -156,7 +159,12 @@ async function main() {
child.once('error', () => { process.stderr.write('Paperclip: GitHub command could not start.\n'); process.exitCode = 1; });
child.once('exit', (code, signal) => { process.exitCode = code === null ? 128 : code; });
}
main().catch(() => { process.stderr.write('Paperclip: GitHub launcher_setup_failed.\n'); process.exitCode = 1; });
main().catch((error) => {
const category = ['timeout', 'unavailable', 'denied', 'invalidresponse'].includes(error && error.credentialCategory)
? error.credentialCategory : 'unavailable';
process.stderr.write('Paperclip: GitHub credential context unavailable (' + category + '); retry this operation.\n');
process.exitCode = 1;
});
`;
}

View File

@ -242,7 +242,7 @@ try {
codex: "0.153.4",
opencode: "1.18.29",
acpx: "0.13.1",
claudeAcp: "0.70.0",
claudeAcp: "0.73.0",
codexAcp: "1.6.2",
pi: "0.84.2",
piAcp: "0.0.33",
@ -262,7 +262,7 @@ try {
claude:
"sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a",
codex:
"sha256:7a923b3829884d3cabcc9659d22cace3f86813e7bfffc90974b10140a45bc400",
"sha256:c4538599d1ab767db5dff50934f13bb5ba313a59d9c4a83e993fac4617ea63d3",
},
artifacts: {
nodeCommand: {

View File

@ -185,7 +185,7 @@ test("both canonical stages exercise pinned CLI versions and verify full tree af
assert(body);
const pi = body.indexOf("verify-pi-provider-launch.mjs /provider-pack"), versions = body.indexOf('test "$(acpx --version)" = "0.13.1"'), verification = body.indexOf("verifyProviderPack('/provider-pack'");
assert(pi >= 0 && versions > pi && verification > versions);
assert(body.includes('test "$(claude-agent-acp --version)" = "0.70.0"'));
assert(body.includes('test "$(claude-agent-acp --version)" = "0.73.0"'));
assert(body.includes('test "$(codex-acp --version)" = "@agentclientprotocol/codex-acp 1.6.2"'));
assert(!body.includes("chmod -R a+rX /provider-pack"), "Modes must be normalized before binding the inventory");
}

View File

@ -1325,6 +1325,14 @@ function commandLease(
privateSnapshot: AcpxPrivateSnapshot | null,
): VerifiedAcpxCommandLease {
let consumed = false;
let closed = false;
let activeChildren = 0;
// A spawned child still needs the verified private snapshot while its
// bootstrap loads. Closing the reusable lease prevents new launches, but
// must not unlink bytes already handed to an existing child.
const closeSnapshotIfIdle = async (): Promise<void> => {
if (activeChildren === 0 && (closed || !reusable)) await privateSnapshot?.close();
};
let directoriesReleased = false;
const releaseDirectories = async (): Promise<void> => {
if (directoriesReleased) return;
@ -1341,11 +1349,12 @@ function commandLease(
void releaseDirectories().catch(() => undefined);
};
const close = async (): Promise<void> => {
if (consumed) return;
if (closed) return;
closed = true;
consumed = true;
verifiedBytes.fill(0);
await releaseDirectories();
await privateSnapshot?.close();
await closeSnapshotIfIdle();
};
return {
spawn(
@ -1528,11 +1537,17 @@ function commandLease(
void privateSnapshot?.close();
throw error;
}
if (!reusable) {
releaseDirectoriesBestEffort();
child.once("exit", () => { void privateSnapshot?.close(); });
child.once("error", () => { void privateSnapshot?.close(); });
}
activeChildren++;
let childSettled = false;
const settleChild = (): void => {
if (childSettled) return;
childSettled = true;
activeChildren--;
void closeSnapshotIfIdle().catch(() => undefined);
};
child.once("exit", settleChild);
child.once("error", settleChild);
if (!reusable) releaseDirectoriesBestEffort();
const sourceInput = child.stdio[COMMAND_SOURCE_FD] as Writable | null;
if (sourceInput === null) {
consumed = true;

View File

@ -22013,8 +22013,9 @@ export function heartbeatService(
// recovery existed. Only an entirely unused replacement row may
// inherit its source checkpoint; any process/provider evidence on the
// replacement makes the ownership ambiguous and therefore ineligible.
const legacyRetrySourceRunId = run.retryOfRunId;
const legacyRetrySource =
!sandboxWorkFolders?.identityChanged && run.retryOfRunId && !isFailedChatRunRetry
!sandboxWorkFolders?.identityChanged && legacyRetrySourceRunId && !isFailedChatRunRetry
? await measureSandboxOperation("heartbeat.db.select.from.where.limit.then", { operationIndex: 121 }, async () => (db
.select({
id: heartbeatRuns.id,
@ -22029,7 +22030,7 @@ export function heartbeatService(
.from(heartbeatRuns)
.where(
and(
eq(heartbeatRuns.id, run.retryOfRunId),
eq(heartbeatRuns.id, legacyRetrySourceRunId),
eq(heartbeatRuns.companyId, agent.companyId),
eq(heartbeatRuns.agentId, agent.id),
),
@ -22913,8 +22914,9 @@ export function heartbeatService(
// A hard restart replays the heartbeat context, not a new user
// action. Do not repeat a completed create/replace/edit (which
// could reactivate or clear a goal that finished while detached).
const goalControlRequestId = sessionGoalControl?.requestId;
const completedGoalControl =
sessionGoalControl !== null &&
goalControlRequestId !== undefined &&
taskKey !== null &&
(await measureSandboxOperation("heartbeat.is_runner_goal_action_completed", { operationIndex: 152 }, async () => (isRunnerGoalActionCompleted(
db,
@ -22923,7 +22925,7 @@ export function heartbeatService(
agentId: agent.id,
issueId: taskKey,
},
sessionGoalControl.requestId,
goalControlRequestId,
))));
if (completedGoalControl) sessionGoalControl = null;
const nativeDispatchAtMs = Date.now();
@ -23922,6 +23924,7 @@ export function heartbeatService(
issueId,
runId: livenessRun.id,
})));
const resolvedText = resolved.text;
const comment = await measureSandboxOperation("heartbeat.issues_svc.add_comment", { operationIndex: 210 }, async () => (issuesSvc.addComment(
issueId,
resolvedText,
@ -24812,7 +24815,8 @@ export function heartbeatService(
latestRun &&
isHeartbeatRunTerminalStatus(latestRun.status)
) {
await measureSandboxOperation("heartbeat.cleanup_git_hub_operation_launchers.catch", { operationIndex: 284 }, async () => (cleanupGitHubOperationLaunchers(githubLauncherLocation).catch(
const completedLauncherLocation = githubLauncherLocation;
await measureSandboxOperation("heartbeat.cleanup_git_hub_operation_launchers.catch", { operationIndex: 284 }, async () => (cleanupGitHubOperationLaunchers(completedLauncherLocation).catch(
(err) => {
logger.warn(
{ err, runId: run.id },