@openclaw/lobster 2026.1.29 → 2026.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -13
- package/SKILL.md +14 -7
- package/index.ts +3 -2
- package/package.json +5 -2
- package/src/lobster-tool.test.ts +139 -35
- package/src/lobster-tool.ts +105 -17
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Adds the `lobster` agent tool as an **optional** plugin tool.
|
|
|
5
5
|
## What this is
|
|
6
6
|
|
|
7
7
|
- Lobster is a standalone workflow shell (typed JSON-first pipelines + approvals/resume).
|
|
8
|
-
- This plugin integrates Lobster with OpenClaw
|
|
8
|
+
- This plugin integrates Lobster with OpenClaw _without core changes_.
|
|
9
9
|
|
|
10
10
|
## Enable
|
|
11
11
|
|
|
@@ -53,22 +53,17 @@ Example (allow only a small set of tools):
|
|
|
53
53
|
{
|
|
54
54
|
"id": "main",
|
|
55
55
|
"tools": {
|
|
56
|
-
"allow": [
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
],
|
|
63
|
-
"deny": ["gateway"]
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
]
|
|
67
|
-
}
|
|
56
|
+
"allow": ["lobster", "web_fetch", "web_search", "gog", "gh"],
|
|
57
|
+
"deny": ["gateway"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
68
62
|
}
|
|
69
63
|
```
|
|
70
64
|
|
|
71
65
|
Notes:
|
|
66
|
+
|
|
72
67
|
- If `tools.allow` is omitted or empty, it behaves like "allow everything (except denied)". For a real allowlist, set a **non-empty** `allow`.
|
|
73
68
|
- Tool names depend on which plugins you have installed/enabled.
|
|
74
69
|
|
package/SKILL.md
CHANGED
|
@@ -8,13 +8,13 @@ Lobster executes multi-step workflows with approval checkpoints. Use it when:
|
|
|
8
8
|
|
|
9
9
|
## When to use Lobster
|
|
10
10
|
|
|
11
|
-
| User intent
|
|
12
|
-
|
|
13
|
-
| "Triage my email"
|
|
14
|
-
| "Send a message"
|
|
15
|
-
| "Check my email every morning and ask before replying" | Yes — scheduled workflow with approval
|
|
16
|
-
| "What's the weather?"
|
|
17
|
-
| "Monitor this PR and notify me of changes"
|
|
11
|
+
| User intent | Use Lobster? |
|
|
12
|
+
| ------------------------------------------------------ | --------------------------------------------- |
|
|
13
|
+
| "Triage my email" | Yes — multi-step, may send replies |
|
|
14
|
+
| "Send a message" | No — single action, use message tool directly |
|
|
15
|
+
| "Check my email every morning and ask before replying" | Yes — scheduled workflow with approval |
|
|
16
|
+
| "What's the weather?" | No — simple query |
|
|
17
|
+
| "Monitor this PR and notify me of changes" | Yes — stateful, recurring |
|
|
18
18
|
|
|
19
19
|
## Basic usage
|
|
20
20
|
|
|
@@ -28,6 +28,7 @@ Lobster executes multi-step workflows with approval checkpoints. Use it when:
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
Returns structured result:
|
|
31
|
+
|
|
31
32
|
```json
|
|
32
33
|
{
|
|
33
34
|
"protocolVersion": 1,
|
|
@@ -41,6 +42,7 @@ Returns structured result:
|
|
|
41
42
|
### Handle approval
|
|
42
43
|
|
|
43
44
|
If the workflow needs approval:
|
|
45
|
+
|
|
44
46
|
```json
|
|
45
47
|
{
|
|
46
48
|
"status": "needs_approval",
|
|
@@ -54,6 +56,7 @@ If the workflow needs approval:
|
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
Present the prompt to the user. If they approve:
|
|
59
|
+
|
|
57
60
|
```json
|
|
58
61
|
{
|
|
59
62
|
"action": "resume",
|
|
@@ -65,15 +68,19 @@ Present the prompt to the user. If they approve:
|
|
|
65
68
|
## Example workflows
|
|
66
69
|
|
|
67
70
|
### Email triage
|
|
71
|
+
|
|
68
72
|
```
|
|
69
73
|
gog.gmail.search --query 'newer_than:1d' --max 20 | email.triage
|
|
70
74
|
```
|
|
75
|
+
|
|
71
76
|
Fetches recent emails, classifies into buckets (needs_reply, needs_action, fyi).
|
|
72
77
|
|
|
73
78
|
### Email triage with approval gate
|
|
79
|
+
|
|
74
80
|
```
|
|
75
81
|
gog.gmail.search --query 'newer_than:1d' | email.triage | approve --prompt 'Process these?'
|
|
76
82
|
```
|
|
83
|
+
|
|
77
84
|
Same as above, but halts for approval before returning.
|
|
78
85
|
|
|
79
86
|
## Key behaviors
|
package/index.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "../../src/plugins/types.js";
|
|
2
|
-
|
|
3
2
|
import { createLobsterTool } from "./src/lobster-tool.js";
|
|
4
3
|
|
|
5
4
|
export default function register(api: OpenClawPluginApi) {
|
|
6
5
|
api.registerTool(
|
|
7
6
|
(ctx) => {
|
|
8
|
-
if (ctx.sandboxed)
|
|
7
|
+
if (ctx.sandboxed) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
9
10
|
return createLobsterTool(api);
|
|
10
11
|
},
|
|
11
12
|
{ optional: true },
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/lobster",
|
|
3
|
-
"version": "2026.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "2026.2.2",
|
|
5
4
|
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"openclaw": "workspace:*"
|
|
8
|
+
},
|
|
6
9
|
"openclaw": {
|
|
7
10
|
"extensions": [
|
|
8
11
|
"./index.ts"
|
package/src/lobster-tool.test.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
|
|
5
4
|
import { describe, expect, it } from "vitest";
|
|
6
|
-
|
|
7
5
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "../../../src/plugins/types.js";
|
|
8
6
|
import { createLobsterTool } from "./lobster-tool.js";
|
|
9
7
|
|
|
@@ -33,12 +31,14 @@ async function writeFakeLobster(params: { payload: unknown }) {
|
|
|
33
31
|
return await writeFakeLobsterScript(scriptBody);
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
function fakeApi(): OpenClawPluginApi {
|
|
34
|
+
function fakeApi(overrides: Partial<OpenClawPluginApi> = {}): OpenClawPluginApi {
|
|
37
35
|
return {
|
|
38
36
|
id: "lobster",
|
|
39
37
|
name: "lobster",
|
|
40
38
|
source: "test",
|
|
41
|
-
config: {}
|
|
39
|
+
config: {},
|
|
40
|
+
pluginConfig: {},
|
|
41
|
+
// oxlint-disable-next-line typescript/no-explicit-any
|
|
42
42
|
runtime: { version: "test" } as any,
|
|
43
43
|
logger: { info() {}, warn() {}, error() {}, debug() {} },
|
|
44
44
|
registerTool() {},
|
|
@@ -48,13 +48,18 @@ function fakeApi(): OpenClawPluginApi {
|
|
|
48
48
|
registerCli() {},
|
|
49
49
|
registerService() {},
|
|
50
50
|
registerProvider() {},
|
|
51
|
+
registerHook() {},
|
|
52
|
+
registerHttpRoute() {},
|
|
53
|
+
registerCommand() {},
|
|
54
|
+
on() {},
|
|
51
55
|
resolvePath: (p) => p,
|
|
56
|
+
...overrides,
|
|
52
57
|
};
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
function fakeCtx(overrides: Partial<OpenClawPluginToolContext> = {}): OpenClawPluginToolContext {
|
|
56
61
|
return {
|
|
57
|
-
config: {}
|
|
62
|
+
config: {},
|
|
58
63
|
workspaceDir: "/tmp",
|
|
59
64
|
agentDir: "/tmp",
|
|
60
65
|
agentId: "main",
|
|
@@ -72,68 +77,167 @@ describe("lobster plugin tool", () => {
|
|
|
72
77
|
payload: { ok: true, status: "ok", output: [{ hello: "world" }], requiresApproval: null },
|
|
73
78
|
});
|
|
74
79
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
const originalPath = process.env.PATH;
|
|
81
|
+
process.env.PATH = `${fake.dir}${path.delimiter}${originalPath ?? ""}`;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const tool = createLobsterTool(fakeApi());
|
|
85
|
+
const res = await tool.execute("call1", {
|
|
86
|
+
action: "run",
|
|
87
|
+
pipeline: "noop",
|
|
88
|
+
timeoutMs: 1000,
|
|
89
|
+
});
|
|
82
90
|
|
|
83
|
-
|
|
91
|
+
expect(res.details).toMatchObject({ ok: true, status: "ok" });
|
|
92
|
+
} finally {
|
|
93
|
+
process.env.PATH = originalPath;
|
|
94
|
+
}
|
|
84
95
|
});
|
|
85
96
|
|
|
86
97
|
it("tolerates noisy stdout before the JSON envelope", async () => {
|
|
87
98
|
const payload = { ok: true, status: "ok", output: [], requiresApproval: null };
|
|
88
|
-
const {
|
|
99
|
+
const { dir } = await writeFakeLobsterScript(
|
|
89
100
|
`const payload = ${JSON.stringify(payload)};\n` +
|
|
90
101
|
`console.log("noise before json");\n` +
|
|
91
102
|
`process.stdout.write(JSON.stringify(payload));\n`,
|
|
92
103
|
"openclaw-lobster-plugin-noisy-",
|
|
93
104
|
);
|
|
94
105
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
const originalPath = process.env.PATH;
|
|
107
|
+
process.env.PATH = `${dir}${path.delimiter}${originalPath ?? ""}`;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const tool = createLobsterTool(fakeApi());
|
|
111
|
+
const res = await tool.execute("call-noisy", {
|
|
112
|
+
action: "run",
|
|
113
|
+
pipeline: "noop",
|
|
114
|
+
timeoutMs: 1000,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(res.details).toMatchObject({ ok: true, status: "ok" });
|
|
118
|
+
} finally {
|
|
119
|
+
process.env.PATH = originalPath;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("requires absolute lobsterPath when provided (even though it is ignored)", async () => {
|
|
124
|
+
const fake = await writeFakeLobster({
|
|
125
|
+
payload: { ok: true, status: "ok", output: [{ hello: "world" }], requiresApproval: null },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const originalPath = process.env.PATH;
|
|
129
|
+
process.env.PATH = `${fake.dir}${path.delimiter}${originalPath ?? ""}`;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const tool = createLobsterTool(fakeApi());
|
|
133
|
+
await expect(
|
|
134
|
+
tool.execute("call2", {
|
|
135
|
+
action: "run",
|
|
136
|
+
pipeline: "noop",
|
|
137
|
+
lobsterPath: "./lobster",
|
|
138
|
+
}),
|
|
139
|
+
).rejects.toThrow(/absolute path/);
|
|
140
|
+
} finally {
|
|
141
|
+
process.env.PATH = originalPath;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("rejects lobsterPath (deprecated) when invalid", async () => {
|
|
146
|
+
const fake = await writeFakeLobster({
|
|
147
|
+
payload: { ok: true, status: "ok", output: [{ hello: "world" }], requiresApproval: null },
|
|
101
148
|
});
|
|
102
149
|
|
|
103
|
-
|
|
150
|
+
const originalPath = process.env.PATH;
|
|
151
|
+
process.env.PATH = `${fake.dir}${path.delimiter}${originalPath ?? ""}`;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const tool = createLobsterTool(fakeApi());
|
|
155
|
+
await expect(
|
|
156
|
+
tool.execute("call2b", {
|
|
157
|
+
action: "run",
|
|
158
|
+
pipeline: "noop",
|
|
159
|
+
lobsterPath: "/bin/bash",
|
|
160
|
+
}),
|
|
161
|
+
).rejects.toThrow(/lobster executable/);
|
|
162
|
+
} finally {
|
|
163
|
+
process.env.PATH = originalPath;
|
|
164
|
+
}
|
|
104
165
|
});
|
|
105
166
|
|
|
106
|
-
it("
|
|
167
|
+
it("rejects absolute cwd", async () => {
|
|
107
168
|
const tool = createLobsterTool(fakeApi());
|
|
108
169
|
await expect(
|
|
109
|
-
tool.execute("
|
|
170
|
+
tool.execute("call2c", {
|
|
110
171
|
action: "run",
|
|
111
172
|
pipeline: "noop",
|
|
112
|
-
|
|
173
|
+
cwd: "/tmp",
|
|
113
174
|
}),
|
|
114
|
-
).rejects.toThrow(/
|
|
175
|
+
).rejects.toThrow(/cwd must be a relative path/);
|
|
115
176
|
});
|
|
116
177
|
|
|
117
|
-
it("rejects
|
|
118
|
-
const { binPath } = await writeFakeLobsterScript(
|
|
119
|
-
`process.stdout.write("nope");\n`,
|
|
120
|
-
"openclaw-lobster-plugin-bad-",
|
|
121
|
-
);
|
|
122
|
-
|
|
178
|
+
it("rejects cwd that escapes the gateway working directory", async () => {
|
|
123
179
|
const tool = createLobsterTool(fakeApi());
|
|
124
180
|
await expect(
|
|
125
|
-
tool.execute("
|
|
181
|
+
tool.execute("call2d", {
|
|
126
182
|
action: "run",
|
|
127
183
|
pipeline: "noop",
|
|
128
|
-
|
|
184
|
+
cwd: "../../etc",
|
|
129
185
|
}),
|
|
130
|
-
).rejects.toThrow(/
|
|
186
|
+
).rejects.toThrow(/must stay within/);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("uses pluginConfig.lobsterPath when provided", async () => {
|
|
190
|
+
const fake = await writeFakeLobster({
|
|
191
|
+
payload: { ok: true, status: "ok", output: [{ hello: "world" }], requiresApproval: null },
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Ensure `lobster` is NOT discoverable via PATH, while still allowing our
|
|
195
|
+
// fake lobster (a Node script with `#!/usr/bin/env node`) to run.
|
|
196
|
+
const originalPath = process.env.PATH;
|
|
197
|
+
process.env.PATH = path.dirname(process.execPath);
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const tool = createLobsterTool(fakeApi({ pluginConfig: { lobsterPath: fake.binPath } }));
|
|
201
|
+
const res = await tool.execute("call-plugin-config", {
|
|
202
|
+
action: "run",
|
|
203
|
+
pipeline: "noop",
|
|
204
|
+
timeoutMs: 1000,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(res.details).toMatchObject({ ok: true, status: "ok" });
|
|
208
|
+
} finally {
|
|
209
|
+
process.env.PATH = originalPath;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("rejects invalid JSON from lobster", async () => {
|
|
214
|
+
const { dir } = await writeFakeLobsterScript(
|
|
215
|
+
`process.stdout.write("nope");\n`,
|
|
216
|
+
"openclaw-lobster-plugin-bad-",
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const originalPath = process.env.PATH;
|
|
220
|
+
process.env.PATH = `${dir}${path.delimiter}${originalPath ?? ""}`;
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const tool = createLobsterTool(fakeApi());
|
|
224
|
+
await expect(
|
|
225
|
+
tool.execute("call3", {
|
|
226
|
+
action: "run",
|
|
227
|
+
pipeline: "noop",
|
|
228
|
+
}),
|
|
229
|
+
).rejects.toThrow(/invalid JSON/);
|
|
230
|
+
} finally {
|
|
231
|
+
process.env.PATH = originalPath;
|
|
232
|
+
}
|
|
131
233
|
});
|
|
132
234
|
|
|
133
235
|
it("can be gated off in sandboxed contexts", async () => {
|
|
134
236
|
const api = fakeApi();
|
|
135
237
|
const factoryTool = (ctx: OpenClawPluginToolContext) => {
|
|
136
|
-
if (ctx.sandboxed)
|
|
238
|
+
if (ctx.sandboxed) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
137
241
|
return createLobsterTool(api);
|
|
138
242
|
};
|
|
139
243
|
|
package/src/lobster-tool.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
|
|
5
5
|
import type { OpenClawPluginApi } from "../../../src/plugins/types.js";
|
|
6
6
|
|
|
7
7
|
type LobsterEnvelope =
|
|
@@ -23,16 +23,77 @@ type LobsterEnvelope =
|
|
|
23
23
|
|
|
24
24
|
function resolveExecutablePath(lobsterPathRaw: string | undefined) {
|
|
25
25
|
const lobsterPath = lobsterPathRaw?.trim() || "lobster";
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
|
|
27
|
+
// SECURITY:
|
|
28
|
+
// Never allow arbitrary executables (e.g. /bin/bash). If the caller overrides
|
|
29
|
+
// the path, it must still be the lobster binary (by name) and be absolute.
|
|
30
|
+
if (lobsterPath !== "lobster") {
|
|
31
|
+
if (!path.isAbsolute(lobsterPath)) {
|
|
32
|
+
throw new Error("lobsterPath must be an absolute path (or omit to use PATH)");
|
|
33
|
+
}
|
|
34
|
+
const base = path.basename(lobsterPath).toLowerCase();
|
|
35
|
+
const allowed =
|
|
36
|
+
process.platform === "win32" ? ["lobster.exe", "lobster.cmd", "lobster.bat"] : ["lobster"];
|
|
37
|
+
if (!allowed.includes(base)) {
|
|
38
|
+
throw new Error("lobsterPath must point to the lobster executable");
|
|
39
|
+
}
|
|
40
|
+
let stat: fs.Stats;
|
|
41
|
+
try {
|
|
42
|
+
stat = fs.statSync(lobsterPath);
|
|
43
|
+
} catch {
|
|
44
|
+
throw new Error("lobsterPath must exist");
|
|
45
|
+
}
|
|
46
|
+
if (!stat.isFile()) {
|
|
47
|
+
throw new Error("lobsterPath must point to a file");
|
|
48
|
+
}
|
|
49
|
+
if (process.platform !== "win32") {
|
|
50
|
+
try {
|
|
51
|
+
fs.accessSync(lobsterPath, fs.constants.X_OK);
|
|
52
|
+
} catch {
|
|
53
|
+
throw new Error("lobsterPath must be executable");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
28
56
|
}
|
|
57
|
+
|
|
29
58
|
return lobsterPath;
|
|
30
59
|
}
|
|
31
60
|
|
|
32
|
-
function
|
|
33
|
-
|
|
61
|
+
function normalizeForCwdSandbox(p: string): string {
|
|
62
|
+
const normalized = path.normalize(p);
|
|
63
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveCwd(cwdRaw: unknown): string {
|
|
67
|
+
if (typeof cwdRaw !== "string" || !cwdRaw.trim()) {
|
|
68
|
+
return process.cwd();
|
|
69
|
+
}
|
|
70
|
+
const cwd = cwdRaw.trim();
|
|
71
|
+
if (path.isAbsolute(cwd)) {
|
|
72
|
+
throw new Error("cwd must be a relative path");
|
|
73
|
+
}
|
|
74
|
+
const base = process.cwd();
|
|
75
|
+
const resolved = path.resolve(base, cwd);
|
|
76
|
+
|
|
77
|
+
const rel = path.relative(normalizeForCwdSandbox(base), normalizeForCwdSandbox(resolved));
|
|
78
|
+
if (rel === "" || rel === ".") {
|
|
79
|
+
return resolved;
|
|
80
|
+
}
|
|
81
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
82
|
+
throw new Error("cwd must stay within the gateway working directory");
|
|
83
|
+
}
|
|
84
|
+
return resolved;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isWindowsSpawnErrorThatCanUseShell(err: unknown) {
|
|
88
|
+
if (!err || typeof err !== "object") {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
34
91
|
const code = (err as { code?: unknown }).code;
|
|
35
|
-
|
|
92
|
+
|
|
93
|
+
// On Windows, spawning scripts discovered on PATH (e.g. lobster.cmd) can fail
|
|
94
|
+
// with EINVAL, and PATH discovery itself can fail with ENOENT when the binary
|
|
95
|
+
// is only available via PATHEXT/script wrappers.
|
|
96
|
+
return code === "EINVAL" || code === "ENOENT";
|
|
36
97
|
}
|
|
37
98
|
|
|
38
99
|
async function runLobsterSubprocessOnce(
|
|
@@ -123,7 +184,7 @@ async function runLobsterSubprocess(params: {
|
|
|
123
184
|
try {
|
|
124
185
|
return await runLobsterSubprocessOnce(params, false);
|
|
125
186
|
} catch (err) {
|
|
126
|
-
if (process.platform === "win32" &&
|
|
187
|
+
if (process.platform === "win32" && isWindowsSpawnErrorThatCanUseShell(err)) {
|
|
127
188
|
return await runLobsterSubprocessOnce(params, true);
|
|
128
189
|
}
|
|
129
190
|
throw err;
|
|
@@ -180,26 +241,49 @@ export function createLobsterTool(api: OpenClawPluginApi) {
|
|
|
180
241
|
argsJson: Type.Optional(Type.String()),
|
|
181
242
|
token: Type.Optional(Type.String()),
|
|
182
243
|
approve: Type.Optional(Type.Boolean()),
|
|
183
|
-
|
|
184
|
-
|
|
244
|
+
// SECURITY: Do not allow the agent to choose an executable path.
|
|
245
|
+
// Host can configure the lobster binary via plugin config.
|
|
246
|
+
lobsterPath: Type.Optional(
|
|
247
|
+
Type.String({ description: "(deprecated) Use plugin config instead." }),
|
|
248
|
+
),
|
|
249
|
+
cwd: Type.Optional(
|
|
250
|
+
Type.String({
|
|
251
|
+
description:
|
|
252
|
+
"Relative working directory (optional). Must stay within the gateway working directory.",
|
|
253
|
+
}),
|
|
254
|
+
),
|
|
185
255
|
timeoutMs: Type.Optional(Type.Number()),
|
|
186
256
|
maxStdoutBytes: Type.Optional(Type.Number()),
|
|
187
257
|
}),
|
|
188
258
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
189
|
-
const action =
|
|
190
|
-
if (!action)
|
|
259
|
+
const action = typeof params.action === "string" ? params.action.trim() : "";
|
|
260
|
+
if (!action) {
|
|
261
|
+
throw new Error("action required");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// SECURITY: never allow tool callers (agent/user) to select executables.
|
|
265
|
+
// If a host needs to override the binary, it must do so via plugin config.
|
|
266
|
+
// We still validate the parameter shape to prevent reintroducing an RCE footgun.
|
|
267
|
+
if (typeof params.lobsterPath === "string" && params.lobsterPath.trim()) {
|
|
268
|
+
resolveExecutablePath(params.lobsterPath);
|
|
269
|
+
}
|
|
191
270
|
|
|
192
271
|
const execPath = resolveExecutablePath(
|
|
193
|
-
typeof
|
|
272
|
+
typeof api.pluginConfig?.lobsterPath === "string"
|
|
273
|
+
? api.pluginConfig.lobsterPath
|
|
274
|
+
: undefined,
|
|
194
275
|
);
|
|
195
|
-
const cwd =
|
|
276
|
+
const cwd = resolveCwd(params.cwd);
|
|
196
277
|
const timeoutMs = typeof params.timeoutMs === "number" ? params.timeoutMs : 20_000;
|
|
197
|
-
const maxStdoutBytes =
|
|
278
|
+
const maxStdoutBytes =
|
|
279
|
+
typeof params.maxStdoutBytes === "number" ? params.maxStdoutBytes : 512_000;
|
|
198
280
|
|
|
199
281
|
const argv = (() => {
|
|
200
282
|
if (action === "run") {
|
|
201
283
|
const pipeline = typeof params.pipeline === "string" ? params.pipeline : "";
|
|
202
|
-
if (!pipeline.trim())
|
|
284
|
+
if (!pipeline.trim()) {
|
|
285
|
+
throw new Error("pipeline required");
|
|
286
|
+
}
|
|
203
287
|
const argv = ["run", "--mode", "tool", pipeline];
|
|
204
288
|
const argsJson = typeof params.argsJson === "string" ? params.argsJson : "";
|
|
205
289
|
if (argsJson.trim()) {
|
|
@@ -209,9 +293,13 @@ export function createLobsterTool(api: OpenClawPluginApi) {
|
|
|
209
293
|
}
|
|
210
294
|
if (action === "resume") {
|
|
211
295
|
const token = typeof params.token === "string" ? params.token : "";
|
|
212
|
-
if (!token.trim())
|
|
296
|
+
if (!token.trim()) {
|
|
297
|
+
throw new Error("token required");
|
|
298
|
+
}
|
|
213
299
|
const approve = params.approve;
|
|
214
|
-
if (typeof approve !== "boolean")
|
|
300
|
+
if (typeof approve !== "boolean") {
|
|
301
|
+
throw new Error("approve required");
|
|
302
|
+
}
|
|
215
303
|
return ["resume", "--token", token, "--approve", approve ? "yes" : "no"];
|
|
216
304
|
}
|
|
217
305
|
throw new Error(`Unknown action: ${action}`);
|