From fc9e9b704f6b9f43246120bd6f63c7d48506fa73 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Sun, 23 Aug 2026 17:16:30 -0700 Subject: [PATCH] fix: stop teaching agents to curl literal {id} route templates (#12061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Adapters inject prompt text that teaches agents how to call the Paperclip API, including copy-pasteable curl examples > - Some of those URLs contained brace placeholders like `/api/issues/{id}/checkout` > - Agents paste such lines verbatim; the placeholder reaches the server as `/api/issues/%7Bid%7D` and 404s, and request logs show agents doing exactly that > - The acpx engine's API note already avoids this by using `$PAPERCLIP_TASK_ID`, and its test pins `/api/issues/{id}` out of the prompt > - This pull request applies the same standard to the gemini adapter, the shared prompt template, and the openclaw gateway workflow > - The benefit is that agents stop burning turns on placeholder 404s and doc examples stay safe to execute as written ## Linked Issues or Issue Description No public issue exists for this defect. The description below follows the bug report template. **What happened?** Server request logs show agents issuing `GET /api/issues/%7Bid%7D` — the literal, percent-encoded text `{id}` — which 404s. The source is adapter prompt text: the gemini adapter's API note embeds a curl example with `/api/issues/{id}/checkout` in the URL, the shared agent prompt template mentions `/api/issues/{issueId}` endpoints, and the harness checkout notice names `/api/issues/{id}/checkout`. Models copy these strings into real requests. **Expected behavior** URL paths in prompt text must carry environment variables or real ids, never brace placeholders, in every string an agent might execute verbatim. Where a placeholder is unavoidable, the prompt must state explicitly that the literal text must never be sent. **Steps to reproduce** 1. Give an agent the gemini adapter's API access note. 2. Watch it call `curl ... "$PAPERCLIP_API_URL/api/issues/{id}/checkout"` as written. 3. The server logs `POST /api/issues/%7Bid%7D/checkout 404`. ## What Changed - gemini-local's API note curl example now uses `$PAPERCLIP_TASK_ID` and tells the agent to substitute a real issue id when that variable is absent — the same convention as the acpx engine's API note. - The shared agent prompt template (`server-utils.ts`) uses `$PAPERCLIP_TASK_ID` in its interaction-creation and resume-endpoint mentions, and the harness checkout notice names `POST /api/issues/$PAPERCLIP_TASK_ID/checkout`. - openclaw-gateway's endpoint workflow keeps its `{issueId}` placeholders — they are defined by its "determine issueId" step — but now states explicitly that the literal text must never be sent in a URL. - `server-utils.test.ts` pins the new form and adds negative pins that keep `/api/issues/{id}` and `/api/issues/{issueId}` out of the shared prompt template, mirroring the existing acpx-engine negative pin. - The `confirmation:{issueId}:plan:{revisionId}` idempotency-key template is untouched: it is a value-construction pattern, not a URL. ## Verification - `npx vitest run packages/adapter-utils/src/server-utils.test.ts packages/adapters/gemini-local packages/adapters/openclaw-gateway` — 152 passed. The single failure (`pre-selects gemini-api-key auth in the managed HOME for sandbox execution`) is a pre-existing environment-specific failure on the development machine, unrelated to prompt text; CI is authoritative for it. - `pnpm --filter @paperclipai/adapter-utils --filter @paperclipai/adapter-gemini-local --filter @paperclipai/adapter-openclaw-gateway typecheck`. ## Risks - Low risk: prompt-text and test changes only; no runtime logic changes. - Agents that memorized the old example strings keep working — the routes are unchanged, only the placeholder text in prompts is. ## Model Used - Claude Fable 5 (Anthropic), model id `claude-fable-5`, extended thinking enabled, agentic tool use via Claude Code (CLI harness), 200k context window. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --- packages/adapter-utils/src/server-utils.test.ts | 7 ++++++- packages/adapter-utils/src/server-utils.ts | 6 +++--- packages/adapters/gemini-local/src/server/execute.ts | 3 ++- packages/adapters/openclaw-gateway/src/server/execute.ts | 1 + 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/adapter-utils/src/server-utils.test.ts b/packages/adapter-utils/src/server-utils.test.ts index 526fe36789..3b497239e8 100644 --- a/packages/adapter-utils/src/server-utils.test.ts +++ b/packages/adapter-utils/src/server-utils.test.ts @@ -880,7 +880,12 @@ describe("renderPaperclipWakePrompt", () => { expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain("Use child issues"); expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain("instead of polling agents, sessions, or processes"); expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain("Create child issues directly when you know what needs to be done"); - expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain("POST /api/issues/{issueId}/interactions"); + expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain("POST /api/issues/$PAPERCLIP_TASK_ID/interactions"); + // URL paths in prompt text carry real ids or env vars, never brace + // placeholders: agents paste these lines verbatim, and a literal {issueId} + // reaches the server as /api/issues/%7BissueId%7D and 404s. + expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).not.toContain("/api/issues/{issueId}"); + expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).not.toContain("/api/issues/{id}"); expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain("kind suggest_tasks, ask_user_questions, or request_confirmation"); expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain( "Use continuationPolicy wake_assignee when you need to resume after a response (it wakes on acceptance and rejection alike; only expiry does not wake); use wake_assignee_on_accept when you want to resume only after acceptance", diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index c9f291387e..a013ada663 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -176,9 +176,9 @@ export const DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE = [ "- If woken by a human comment on a dependency-blocked issue, respond or triage the comment without treating the blocked deliverable work as unblocked.", "- Create child issues directly when you know what needs to be done; use issue-thread interactions when the board/user must choose suggested tasks, answer structured questions, or confirm a proposal.", "- Use `PAPERCLIP_SCRATCH_DIR` / `PAPERCLIP_RUN_SCRATCH_DIR` for temporary scratch files instead of ad hoc `/tmp` paths; Paperclip removes that run-owned directory after the run ends.", - "- To ask for that input, create an interaction on the current issue with POST /api/issues/{issueId}/interactions using kind suggest_tasks, ask_user_questions, or request_confirmation. Use continuationPolicy wake_assignee when you need to resume after a response (it wakes on acceptance and rejection alike; only expiry does not wake); use wake_assignee_on_accept when you want to resume only after acceptance.", + "- To ask for that input, create an interaction on the current issue with POST /api/issues/$PAPERCLIP_TASK_ID/interactions using kind suggest_tasks, ask_user_questions, or request_confirmation. Use continuationPolicy wake_assignee when you need to resume after a response (it wakes on acceptance and rejection alike; only expiry does not wake); use wake_assignee_on_accept when you want to resume only after acceptance.", "- Never create probe or throwaway issue-thread interactions to discover the interactions API shape or your permissions; schema discovery goes through the OpenAPI spec and explicit validation errors, not placeholder cards. Every ask_user_questions, suggest_tasks, or request_confirmation you post must carry a real, answerable prompt; withdraw one you no longer need instead of leaving it pending.", - "- When you intentionally restart follow-up work on a completed assigned issue, include structured `resume: true` with the POST /api/issues/{issueId}/comments or PATCH /api/issues/{issueId} comment payload. Generic agent comments on closed issues are inert by default.", + "- When you intentionally restart follow-up work on a completed assigned issue, include structured `resume: true` with the POST /api/issues/$PAPERCLIP_TASK_ID/comments or PATCH /api/issues/$PAPERCLIP_TASK_ID comment payload (substitute that issue's real id when it is not the current task). Generic agent comments on closed issues are inert by default.", "- For plan approval, update the plan document first, then create request_confirmation targeting the latest plan revision with idempotencyKey confirmation:{issueId}:plan:{revisionId}. Wait for acceptance before creating implementation subtasks, and create a fresh confirmation after superseding board/user comments if approval is still needed.", "- If blocked, mark the issue blocked and name the unblock owner and action.", "- Respect budget, pause/cancel, approval gates, and company boundaries.", @@ -2010,7 +2010,7 @@ export function renderPaperclipWakePrompt( lines.push( "", "The harness already checked out this issue for the current run.", - "Do not call `/api/issues/{id}/checkout` again unless you intentionally switch to a different task.", + "Do not call `POST /api/issues/$PAPERCLIP_TASK_ID/checkout` again unless you intentionally switch to a different task.", "", ); } diff --git a/packages/adapters/gemini-local/src/server/execute.ts b/packages/adapters/gemini-local/src/server/execute.ts index bfdcd5a07c..0c6604a967 100644 --- a/packages/adapters/gemini-local/src/server/execute.ts +++ b/packages/adapters/gemini-local/src/server/execute.ts @@ -124,7 +124,8 @@ function renderApiAccessNote(env: Record): string { "GET example:", ` run_shell_command({ command: "curl -s -H \\"Authorization: Bearer $PAPERCLIP_API_KEY\\" \\"$PAPERCLIP_API_URL/api/agents/me\\"" })`, "POST/PATCH example:", - ` run_shell_command({ command: "curl -s -X POST -H \\"Authorization: Bearer $PAPERCLIP_API_KEY\\" -H 'Content-Type: application/json' -H \\"X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID\\" -d '{...}' \\"$PAPERCLIP_API_URL/api/issues/{id}/checkout\\"" })`, + ` run_shell_command({ command: "curl -s -X POST -H \\"Authorization: Bearer $PAPERCLIP_API_KEY\\" -H 'Content-Type: application/json' -H \\"X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID\\" -d '{...}' \\"$PAPERCLIP_API_URL/api/issues/$PAPERCLIP_TASK_ID/checkout\\"" })`, + "When PAPERCLIP_TASK_ID is not set, substitute a real issue id from the current context; never send a placeholder like {id} in the URL.", "", "", ].join("\n"); diff --git a/packages/adapters/openclaw-gateway/src/server/execute.ts b/packages/adapters/openclaw-gateway/src/server/execute.ts index 7771254de4..fb193c2261 100644 --- a/packages/adapters/openclaw-gateway/src/server/execute.ts +++ b/packages/adapters/openclaw-gateway/src/server/execute.ts @@ -423,6 +423,7 @@ function buildWakeText( "Workflow:", "1) GET /api/agents/me", `2) Determine issueId: PAPERCLIP_TASK_ID if present, otherwise issue_id (${issueIdHint}).`, + ' Replace {issueId} in every endpoint below with that determined id. Never send the literal text "{issueId}" in a URL.', "3) If issueId exists:", " - POST /api/issues/{issueId}/checkout with {\"agentId\":\"$PAPERCLIP_AGENT_ID\",\"expectedStatuses\":[\"todo\",\"backlog\",\"blocked\",\"in_review\"]}", " - GET /api/issues/{issueId}",