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:
DottaandPaperclip committed 2026-09-22 16:26:06 -05:00
1 parent 0019a057c3
commit 6adb00bcfe
4 files changed
+58 -2

No files matched your search

+13 -1
View File
@@ -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"),