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:
parent
398a82646e
commit
d2ecec3359
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
Loading…
Reference in New Issue