From 905da8bd6b7831cd9b9040319190e48d5076d29b Mon Sep 17 00:00:00 2001 From: kirkluokun Date: Tue, 21 Apr 2026 03:15:05 +0800 Subject: [PATCH] fix(lobster): forward approvalId alongside resumeToken in tool envelope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @clawdbot/lobster/core returns both resumeToken and approvalId when a workflow step needs approval, but the lobster plugin was dropping approvalId in three places: normalizeEnvelope, the tool schema, and the embedded-runner resume branch. Agents forced to round-trip the ~155-byte base64url resumeToken across tool calls are one stray truncation away from "Invalid token". The 8-hex approvalId is a disk-indexed alias (~/.lobster/state/approval_* .json) — stable and escape-safe. Changes are additive: token-based resume keeps working unchanged, callers just gain an approvalId path. --- extensions/lobster/src/lobster-core.d.ts | 1 + extensions/lobster/src/lobster-runner.test.ts | 78 ++++++++++++++++++- extensions/lobster/src/lobster-runner.ts | 14 +++- extensions/lobster/src/lobster-tool.ts | 2 + 4 files changed, 91 insertions(+), 4 deletions(-) diff --git a/extensions/lobster/src/lobster-core.d.ts b/extensions/lobster/src/lobster-core.d.ts index c7a869dba42..5a16f0d4da9 100644 --- a/extensions/lobster/src/lobster-core.d.ts +++ b/extensions/lobster/src/lobster-core.d.ts @@ -4,6 +4,7 @@ declare module "@clawdbot/lobster/core" { prompt: string; items: unknown[]; resumeToken?: string; + approvalId?: string; } | null; type LobsterToolContext = { diff --git a/extensions/lobster/src/lobster-runner.test.ts b/extensions/lobster/src/lobster-runner.test.ts index 4176de12c14..3d48b45bd2a 100644 --- a/extensions/lobster/src/lobster-runner.test.ts +++ b/extensions/lobster/src/lobster-runner.test.ts @@ -236,6 +236,82 @@ describe("createEmbeddedLobsterRunner", () => { }); }); + it("forwards approvalId through resume when token is absent", async () => { + const runtime = { + runToolRequest: vi.fn(), + resumeToolRequest: vi.fn().mockResolvedValue({ + ok: true, + protocolVersion: 1, + status: "ok", + output: [], + requiresApproval: null, + }), + }; + + const runner = createEmbeddedLobsterRunner({ + loadRuntime: vi.fn().mockResolvedValue(runtime), + }); + + await runner.run({ + action: "resume", + approvalId: "dbc98d05", + approve: true, + cwd: process.cwd(), + timeoutMs: 2000, + maxStdoutBytes: 4096, + }); + + expect(runtime.resumeToolRequest).toHaveBeenCalledWith({ + approvalId: "dbc98d05", + approved: true, + ctx: expect.objectContaining({ mode: "tool" }), + }); + }); + + it("passes approvalId through the normalized needs_approval envelope", async () => { + const runtime = { + runToolRequest: vi.fn().mockResolvedValue({ + ok: true, + protocolVersion: 1, + status: "needs_approval", + output: [], + requiresApproval: { + type: "approval_request", + prompt: "ok?", + items: [], + resumeToken: "eyJ...", + approvalId: "dbc98d05", + }, + }), + resumeToolRequest: vi.fn(), + }; + + const runner = createEmbeddedLobsterRunner({ + loadRuntime: vi.fn().mockResolvedValue(runtime), + }); + + const envelope = await runner.run({ + action: "run", + pipeline: "exec --json=true echo hi", + cwd: process.cwd(), + timeoutMs: 2000, + maxStdoutBytes: 4096, + }); + + expect(envelope).toEqual({ + ok: true, + status: "needs_approval", + output: [], + requiresApproval: { + type: "approval_request", + prompt: "ok?", + items: [], + resumeToken: "eyJ...", + approvalId: "dbc98d05", + }, + }); + }); + it("loads the embedded runtime once per runner", async () => { const runtime = { runToolRequest: vi.fn().mockResolvedValue({ @@ -310,7 +386,7 @@ describe("createEmbeddedLobsterRunner", () => { timeoutMs: 2000, maxStdoutBytes: 4096, }), - ).rejects.toThrow(/token required/); + ).rejects.toThrow(/token or approvalId required/); await expect( runner.run({ diff --git a/extensions/lobster/src/lobster-runner.ts b/extensions/lobster/src/lobster-runner.ts index 4a66f66f8ba..8d0c891901b 100644 --- a/extensions/lobster/src/lobster-runner.ts +++ b/extensions/lobster/src/lobster-runner.ts @@ -16,6 +16,7 @@ export type LobsterEnvelope = prompt: string; items: unknown[]; resumeToken?: string; + approvalId?: string; }; } | { @@ -28,6 +29,7 @@ export type LobsterRunnerParams = { pipeline?: string; argsJson?: string; token?: string; + approvalId?: string; approve?: boolean; cwd: string; timeoutMs: number; @@ -61,6 +63,7 @@ type EmbeddedToolEnvelope = { items: unknown[]; preview?: string; resumeToken?: string; + approvalId?: string; } | null; requiresInput?: { prompt: string; @@ -157,6 +160,9 @@ function normalizeEnvelope(envelope: EmbeddedToolEnvelope): LobsterEnvelope { ...(envelope.requiresApproval.resumeToken ? { resumeToken: envelope.requiresApproval.resumeToken } : {}), + ...(envelope.requiresApproval.approvalId + ? { approvalId: envelope.requiresApproval.approvalId } + : {}), } : null, }; @@ -296,8 +302,9 @@ export function createEmbeddedLobsterRunner(options?: { } const token = params.token?.trim() ?? ""; - if (!token) { - throw new Error("token required"); + const approvalId = params.approvalId?.trim() ?? ""; + if (!token && !approvalId) { + throw new Error("token or approvalId required"); } if (typeof params.approve !== "boolean") { throw new Error("approve required"); @@ -306,7 +313,8 @@ export function createEmbeddedLobsterRunner(options?: { return throwOnErrorEnvelope( normalizeEnvelope( await runtime.resumeToolRequest({ - token, + ...(token ? { token } : {}), + ...(approvalId ? { approvalId } : {}), approved: params.approve, ctx, }), diff --git a/extensions/lobster/src/lobster-tool.ts b/extensions/lobster/src/lobster-tool.ts index 47cea46d3f2..8dfba439c54 100644 --- a/extensions/lobster/src/lobster-tool.ts +++ b/extensions/lobster/src/lobster-tool.ts @@ -221,6 +221,7 @@ export function createLobsterTool(api: OpenClawPluginApi, options?: LobsterToolO pipeline: Type.Optional(Type.String()), argsJson: Type.Optional(Type.String()), token: Type.Optional(Type.String()), + approvalId: Type.Optional(Type.String()), approve: Type.Optional(Type.Boolean()), cwd: Type.Optional( Type.String({ @@ -261,6 +262,7 @@ export function createLobsterTool(api: OpenClawPluginApi, options?: LobsterToolO ...(typeof params.pipeline === "string" ? { pipeline: params.pipeline } : {}), ...(typeof params.argsJson === "string" ? { argsJson: params.argsJson } : {}), ...(typeof params.token === "string" ? { token: params.token } : {}), + ...(typeof params.approvalId === "string" ? { approvalId: params.approvalId } : {}), ...(typeof params.approve === "boolean" ? { approve: params.approve } : {}), cwd, timeoutMs,