fix(openclaw-gateway): use per-agent claimedApiKeyPath in wake text (#4668)

## 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/<agent>.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) <pieter@openclaw.local>
Co-authored-by: Andrew Aymeloglu <aaymeloglu@gmail.com>
This commit is contained in:
Joonyoung Park 2026-08-05 14:53:43 +09:00 committed by GitHub
parent 8142e54150
commit c54936e2e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 38 additions and 3 deletions

View File

@ -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);
});
});

View File

@ -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<string, string>,
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<AdapterExec
structuredWakeJson
? joinWakePayloadSections(structuredWakePrompt, structuredWakeJson)
: structuredWakePrompt,
resolveClaimedApiKeyPath(ctx.config.claimedApiKeyPath),
);
const sessionKeyStrategy = normalizeSessionKeyStrategy(ctx.config.sessionKeyStrategy);

View File

@ -0,0 +1,7 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
},
});

View File

@ -21,6 +21,7 @@ const nonServerProjects = [
"@paperclipai/adapter-utils",
"@paperclipai/adapter-claude-local",
"@paperclipai/adapter-codex-local",
"@paperclipai/adapter-openclaw-gateway",
"@paperclipai/adapter-opencode-local",
"@paperclipai/plugin-sdk",
"@paperclipai/create-paperclip-plugin",

View File

@ -13,6 +13,7 @@ export default defineConfig({
"packages/adapters/cursor-local",
"packages/adapters/gemini-local",
"packages/adapters/grok-local",
"packages/adapters/openclaw-gateway",
"packages/adapters/opencode-local",
"packages/adapters/pi-local",
"packages/plugins/sdk",