@lobu/worker 6.1.1 → 7.1.0

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.
Files changed (124) hide show
  1. package/dist/core/error-handler.d.ts +0 -4
  2. package/dist/core/error-handler.d.ts.map +1 -1
  3. package/dist/core/error-handler.js +4 -15
  4. package/dist/core/error-handler.js.map +1 -1
  5. package/dist/core/types.d.ts +1 -19
  6. package/dist/core/types.d.ts.map +1 -1
  7. package/dist/core/types.js +0 -4
  8. package/dist/core/types.js.map +1 -1
  9. package/dist/core/workspace.d.ts +2 -11
  10. package/dist/core/workspace.d.ts.map +1 -1
  11. package/dist/core/workspace.js +14 -36
  12. package/dist/core/workspace.js.map +1 -1
  13. package/dist/embedded/just-bash-bootstrap.d.ts.map +1 -1
  14. package/dist/embedded/just-bash-bootstrap.js +60 -6
  15. package/dist/embedded/just-bash-bootstrap.js.map +1 -1
  16. package/dist/embedded/mcp-cli-commands.d.ts.map +1 -1
  17. package/dist/embedded/mcp-cli-commands.js +3 -38
  18. package/dist/embedded/mcp-cli-commands.js.map +1 -1
  19. package/dist/gateway/gateway-integration.js +4 -4
  20. package/dist/gateway/gateway-integration.js.map +1 -1
  21. package/dist/gateway/message-batcher.d.ts.map +1 -1
  22. package/dist/gateway/message-batcher.js +3 -5
  23. package/dist/gateway/message-batcher.js.map +1 -1
  24. package/dist/gateway/sse-client.d.ts +1 -0
  25. package/dist/gateway/sse-client.d.ts.map +1 -1
  26. package/dist/gateway/sse-client.js +52 -8
  27. package/dist/gateway/sse-client.js.map +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +7 -24
  30. package/dist/index.js.map +1 -1
  31. package/dist/instructions/builder.d.ts.map +1 -1
  32. package/dist/instructions/builder.js +2 -1
  33. package/dist/instructions/builder.js.map +1 -1
  34. package/dist/openclaw/plugin-loader.d.ts.map +1 -1
  35. package/dist/openclaw/plugin-loader.js +8 -19
  36. package/dist/openclaw/plugin-loader.js.map +1 -1
  37. package/dist/openclaw/processor.d.ts.map +1 -1
  38. package/dist/openclaw/processor.js +2 -0
  39. package/dist/openclaw/processor.js.map +1 -1
  40. package/dist/openclaw/sandbox-leak.d.ts.map +1 -1
  41. package/dist/openclaw/sandbox-leak.js +1 -6
  42. package/dist/openclaw/sandbox-leak.js.map +1 -1
  43. package/dist/openclaw/session-context.d.ts.map +1 -1
  44. package/dist/openclaw/session-context.js +3 -0
  45. package/dist/openclaw/session-context.js.map +1 -1
  46. package/dist/openclaw/tool-policy.d.ts.map +1 -1
  47. package/dist/openclaw/tool-policy.js +5 -11
  48. package/dist/openclaw/tool-policy.js.map +1 -1
  49. package/dist/openclaw/worker.d.ts +0 -1
  50. package/dist/openclaw/worker.d.ts.map +1 -1
  51. package/dist/openclaw/worker.js +19 -85
  52. package/dist/openclaw/worker.js.map +1 -1
  53. package/dist/server.d.ts.map +1 -1
  54. package/dist/server.js +3 -40
  55. package/dist/server.js.map +1 -1
  56. package/dist/shared/audio-provider-suggestions.d.ts.map +1 -1
  57. package/dist/shared/audio-provider-suggestions.js +4 -6
  58. package/dist/shared/audio-provider-suggestions.js.map +1 -1
  59. package/dist/shared/tool-implementations.d.ts.map +1 -1
  60. package/dist/shared/tool-implementations.js +99 -37
  61. package/dist/shared/tool-implementations.js.map +1 -1
  62. package/package.json +14 -4
  63. package/src/__tests__/audio-provider-suggestions.test.ts +199 -0
  64. package/src/__tests__/custom-tools.test.ts +92 -0
  65. package/src/__tests__/embedded-just-bash-bootstrap.test.ts +128 -0
  66. package/src/__tests__/embedded-mcp-cli-bash.test.ts +179 -0
  67. package/src/__tests__/embedded-tools.test.ts +744 -0
  68. package/src/__tests__/exec-sandbox-extra.test.ts +0 -0
  69. package/src/__tests__/exec-sandbox.test.ts +550 -0
  70. package/src/__tests__/generated-media.test.ts +142 -0
  71. package/src/__tests__/instructions.test.ts +60 -0
  72. package/src/__tests__/mcp-cli-commands-extra.test.ts +478 -0
  73. package/src/__tests__/mcp-cli-commands.test.ts +383 -0
  74. package/src/__tests__/mcp-tool-call.test.ts +423 -0
  75. package/src/__tests__/memory-flush-harden.test.ts +367 -0
  76. package/src/__tests__/memory-flush-runtime.test.ts +138 -0
  77. package/src/__tests__/memory-flush.test.ts +64 -0
  78. package/src/__tests__/message-batcher.test.ts +247 -0
  79. package/src/__tests__/model-resolver-harden.test.ts +197 -0
  80. package/src/__tests__/model-resolver.test.ts +156 -0
  81. package/src/__tests__/processor-harden.test.ts +259 -0
  82. package/src/__tests__/processor.test.ts +225 -0
  83. package/src/__tests__/replace-base-prompt-identity.test.ts +41 -0
  84. package/src/__tests__/sandbox-leak-harden.test.ts +200 -0
  85. package/src/__tests__/sandbox-leak.test.ts +167 -0
  86. package/src/__tests__/setup.ts +102 -0
  87. package/src/__tests__/sse-client-harden.test.ts +588 -0
  88. package/src/__tests__/sse-client.test.ts +90 -0
  89. package/src/__tests__/tool-implementations.test.ts +196 -0
  90. package/src/__tests__/tool-policy-edge-cases.test.ts +263 -0
  91. package/src/__tests__/tool-policy.test.ts +269 -0
  92. package/src/__tests__/worker.test.ts +89 -0
  93. package/src/core/error-handler.ts +47 -0
  94. package/src/core/project-scanner.ts +65 -0
  95. package/src/core/types.ts +94 -0
  96. package/src/core/workspace.ts +66 -0
  97. package/src/embedded/exec-sandbox.ts +372 -0
  98. package/src/embedded/just-bash-bootstrap.ts +575 -0
  99. package/src/embedded/mcp-cli-commands.ts +405 -0
  100. package/src/gateway/gateway-integration.ts +298 -0
  101. package/src/gateway/message-batcher.ts +123 -0
  102. package/src/gateway/sse-client.ts +988 -0
  103. package/src/gateway/types.ts +68 -0
  104. package/src/index.ts +123 -0
  105. package/src/instructions/builder.ts +44 -0
  106. package/src/instructions/providers.ts +27 -0
  107. package/src/modules/lifecycle.ts +92 -0
  108. package/src/openclaw/custom-tools.ts +315 -0
  109. package/src/openclaw/instructions.ts +36 -0
  110. package/src/openclaw/model-resolver.ts +150 -0
  111. package/src/openclaw/plugin-loader.ts +423 -0
  112. package/src/openclaw/processor.ts +199 -0
  113. package/src/openclaw/sandbox-leak.ts +100 -0
  114. package/src/openclaw/session-context.ts +323 -0
  115. package/src/openclaw/tool-policy.ts +241 -0
  116. package/src/openclaw/tools.ts +277 -0
  117. package/src/openclaw/worker.ts +1836 -0
  118. package/src/server.ts +330 -0
  119. package/src/shared/audio-provider-suggestions.ts +130 -0
  120. package/src/shared/processor-utils.ts +33 -0
  121. package/src/shared/provider-auth-hints.ts +68 -0
  122. package/src/shared/tool-display-config.ts +75 -0
  123. package/src/shared/tool-implementations.ts +981 -0
  124. package/src/shared/worker-env-keys.ts +8 -0
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Per-exec sandbox for embedded just-bash custom commands.
3
+ *
4
+ * just-bash's interpreter and `ReadWriteFs` already root file ops in the
5
+ * thread workspace, but spawned binaries (gh, git, lobu, anything from
6
+ * /nix/store) bypass that — once execFile fires, the child has the host's
7
+ * full FS view. This module wraps every spawn in an OS sandbox so the child
8
+ * sees only the workspace + ro system paths.
9
+ *
10
+ * Strategies:
11
+ * - "sandbox-exec" (macOS) — allow-default profile with denies for personal
12
+ * data paths (~/.ssh, ~/.aws, /etc, /Users, keychains, etc.) and writes
13
+ * restricted to the workspace.
14
+ * - "bwrap" (Linux) — true deny-default via bind mounts; only the workspace
15
+ * and ro system paths are visible. Probed at startup with a real spawn
16
+ * because `--unshare-user` requires `kernel.unprivileged_userns_clone=1`.
17
+ * - "none" — no sandbox available; commands run with host privileges. A
18
+ * warning is logged once at probe time.
19
+ *
20
+ * Override with LOBU_EXEC_SANDBOX={auto,bwrap,sandbox-exec,off}. Explicit
21
+ * overrides fail closed: requesting `bwrap` on a host without bubblewrap is a
22
+ * hard error, not a silent fallback to "none".
23
+ */
24
+
25
+ import { execFileSync } from "node:child_process";
26
+ import fs from "node:fs";
27
+
28
+ export type SandboxKind = "sandbox-exec" | "bwrap" | "none";
29
+
30
+ export interface SandboxStrategy {
31
+ kind: SandboxKind;
32
+ /** Absolute path to the wrapper binary. Absent for "none". */
33
+ path?: string;
34
+ }
35
+
36
+ export interface SandboxWrapOptions {
37
+ /** Workspace dir on the host FS. Wrapped child gets rw access here only. */
38
+ workspaceDir: string;
39
+ /**
40
+ * If false, the child cannot reach the network. just-bash's domain allowlist
41
+ * still applies inside the interpreter; this controls whether spawned
42
+ * binaries can open sockets at all. Set true when the gateway HTTP proxy is
43
+ * the egress path (binaries respect HTTP_PROXY).
44
+ */
45
+ allowNet?: boolean;
46
+ /** Absolute cwd inside the bwrap namespace. Must be /workspace or below. */
47
+ bwrapCwd?: string;
48
+ }
49
+
50
+ /** Workspace path is interpolated into SBPL/argv unescaped. Reject anything
51
+ * that could break out of the SBPL string literal, inject extra rules, or
52
+ * paper over a non-canonical path. Callers must canonicalize first. */
53
+ const WORKSPACE_DIR_SAFE = /^\/[A-Za-z0-9._\-/+@:]+$/;
54
+
55
+ interface CacheEntry {
56
+ key: string;
57
+ strategy: SandboxStrategy;
58
+ }
59
+ let cached: CacheEntry | null = null;
60
+ let warnedNoSandbox = false;
61
+
62
+ function cacheKey(): string {
63
+ return `${process.platform}|${process.env.LOBU_EXEC_SANDBOX ?? ""}`;
64
+ }
65
+
66
+ function which(bin: string): string | null {
67
+ try {
68
+ return (
69
+ execFileSync("/usr/bin/which", [bin], {
70
+ encoding: "utf8",
71
+ stdio: ["ignore", "pipe", "ignore"],
72
+ }).trim() || null
73
+ );
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ function envOverride(): SandboxKind | null {
80
+ const v = process.env.LOBU_EXEC_SANDBOX?.toLowerCase();
81
+ if (!v || v === "auto") return null;
82
+ if (v === "off" || v === "none") return "none";
83
+ if (v === "bwrap" || v === "sandbox-exec") return v;
84
+ console.warn(
85
+ `[exec-sandbox] Unknown LOBU_EXEC_SANDBOX=${v}, falling back to auto.`
86
+ );
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * Run bwrap with the same unshare flags we use in production but against
92
+ * `/bin/true`. If user namespaces aren't available (kernel disabled, seccomp
93
+ * profile blocking `unshare(CLONE_NEWUSER)`), this fails — and we should
94
+ * surface it rather than silently degrade to "no sandbox".
95
+ */
96
+ function bwrapDeliversIsolation(bwrapPath: string): boolean {
97
+ try {
98
+ execFileSync(
99
+ bwrapPath,
100
+ [
101
+ "--unshare-user",
102
+ "--unshare-pid",
103
+ "--unshare-ipc",
104
+ "--unshare-uts",
105
+ "--unshare-net",
106
+ "--ro-bind",
107
+ "/usr",
108
+ "/usr",
109
+ "--ro-bind-try",
110
+ "/lib",
111
+ "/lib",
112
+ "--ro-bind-try",
113
+ "/lib64",
114
+ "/lib64",
115
+ "--ro-bind-try",
116
+ "/bin",
117
+ "/bin",
118
+ "--proc",
119
+ "/proc",
120
+ "--dev",
121
+ "/dev",
122
+ "--",
123
+ "/usr/bin/true",
124
+ ],
125
+ { stdio: "ignore", timeout: 3000 }
126
+ );
127
+ return true;
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+
133
+ function setCache(key: string, strategy: SandboxStrategy): SandboxStrategy {
134
+ cached = { key, strategy };
135
+ return strategy;
136
+ }
137
+
138
+ export function probeSandboxStrategy(): SandboxStrategy {
139
+ const key = cacheKey();
140
+ if (cached && cached.key === key) return cached.strategy;
141
+
142
+ const override = envOverride();
143
+
144
+ if (override === "none") {
145
+ return setCache(key, { kind: "none" });
146
+ }
147
+
148
+ // Explicit override: try only the requested backend, fail closed.
149
+ if (override) {
150
+ if (override === "sandbox-exec") {
151
+ const p = which("sandbox-exec") ?? "/usr/bin/sandbox-exec";
152
+ if (fs.existsSync(p)) {
153
+ return setCache(key, { kind: "sandbox-exec", path: p });
154
+ }
155
+ throw new Error(
156
+ `[exec-sandbox] LOBU_EXEC_SANDBOX=sandbox-exec but ${p} not found.`
157
+ );
158
+ }
159
+ if (override === "bwrap") {
160
+ const p = which("bwrap");
161
+ if (p && fs.existsSync(p) && bwrapDeliversIsolation(p)) {
162
+ return setCache(key, { kind: "bwrap", path: p });
163
+ }
164
+ throw new Error(
165
+ `[exec-sandbox] LOBU_EXEC_SANDBOX=bwrap but bubblewrap is unavailable ` +
166
+ `or user namespaces are blocked. Install bubblewrap and ensure ` +
167
+ `kernel.unprivileged_userns_clone=1 (or seccomp profile permits unshare).`
168
+ );
169
+ }
170
+ }
171
+
172
+ // Auto-detect by platform.
173
+ if (process.platform === "darwin") {
174
+ const p = which("sandbox-exec") ?? "/usr/bin/sandbox-exec";
175
+ if (fs.existsSync(p)) {
176
+ return setCache(key, { kind: "sandbox-exec", path: p });
177
+ }
178
+ }
179
+
180
+ if (process.platform === "linux") {
181
+ const p = which("bwrap");
182
+ if (p && fs.existsSync(p) && bwrapDeliversIsolation(p)) {
183
+ return setCache(key, { kind: "bwrap", path: p });
184
+ }
185
+ }
186
+
187
+ if (!warnedNoSandbox) {
188
+ warnedNoSandbox = true;
189
+ console.warn(
190
+ `[exec-sandbox] No sandbox available on platform=${process.platform}. ` +
191
+ `Spawned binaries will run with host privileges. ` +
192
+ `Install bubblewrap (Linux) or check sandbox-exec (macOS).`
193
+ );
194
+ }
195
+ return setCache(key, { kind: "none" });
196
+ }
197
+
198
+ /** For tests: forget the cached strategy + warning state. */
199
+ export function resetSandboxProbeForTests(): void {
200
+ cached = null;
201
+ warnedNoSandbox = false;
202
+ }
203
+
204
+ function assertSafeWorkspacePath(workspaceDir: string): void {
205
+ if (!WORKSPACE_DIR_SAFE.test(workspaceDir)) {
206
+ throw new Error(
207
+ `[exec-sandbox] workspaceDir ${JSON.stringify(workspaceDir)} contains ` +
208
+ `unsafe characters; only [A-Za-z0-9._\\-/+@:] is allowed.`
209
+ );
210
+ }
211
+ if (
212
+ workspaceDir === "/" ||
213
+ workspaceDir.includes("//") ||
214
+ workspaceDir.split("/").includes("..") ||
215
+ workspaceDir.endsWith("/")
216
+ ) {
217
+ throw new Error(
218
+ `[exec-sandbox] workspaceDir ${JSON.stringify(workspaceDir)} is not a ` +
219
+ `canonical absolute path. Pass realpathSync(dir) at boot.`
220
+ );
221
+ }
222
+ }
223
+
224
+ function assertSafeBwrapCwd(bwrapCwd: string): void {
225
+ if (bwrapCwd !== "/workspace" && !bwrapCwd.startsWith("/workspace/")) {
226
+ throw new Error(
227
+ `[exec-sandbox] bwrap cwd ${JSON.stringify(bwrapCwd)} must be ` +
228
+ `/workspace or below.`
229
+ );
230
+ }
231
+ if (bwrapCwd.includes("\0") || bwrapCwd.split("/").includes("..")) {
232
+ throw new Error(
233
+ `[exec-sandbox] bwrap cwd ${JSON.stringify(bwrapCwd)} is unsafe.`
234
+ );
235
+ }
236
+ }
237
+
238
+ /**
239
+ * SBPL profile for macOS. Allow-default with targeted denies for personal-data
240
+ * paths and a write-island scoped to the workspace. Last-match-wins lets the
241
+ * workspace allow override the broader `/Users` deny when workspaceDir is
242
+ * under /Users (developer dev machine).
243
+ */
244
+ function buildSandboxExecProfile(
245
+ workspaceDir: string,
246
+ opts: SandboxWrapOptions
247
+ ): string {
248
+ assertSafeWorkspacePath(workspaceDir);
249
+ return `(version 1)
250
+ (allow default)
251
+
252
+ ;; personal data + system config
253
+ (deny file-read*
254
+ (subpath "/Users")
255
+ (subpath "/etc")
256
+ (subpath "/private/etc")
257
+ (subpath "/var/log")
258
+ (subpath "/private/var/log")
259
+ (subpath "/var/run")
260
+ (subpath "/private/var/run")
261
+ (subpath "/Volumes")
262
+ (subpath "/Library/Keychains")
263
+ (subpath "/Library/Preferences")
264
+ (subpath "/Library/Application Support")
265
+ (subpath "/Library/Cookies")
266
+ (subpath "/Library/Mail")
267
+ (subpath "/Library/Messages")
268
+ (subpath "/Library/Safari")
269
+ (subpath "/private/tmp")
270
+ (subpath "/tmp"))
271
+ (allow file-read* (subpath "${workspaceDir}"))
272
+
273
+ ;; writes constrained to workspace
274
+ (deny file-write*)
275
+ (allow file-write* (subpath "${workspaceDir}"))
276
+
277
+ ;; network
278
+ ${opts.allowNet ? "(allow network*)" : "(deny network*)"}
279
+ `;
280
+ }
281
+
282
+ function buildBwrapArgs(
283
+ workspaceDir: string,
284
+ opts: SandboxWrapOptions
285
+ ): string[] {
286
+ assertSafeWorkspacePath(workspaceDir);
287
+ const bwrapCwd = opts.bwrapCwd ?? "/workspace";
288
+ assertSafeBwrapCwd(bwrapCwd);
289
+ return [
290
+ "--die-with-parent",
291
+ "--new-session",
292
+ "--unshare-user",
293
+ "--unshare-pid",
294
+ "--unshare-ipc",
295
+ "--unshare-uts",
296
+ "--unshare-cgroup-try",
297
+ ...(opts.allowNet ? ["--share-net"] : ["--unshare-net"]),
298
+ "--bind",
299
+ workspaceDir,
300
+ "/workspace",
301
+ "--chdir",
302
+ bwrapCwd,
303
+ "--ro-bind",
304
+ "/usr",
305
+ "/usr",
306
+ "--ro-bind-try",
307
+ "/lib",
308
+ "/lib",
309
+ "--ro-bind-try",
310
+ "/lib64",
311
+ "/lib64",
312
+ "--ro-bind-try",
313
+ "/bin",
314
+ "/bin",
315
+ "--ro-bind-try",
316
+ "/sbin",
317
+ "/sbin",
318
+ "--ro-bind-try",
319
+ "/nix",
320
+ "/nix",
321
+ "--ro-bind-try",
322
+ "/etc/resolv.conf",
323
+ "/etc/resolv.conf",
324
+ "--ro-bind-try",
325
+ "/etc/ssl",
326
+ "/etc/ssl",
327
+ "--ro-bind-try",
328
+ "/etc/ca-certificates",
329
+ "/etc/ca-certificates",
330
+ "--proc",
331
+ "/proc",
332
+ "--dev",
333
+ "/dev",
334
+ "--tmpfs",
335
+ "/tmp",
336
+ ];
337
+ }
338
+
339
+ /**
340
+ * Wrap a `(command, args)` invocation with the active sandbox strategy.
341
+ * Pass-through for `kind: "none"`. The returned shape is what should be handed
342
+ * to `child_process.execFile`.
343
+ */
344
+ export function wrapInvocation(
345
+ strategy: SandboxStrategy,
346
+ inner: { command: string; args: string[] },
347
+ opts: SandboxWrapOptions
348
+ ): { command: string; args: string[] } {
349
+ if (strategy.kind === "none" || !strategy.path) return inner;
350
+
351
+ if (strategy.kind === "sandbox-exec") {
352
+ const profile = buildSandboxExecProfile(opts.workspaceDir, opts);
353
+ return {
354
+ command: strategy.path,
355
+ args: ["-p", profile, inner.command, ...inner.args],
356
+ };
357
+ }
358
+
359
+ if (strategy.kind === "bwrap") {
360
+ return {
361
+ command: strategy.path,
362
+ args: [
363
+ ...buildBwrapArgs(opts.workspaceDir, opts),
364
+ "--",
365
+ inner.command,
366
+ ...inner.args,
367
+ ],
368
+ };
369
+ }
370
+
371
+ return inner;
372
+ }