From c54936e2e958b1574c88adfd7dbf1d6a7dc440ad Mon Sep 17 00:00:00 2001 From: Joonyoung Park Date: Wed, 5 Aug 2026 14:53:43 +0900 Subject: [PATCH] fix(openclaw-gateway): use per-agent claimedApiKeyPath in wake text (#4668) 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 > - The `openclaw-gateway` adapter wakes a remote agent over a WebSocket gateway. It sends a wake prompt. That prompt tells the agent which environment variables to set and which file holds its Paperclip API key. > - Each agent stores its claimed key in its own JSON file. The adapter already exposes a `claimedApiKeyPath` config field for this. The field is documented in `src/index.ts`. It also has an input in the agent settings UI. > - `buildWakeText` ignored that field. It hardcoded the shared default path into the wake prompt text. > - Every agent therefore read the same key file at wake time. Agents authenticated as the wrong identity. The first API call failed. > - This pull request passes `ctx.config.claimedApiKeyPath` into `buildWakeText`. It uses the existing `resolveClaimedApiKeyPath` helper. That helper falls back to the documented default. > - The benefit is that each agent reads its own claimed-key file. Each agent authenticates as itself. ## Linked Issues or Issue Description Fixes #10071 Fixes #4976 Fixes #3098 Fixes #8076 These four open issues report the same defect. Earlier duplicates are already closed: Refs #2561, Refs #2592, Refs #930. Related pull requests that address the same root problem (duplicate search): - #3396 — same core change, no tests - #3370 — heavier approach, injects `PAPERCLIP_CLAIMED_API_KEY_PATH` into the wake env and adds server onboarding defaults - #5970 — renames the config field to `paperclipApiKeyPath` - #8072 — same core change, bundled with an unrelated protocol-version change - #784 — adds shell quoting and preflight instructions - #3296 — bundled with an unrelated Claude hello-probe fix ## What Changed - `packages/adapters/openclaw-gateway/src/server/execute.ts` - `buildWakeText` now accepts `claimedApiKeyPath` as a parameter. It no longer hardcodes the path. - The `execute` call site passes `resolveClaimedApiKeyPath(ctx.config.claimedApiKeyPath)`. That helper returns the documented default `~/.openclaw/workspace/paperclip-claimed-api-key.json` when the agent sets no override. - `resolveClaimedApiKeyPath` is now exported so tests can call it. - `packages/adapters/openclaw-gateway/src/server/execute.test.ts` — adds `resolveClaimedApiKeyPath` cases: a configured value, an empty string, a whitespace-only string, `undefined`, `null`, and non-string input. - `packages/adapters/openclaw-gateway/vitest.config.ts` (new) — package-level vitest config. It matches the config used by sibling adapters such as `opencode-local`. - `vitest.config.ts` (root) — adds the adapter to the workspace project list. - `scripts/run-vitest-stable.mjs` — adds `@paperclipai/adapter-openclaw-gateway` to `nonServerProjects`. **Maintainer-added during rebase.** The CI test lanes do not run a bare `vitest`. They call `run-vitest-stable.mjs`, which invokes vitest with an explicit `--project` allowlist. Without this entry the CI lanes skip this package, and the root project-list entry alone has no effect on CI. ## Verification Run the package suite directly: ``` pnpm install --frozen-lockfile pnpm exec vitest run --project @paperclipai/adapter-openclaw-gateway ``` The suite covers `resolveSessionKey`, `buildAgentParams`, and the new `resolveClaimedApiKeyPath` cases. The first two already existed in this file but never executed in CI before this change. Typecheck the package: ``` pnpm --filter @paperclipai/adapter-openclaw-gateway typecheck ``` Behavioural check, which no automated test covers: 1. Set `claimedApiKeyPath` to a per-agent value such as `~/.openclaw/workspace/paperclip-keys/.json` in the agent's gateway adapter settings. 2. Trigger a wake for that agent. 3. Confirm the rendered wake text names that file. It must not name the shared default. Maintainer note: this branch was rebased onto current `master` by a maintainer. The original branch was two months stale. Only two conflicts occurred, both additive: the import line and the tail of `execute.test.ts`, and the project list in the root `vitest.config.ts`. The `execute.ts` change applied without conflict. CI and Greptile re-run against the rebased head. ## Risks - Low for existing deployments. `resolveClaimedApiKeyPath` preserves the default path exactly. Any agent that never set `claimedApiKeyPath` receives the same wake text as before. - The behaviour changes only for agents that already set a per-agent path. Those agents previously received the wrong instruction. They now receive the correct one. - No database, schema, or API surface changes. - CI now runs this package's test file for the first time. That file includes the pre-existing `resolveSessionKey` and `buildAgentParams` tests, which were never executed before. - Five other adapters (`cursor-cloud`, `cursor-local`, `gemini-local`, `grok-local`, `pi-local`) sit in the root project list but remain absent from the CI allowlist. This pull request does not change them. That gap is tracked separately. ## Model Used - Contributor's change: Anthropic Claude, model ID `claude-opus-4-7`, approximately 200K context, extended thinking. Used for triage, patch authoring, and the original description. - Rebase, the `run-vitest-stable.mjs` entry, and this description: Anthropic Claude, model ID `claude-opus-5`, tool use enabled. Run by a Paperclip maintainer. ## 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) - [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details — the branch name carries an internal ticket id. A fork branch cannot be renamed without opening a new pull request, so this is left as-is. The internal reference has been removed from the description. - [ ] I have run tests locally and they pass — the contributor verified the pre-rebase branch. The rebased head is verified by CI on this pull request. - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes — `claimedApiKeyPath` is already documented in `src/index.ts` and exposed in the agent settings UI, so no documentation change is needed - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green — pending the post-rebase run - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups — pending re-review of the rebased head - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Pieter (CTO) Co-authored-by: Andrew Aymeloglu --- .../src/server/execute.test.ts | 27 ++++++++++++++++++- .../openclaw-gateway/src/server/execute.ts | 5 ++-- .../openclaw-gateway/vitest.config.ts | 7 +++++ scripts/run-vitest-stable.mjs | 1 + vitest.config.ts | 1 + 5 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 packages/adapters/openclaw-gateway/vitest.config.ts diff --git a/packages/adapters/openclaw-gateway/src/server/execute.test.ts b/packages/adapters/openclaw-gateway/src/server/execute.test.ts index 316b81c7d5..3689e41245 100644 --- a/packages/adapters/openclaw-gateway/src/server/execute.test.ts +++ b/packages/adapters/openclaw-gateway/src/server/execute.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { buildAgentParams, resolveSessionKey } from "./execute.js"; +import { buildAgentParams, resolveClaimedApiKeyPath, resolveSessionKey } from "./execute.js"; describe("resolveSessionKey", () => { it("prefixes run-scoped session keys with the configured agent", () => { @@ -98,3 +98,28 @@ describe("buildAgentParams", () => { }); }); }); + +describe("resolveClaimedApiKeyPath", () => { + const DEFAULT_PATH = "~/.openclaw/workspace/paperclip-claimed-api-key.json"; + + it("returns the configured per-agent path when set", () => { + expect( + resolveClaimedApiKeyPath("~/.openclaw/workspace/paperclip-keys/happy.json"), + ).toBe("~/.openclaw/workspace/paperclip-keys/happy.json"); + }); + + it("falls back to the shared default when value is empty", () => { + expect(resolveClaimedApiKeyPath("")).toBe(DEFAULT_PATH); + expect(resolveClaimedApiKeyPath(" ")).toBe(DEFAULT_PATH); + }); + + it("falls back to the shared default when value is missing", () => { + expect(resolveClaimedApiKeyPath(undefined)).toBe(DEFAULT_PATH); + expect(resolveClaimedApiKeyPath(null)).toBe(DEFAULT_PATH); + }); + + it("falls back to the shared default when value is not a string", () => { + expect(resolveClaimedApiKeyPath(42)).toBe(DEFAULT_PATH); + expect(resolveClaimedApiKeyPath({})).toBe(DEFAULT_PATH); + }); +}); diff --git a/packages/adapters/openclaw-gateway/src/server/execute.ts b/packages/adapters/openclaw-gateway/src/server/execute.ts index ab84aa7de0..7771254de4 100644 --- a/packages/adapters/openclaw-gateway/src/server/execute.ts +++ b/packages/adapters/openclaw-gateway/src/server/execute.ts @@ -337,7 +337,7 @@ function resolvePaperclipApiUrlOverride(value: unknown): string | null { const DEFAULT_CLAIMED_API_KEY_PATH = "~/.openclaw/workspace/paperclip-claimed-api-key.json"; -function resolveClaimedApiKeyPath(value: unknown): string { +export function resolveClaimedApiKeyPath(value: unknown): string { return nonEmpty(value) ?? DEFAULT_CLAIMED_API_KEY_PATH; } @@ -369,8 +369,8 @@ function buildWakeText( payload: WakePayload, paperclipEnv: Record, structuredWakePrompt: string, + claimedApiKeyPath: string, ): string { - const claimedApiKeyPath = "~/.openclaw/workspace/paperclip-claimed-api-key.json"; const orderedKeys = [ "PAPERCLIP_RUN_ID", "PAPERCLIP_AGENT_ID", @@ -1096,6 +1096,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise