mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 13:20:54 +01:00
fix(runner): normalize unavailable ACPX permission outcomes
Resolve remote build targets once before checkout and keep image jobs free of provider credentials. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
0019a057c3
commit
6adb00bcfe
4 files changed
+58
-2
No files matched your search
@@ -3,6 +3,10 @@ name: Docker Runner check
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target_branch:
|
||||
description: "Branch in this repository to build; resolved to one immutable commit before checkout"
|
||||
type: string
|
||||
required: false
|
||||
publish_eval_image:
|
||||
description: "Publish an immutable Daytona qualification image on the EC2 fleet (no provider credentials)"
|
||||
type: boolean
|
||||
@@ -58,8 +62,11 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
target_sha: ${{ steps.authorize.outputs.target_sha }}
|
||||
steps:
|
||||
- name: Authorize an explicit maintainer image build
|
||||
id: authorize
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
@@ -67,6 +74,7 @@ jobs:
|
||||
ACTOR_ID: ${{ github.actor_id }}
|
||||
TRIGGERING_ACTOR: ${{ github.triggering_actor }}
|
||||
ALLOWED_IDS: ${{ vars.AWS_CI_TRUSTED_USER_IDS }}
|
||||
TARGET_BRANCH: ${{ inputs.target_branch || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test "$REPOSITORY" = paperclipai/paperclip
|
||||
@@ -76,6 +84,9 @@ jobs:
|
||||
for id in "$ACTOR_ID" "$triggering_id"; do
|
||||
jq -e --argjson id "$id" 'index($id) != null' <<< "$ALLOWED_IDS" >/dev/null
|
||||
done
|
||||
target_sha="$(gh api "repos/$REPOSITORY/git/ref/heads/$TARGET_BRANCH" --jq '.object.sha')"
|
||||
[[ "$target_sha" =~ ^[0-9a-f]{40}$ ]]
|
||||
echo "target_sha=$target_sha" >> "$GITHUB_OUTPUT"
|
||||
|
||||
manual_image:
|
||||
name: Build and verify on EC2
|
||||
@@ -105,6 +116,7 @@ jobs:
|
||||
done
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ needs.authorize_manual.outputs.target_sha }}
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
||||
with:
|
||||
@@ -129,7 +141,7 @@ jobs:
|
||||
- name: Build checksum-verified Grok provider image
|
||||
if: inputs.publish_eval_image
|
||||
env:
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
SOURCE_SHA: ${{ needs.authorize_manual.outputs.target_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
image="ghcr.io/paperclipai/paperclip-daytona-runner:qualification-${SOURCE_SHA}-${GITHUB_RUN_ID}"
|
||||
|
||||
@@ -70,6 +70,27 @@ describe("Codex ACPX runtime adapter", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["PERMISSION_PROMPT_UNAVAILABLE", "AGENT_DISCONNECTED"])("preserves the meaning of ACPX terminal failure %s", async (code) => {
|
||||
const runtime = fakeRuntime();
|
||||
const result = { status: "failed" as const, error: { code, message: "Provider failed" } };
|
||||
vi.mocked(runtime.startTurn).mockReturnValue({
|
||||
requestId: "permission-fallback", promptStarted: Promise.resolve(),
|
||||
events: { async *[Symbol.asyncIterator]() {} }, result: Promise.resolve(result),
|
||||
cancel: vi.fn(), closeStream: vi.fn(),
|
||||
});
|
||||
const port = await openCodexAcpxRuntime(openOptions(fakeCommand()), {
|
||||
createRegistry: () => registry(), createStore: () => store(), createRuntime: () => runtime,
|
||||
});
|
||||
try {
|
||||
const turn = port.startTurn({ text: "Attempt a write", requestId: "permission-fallback" });
|
||||
if (code === "PERMISSION_PROMPT_UNAVAILABLE") {
|
||||
await expect(turn.result).rejects.toMatchObject({ code: "approval_required" });
|
||||
} else {
|
||||
await expect(turn.result).resolves.toEqual(result);
|
||||
}
|
||||
} finally { await port.close({ reason: "terminal permission outcome verified" }); }
|
||||
});
|
||||
|
||||
it("rejects a pre-aborted admission before constructing or spawning ACPX", async () => {
|
||||
const cancellation = new Error("runtime admission cancelled");
|
||||
const controller = new AbortController();
|
||||
|
||||
@@ -1230,7 +1230,15 @@ function runtimePort(
|
||||
}
|
||||
const guarded = turnWithVerifiedLifetimeOwnership(turn, finishOwnershipAdmission);
|
||||
const result = guarded.result.then(
|
||||
(value) => { approval.signal.throwIfAborted(); return value; },
|
||||
(value) => {
|
||||
approval.signal.throwIfAborted();
|
||||
// ACPX may resolve its own permission policy before invoking the
|
||||
// host callback. Keep that typed denial on the same runner outcome.
|
||||
if (value.status === "failed" && value.error?.code === "PERMISSION_PROMPT_UNAVAILABLE") {
|
||||
throw new AcpxApprovalRequiredError();
|
||||
}
|
||||
return value;
|
||||
},
|
||||
(error: unknown) => { approval.signal.throwIfAborted(); throw error; },
|
||||
).finally(() => {
|
||||
if (permissionBoundary.active === approval) permissionBoundary.active = null;
|
||||
|
||||
@@ -15,6 +15,21 @@ const everydayOracleImage =
|
||||
"python@sha256:9d2e5553305c7c7b0097999bb17187c69b921ccd6bc9d40e4bb5ebe652c00285";
|
||||
|
||||
describe("public repository paid workflow security", () => {
|
||||
it("keeps the manual EC2 image build credential-free and pins the authorized target", async () => {
|
||||
const workflow = await readFile(path.join(repositoryRoot, ".github/workflows/docker-runner-check.yml"), "utf8");
|
||||
const manual = workflow.slice(workflow.indexOf(" authorize_manual:"));
|
||||
expect(manual.match(/AWS_CI_TRUSTED_USER_IDS/gu)).toHaveLength(2);
|
||||
expect(manual.match(/test "\$REPOSITORY_ID" = 1170821064/gu)).toHaveLength(2);
|
||||
expect(manual).toContain('runs-on: runs-on/fleet=paperclip-public-pr-x64/env=public-ci');
|
||||
expect(manual).toContain('repos/$REPOSITORY/git/ref/heads/$TARGET_BRANCH');
|
||||
expect(manual).toContain('ref: ${{ needs.authorize_manual.outputs.target_sha }}');
|
||||
expect(manual).toContain('SOURCE_SHA: ${{ needs.authorize_manual.outputs.target_sha }}');
|
||||
expect([...manual.matchAll(/secrets\.([A-Z_]+)/gu)].map(match => match[1])).toEqual(["GITHUB_TOKEN"]);
|
||||
expect(manual).toContain('pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile');
|
||||
expect(manual).toContain('PAPERCLIP_RUNNER_LOCK_SHA256=$lock_sha');
|
||||
expect(manual).toContain('docker logout ghcr.io');
|
||||
});
|
||||
|
||||
it("uses the reviewed master branch for the first-party trusted PR workflow", async () => {
|
||||
const ordinaryPrWorkflow = await readFile(
|
||||
path.join(repositoryRoot, ".github/workflows/pr.yml"),
|
||||
|
||||
Reference in new issue
Block a user