fix(cancel-poll): use ctx.authToken instead of process.env for cancel polling

The cancel poll was sending empty Authorization headers because
PAPERCLIP_API_KEY is not set on the Paperclip server pod. Use the
per-run authToken from ctx instead, which is the JWT issued by Paperclip
for this execution. PAPERCLIP_DEV_API_KEY still overrides for dev instances.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 07:11:47 -04:00
parent 5e67a4dd3b
commit 985d55e125
3 changed files with 44 additions and 14 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "paperclip-adapter-opencode-k8s",
"version": "0.1.34",
"version": "0.1.35",
"description": "Paperclip adapter plugin that runs OpenCode agents as Kubernetes Jobs",
"license": "MIT",
"type": "module",
+40 -10
View File
@@ -56,12 +56,13 @@ const HAPPY_JSONL = [
JSON.stringify({ type: "step_finish", part: { tokens: { input: 100, output: 50, cache: { read: 20 } }, cost: 0.002 } }),
].join("\n");
function makeCtx(configOverrides: Record<string, unknown> = {}, contextOverrides: Record<string, unknown> = {}): AdapterExecutionContext {
function makeCtx(configOverrides: Record<string, unknown> = {}, contextOverrides: Record<string, unknown> = {}, authToken = "test-auth-token"): AdapterExecutionContext {
return {
runId: "run-test-123",
agent: { id: "agent-id-test", name: "Test Agent", companyId: "co-1", adapterType: null, adapterConfig: null },
runtime: { sessionId: null, sessionParams: {}, sessionDisplayId: null, taskKey: null },
config: configOverrides,
authToken,
context: {
taskId: null,
issueId: null,
@@ -879,15 +880,10 @@ describe("execute — log dedup (waitForPod status dedup)", () => {
describe("execute — external cancel polling", () => {
const KEEPALIVE_MS = 15_000;
beforeEach(() => {
process.env.PAPERCLIP_DEV_API_KEY = "test-key";
});
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
delete process.env.PAPERCLIP_API_URL;
delete process.env.PAPERCLIP_API_KEY;
delete process.env.PAPERCLIP_DEV_API_KEY;
});
@@ -895,7 +891,6 @@ describe("execute — external cancel polling", () => {
vi.useFakeTimers();
process.env.PAPERCLIP_API_URL = "http://test-api";
process.env.PAPERCLIP_API_KEY = "test-key";
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
@@ -918,7 +913,7 @@ describe("execute — external cancel polling", () => {
});
vi.mocked(getBatchApi).mockReturnValue(batchApi as unknown as ReturnType<typeof getBatchApi>);
const ctx = makeCtx({}, { issueId: "issue-test-456" });
const ctx = makeCtx({}, { issueId: "issue-test-456" }, "run-jwt-token");
const executePromise = execute(ctx);
// Advance in 1-second steps. vi.advanceTimersByTimeAsync fires fake timers
@@ -939,7 +934,7 @@ describe("execute — external cancel polling", () => {
);
expect(fetchMock).toHaveBeenCalledWith(
"http://test-api/api/issues/issue-test-456",
expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer test-key" }) }),
expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer run-jwt-token" }) }),
);
});
@@ -958,7 +953,6 @@ describe("execute — external cancel polling", () => {
vi.useFakeTimers();
process.env.PAPERCLIP_API_URL = "http://test-api";
process.env.PAPERCLIP_API_KEY = "test-key";
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
ok: true,
@@ -976,6 +970,42 @@ describe("execute — external cancel polling", () => {
expect(result.errorCode).toBeUndefined();
expect(result.exitCode).toBe(0);
});
it("uses PAPERCLIP_DEV_API_KEY over ctx.authToken when set", async () => {
vi.useFakeTimers();
process.env.PAPERCLIP_API_URL = "http://test-api";
process.env.PAPERCLIP_DEV_API_KEY = "dev-override-key";
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ status: "cancelled" }),
});
vi.stubGlobal("fetch", fetchMock);
let jobDeleted = false;
const batchApi = makeBatchApi();
batchApi.deleteNamespacedJob.mockImplementation(() => { jobDeleted = true; return Promise.resolve({}); });
batchApi.readNamespacedJob.mockImplementation(() => {
if (jobDeleted) return Promise.reject(Object.assign(new Error("not found"), { statusCode: 404 }));
return Promise.resolve({ status: { conditions: [] } });
});
vi.mocked(getBatchApi).mockReturnValue(batchApi as unknown as ReturnType<typeof getBatchApi>);
const ctx = makeCtx({}, { issueId: "issue-test-456" }, "ctx-auth-token");
const executePromise = execute(ctx);
for (let i = 0; i < 20; i++) {
await vi.advanceTimersByTimeAsync(1_000);
}
await executePromise;
expect(fetchMock).toHaveBeenCalledWith(
"http://test-api/api/issues/issue-test-456",
expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer dev-override-key" }) }),
);
});
});
describe("execute — large-prompt Secret path", () => {
+3 -3
View File
@@ -569,9 +569,9 @@ async function streamAndAwaitJob(
await new Promise<void>((resolve) => setTimeout(resolve, KEEPALIVE_INTERVAL_MS));
if (logStopSignal.stopped || cancelSignal.cancelled) break;
try {
// Prefer PAPERCLIP_DEV_API_KEY if set (allows dev instance key to be
// distinct from the main-instance run JWT in PAPERCLIP_API_KEY).
const apiKey = process.env.PAPERCLIP_DEV_API_KEY ?? process.env.PAPERCLIP_API_KEY ?? "";
// Prefer PAPERCLIP_DEV_API_KEY if set (dev override), otherwise use
// the per-run authToken issued by Paperclip for this execution.
const apiKey = process.env.PAPERCLIP_DEV_API_KEY ?? ctx.authToken ?? "";
const resp = await fetch(`${apiUrl}/api/issues/${issueId}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});