mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix(lobster): forward approvalId alongside resumeToken in tool envelope
@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.
This commit is contained in:
committed by
Peter Steinberger
parent
91dde183dc
commit
905da8bd6b
1
extensions/lobster/src/lobster-core.d.ts
vendored
1
extensions/lobster/src/lobster-core.d.ts
vendored
@@ -4,6 +4,7 @@ declare module "@clawdbot/lobster/core" {
|
||||
prompt: string;
|
||||
items: unknown[];
|
||||
resumeToken?: string;
|
||||
approvalId?: string;
|
||||
} | null;
|
||||
|
||||
type LobsterToolContext = {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user