@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.
- package/dist/core/error-handler.d.ts +0 -4
- package/dist/core/error-handler.d.ts.map +1 -1
- package/dist/core/error-handler.js +4 -15
- package/dist/core/error-handler.js.map +1 -1
- package/dist/core/types.d.ts +1 -19
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +0 -4
- package/dist/core/types.js.map +1 -1
- package/dist/core/workspace.d.ts +2 -11
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +14 -36
- package/dist/core/workspace.js.map +1 -1
- package/dist/embedded/just-bash-bootstrap.d.ts.map +1 -1
- package/dist/embedded/just-bash-bootstrap.js +60 -6
- package/dist/embedded/just-bash-bootstrap.js.map +1 -1
- package/dist/embedded/mcp-cli-commands.d.ts.map +1 -1
- package/dist/embedded/mcp-cli-commands.js +3 -38
- package/dist/embedded/mcp-cli-commands.js.map +1 -1
- package/dist/gateway/gateway-integration.js +4 -4
- package/dist/gateway/gateway-integration.js.map +1 -1
- package/dist/gateway/message-batcher.d.ts.map +1 -1
- package/dist/gateway/message-batcher.js +3 -5
- package/dist/gateway/message-batcher.js.map +1 -1
- package/dist/gateway/sse-client.d.ts +1 -0
- package/dist/gateway/sse-client.d.ts.map +1 -1
- package/dist/gateway/sse-client.js +52 -8
- package/dist/gateway/sse-client.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -24
- package/dist/index.js.map +1 -1
- package/dist/instructions/builder.d.ts.map +1 -1
- package/dist/instructions/builder.js +2 -1
- package/dist/instructions/builder.js.map +1 -1
- package/dist/openclaw/plugin-loader.d.ts.map +1 -1
- package/dist/openclaw/plugin-loader.js +8 -19
- package/dist/openclaw/plugin-loader.js.map +1 -1
- package/dist/openclaw/processor.d.ts.map +1 -1
- package/dist/openclaw/processor.js +2 -0
- package/dist/openclaw/processor.js.map +1 -1
- package/dist/openclaw/sandbox-leak.d.ts.map +1 -1
- package/dist/openclaw/sandbox-leak.js +1 -6
- package/dist/openclaw/sandbox-leak.js.map +1 -1
- package/dist/openclaw/session-context.d.ts.map +1 -1
- package/dist/openclaw/session-context.js +3 -0
- package/dist/openclaw/session-context.js.map +1 -1
- package/dist/openclaw/tool-policy.d.ts.map +1 -1
- package/dist/openclaw/tool-policy.js +5 -11
- package/dist/openclaw/tool-policy.js.map +1 -1
- package/dist/openclaw/worker.d.ts +0 -1
- package/dist/openclaw/worker.d.ts.map +1 -1
- package/dist/openclaw/worker.js +19 -85
- package/dist/openclaw/worker.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -40
- package/dist/server.js.map +1 -1
- package/dist/shared/audio-provider-suggestions.d.ts.map +1 -1
- package/dist/shared/audio-provider-suggestions.js +4 -6
- package/dist/shared/audio-provider-suggestions.js.map +1 -1
- package/dist/shared/tool-implementations.d.ts.map +1 -1
- package/dist/shared/tool-implementations.js +99 -37
- package/dist/shared/tool-implementations.js.map +1 -1
- package/package.json +14 -4
- package/src/__tests__/audio-provider-suggestions.test.ts +199 -0
- package/src/__tests__/custom-tools.test.ts +92 -0
- package/src/__tests__/embedded-just-bash-bootstrap.test.ts +128 -0
- package/src/__tests__/embedded-mcp-cli-bash.test.ts +179 -0
- package/src/__tests__/embedded-tools.test.ts +744 -0
- package/src/__tests__/exec-sandbox-extra.test.ts +0 -0
- package/src/__tests__/exec-sandbox.test.ts +550 -0
- package/src/__tests__/generated-media.test.ts +142 -0
- package/src/__tests__/instructions.test.ts +60 -0
- package/src/__tests__/mcp-cli-commands-extra.test.ts +478 -0
- package/src/__tests__/mcp-cli-commands.test.ts +383 -0
- package/src/__tests__/mcp-tool-call.test.ts +423 -0
- package/src/__tests__/memory-flush-harden.test.ts +367 -0
- package/src/__tests__/memory-flush-runtime.test.ts +138 -0
- package/src/__tests__/memory-flush.test.ts +64 -0
- package/src/__tests__/message-batcher.test.ts +247 -0
- package/src/__tests__/model-resolver-harden.test.ts +197 -0
- package/src/__tests__/model-resolver.test.ts +156 -0
- package/src/__tests__/processor-harden.test.ts +259 -0
- package/src/__tests__/processor.test.ts +225 -0
- package/src/__tests__/replace-base-prompt-identity.test.ts +41 -0
- package/src/__tests__/sandbox-leak-harden.test.ts +200 -0
- package/src/__tests__/sandbox-leak.test.ts +167 -0
- package/src/__tests__/setup.ts +102 -0
- package/src/__tests__/sse-client-harden.test.ts +588 -0
- package/src/__tests__/sse-client.test.ts +90 -0
- package/src/__tests__/tool-implementations.test.ts +196 -0
- package/src/__tests__/tool-policy-edge-cases.test.ts +263 -0
- package/src/__tests__/tool-policy.test.ts +269 -0
- package/src/__tests__/worker.test.ts +89 -0
- package/src/core/error-handler.ts +47 -0
- package/src/core/project-scanner.ts +65 -0
- package/src/core/types.ts +94 -0
- package/src/core/workspace.ts +66 -0
- package/src/embedded/exec-sandbox.ts +372 -0
- package/src/embedded/just-bash-bootstrap.ts +575 -0
- package/src/embedded/mcp-cli-commands.ts +405 -0
- package/src/gateway/gateway-integration.ts +298 -0
- package/src/gateway/message-batcher.ts +123 -0
- package/src/gateway/sse-client.ts +988 -0
- package/src/gateway/types.ts +68 -0
- package/src/index.ts +123 -0
- package/src/instructions/builder.ts +44 -0
- package/src/instructions/providers.ts +27 -0
- package/src/modules/lifecycle.ts +92 -0
- package/src/openclaw/custom-tools.ts +315 -0
- package/src/openclaw/instructions.ts +36 -0
- package/src/openclaw/model-resolver.ts +150 -0
- package/src/openclaw/plugin-loader.ts +423 -0
- package/src/openclaw/processor.ts +199 -0
- package/src/openclaw/sandbox-leak.ts +100 -0
- package/src/openclaw/session-context.ts +323 -0
- package/src/openclaw/tool-policy.ts +241 -0
- package/src/openclaw/tools.ts +277 -0
- package/src/openclaw/worker.ts +1836 -0
- package/src/server.ts +330 -0
- package/src/shared/audio-provider-suggestions.ts +130 -0
- package/src/shared/processor-utils.ts +33 -0
- package/src/shared/provider-auth-hints.ts +68 -0
- package/src/shared/tool-display-config.ts +75 -0
- package/src/shared/tool-implementations.ts +981 -0
- package/src/shared/worker-env-keys.ts +8 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
type BashCommandPolicy,
|
|
4
|
+
buildToolPolicy,
|
|
5
|
+
enforceBashCommandPolicy,
|
|
6
|
+
isToolAllowedByPolicy,
|
|
7
|
+
normalizeToolList,
|
|
8
|
+
} from "../openclaw/tool-policy";
|
|
9
|
+
|
|
10
|
+
describe("normalizeToolList", () => {
|
|
11
|
+
test("returns empty array for undefined", () => {
|
|
12
|
+
expect(normalizeToolList(undefined)).toEqual([]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("returns empty array for empty string", () => {
|
|
16
|
+
expect(normalizeToolList("")).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("splits comma-separated string", () => {
|
|
20
|
+
expect(normalizeToolList("read,write,edit")).toEqual([
|
|
21
|
+
"read",
|
|
22
|
+
"write",
|
|
23
|
+
"edit",
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("splits newline-separated string", () => {
|
|
28
|
+
expect(normalizeToolList("read\nwrite\nedit")).toEqual([
|
|
29
|
+
"read",
|
|
30
|
+
"write",
|
|
31
|
+
"edit",
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("trims whitespace from entries", () => {
|
|
36
|
+
expect(normalizeToolList(" read , write , edit ")).toEqual([
|
|
37
|
+
"read",
|
|
38
|
+
"write",
|
|
39
|
+
"edit",
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("filters empty entries", () => {
|
|
44
|
+
expect(normalizeToolList("read,,write,,")).toEqual(["read", "write"]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("passes through arrays", () => {
|
|
48
|
+
expect(normalizeToolList(["read", "write"])).toEqual(["read", "write"]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("buildToolPolicy", () => {
|
|
53
|
+
test("returns default policy with no inputs", () => {
|
|
54
|
+
const policy = buildToolPolicy({});
|
|
55
|
+
expect(policy.allowedPatterns).toEqual([]);
|
|
56
|
+
expect(policy.deniedPatterns).toEqual([]);
|
|
57
|
+
expect(policy.strictMode).toBe(false);
|
|
58
|
+
expect(policy.bashPolicy.allowAll).toBe(false);
|
|
59
|
+
expect(policy.bashPolicy.allowPrefixes).toEqual([]);
|
|
60
|
+
expect(policy.bashPolicy.denyPrefixes).toContain("apt-get ");
|
|
61
|
+
expect(policy.bashPolicy.denyPrefixes).toContain("nix-shell ");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("merges toolsConfig with params", () => {
|
|
65
|
+
const policy = buildToolPolicy({
|
|
66
|
+
toolsConfig: { allowedTools: ["Read"], deniedTools: ["Write"] },
|
|
67
|
+
allowedTools: "Edit",
|
|
68
|
+
disallowedTools: "Bash",
|
|
69
|
+
});
|
|
70
|
+
expect(policy.allowedPatterns).toEqual(["Read", "Edit"]);
|
|
71
|
+
expect(policy.deniedPatterns).toEqual(["Write", "Bash"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("sets strictMode from toolsConfig", () => {
|
|
75
|
+
const policy = buildToolPolicy({
|
|
76
|
+
toolsConfig: { strictMode: true },
|
|
77
|
+
});
|
|
78
|
+
expect(policy.strictMode).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("extracts Bash allow prefixes", () => {
|
|
82
|
+
const policy = buildToolPolicy({
|
|
83
|
+
allowedTools: ["Bash(npm:*)", "Bash(git:*)"],
|
|
84
|
+
});
|
|
85
|
+
expect(policy.bashPolicy.allowPrefixes).toEqual(["npm", "git"]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("extracts Bash deny prefixes", () => {
|
|
89
|
+
const policy = buildToolPolicy({
|
|
90
|
+
disallowedTools: ["Bash(rm:*)"],
|
|
91
|
+
});
|
|
92
|
+
expect(policy.bashPolicy.denyPrefixes).toContain("rm");
|
|
93
|
+
expect(policy.bashPolicy.denyPrefixes).toContain("apt ");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("detects bash allowAll when Bash is in allowed patterns", () => {
|
|
97
|
+
const policy = buildToolPolicy({ allowedTools: ["Bash", "Read"] });
|
|
98
|
+
expect(policy.bashPolicy.allowAll).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("wildcard * enables bash allowAll", () => {
|
|
102
|
+
const policy = buildToolPolicy({ allowedTools: ["*"] });
|
|
103
|
+
expect(policy.bashPolicy.allowAll).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("isToolAllowedByPolicy", () => {
|
|
108
|
+
test("allows all tools in non-strict mode", () => {
|
|
109
|
+
const policy = buildToolPolicy({});
|
|
110
|
+
expect(isToolAllowedByPolicy("Read", policy)).toBe(true);
|
|
111
|
+
expect(isToolAllowedByPolicy("Write", policy)).toBe(true);
|
|
112
|
+
expect(isToolAllowedByPolicy("CustomTool", policy)).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("denies explicitly denied tools", () => {
|
|
116
|
+
const policy = buildToolPolicy({ disallowedTools: ["Write"] });
|
|
117
|
+
expect(isToolAllowedByPolicy("Write", policy)).toBe(false);
|
|
118
|
+
expect(isToolAllowedByPolicy("Read", policy)).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("allows bash in non-strict mode even without explicit allow", () => {
|
|
122
|
+
const policy = buildToolPolicy({});
|
|
123
|
+
expect(isToolAllowedByPolicy("Bash", policy)).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("blocks bash in strict mode without explicit allow", () => {
|
|
127
|
+
const policy = buildToolPolicy({
|
|
128
|
+
toolsConfig: { strictMode: true },
|
|
129
|
+
});
|
|
130
|
+
expect(isToolAllowedByPolicy("Bash", policy)).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("allows bash in strict mode with explicit allow", () => {
|
|
134
|
+
const policy = buildToolPolicy({
|
|
135
|
+
toolsConfig: { strictMode: true, allowedTools: ["Bash"] },
|
|
136
|
+
});
|
|
137
|
+
expect(isToolAllowedByPolicy("Bash", policy)).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("allows bash in strict mode with command allowlist", () => {
|
|
141
|
+
const policy = buildToolPolicy({
|
|
142
|
+
toolsConfig: { strictMode: true },
|
|
143
|
+
allowedTools: ["Bash(npm:*)"],
|
|
144
|
+
});
|
|
145
|
+
expect(isToolAllowedByPolicy("Bash", policy)).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("blocks non-allowed tools in strict mode", () => {
|
|
149
|
+
const policy = buildToolPolicy({
|
|
150
|
+
toolsConfig: { strictMode: true, allowedTools: ["Read"] },
|
|
151
|
+
});
|
|
152
|
+
expect(isToolAllowedByPolicy("Read", policy)).toBe(true);
|
|
153
|
+
expect(isToolAllowedByPolicy("Write", policy)).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("wildcard in allowed patterns allows all tools", () => {
|
|
157
|
+
const policy = buildToolPolicy({
|
|
158
|
+
toolsConfig: { strictMode: true, allowedTools: ["*"] },
|
|
159
|
+
});
|
|
160
|
+
expect(isToolAllowedByPolicy("AnythingGoes", policy)).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("case-insensitive tool matching", () => {
|
|
164
|
+
const policy = buildToolPolicy({ disallowedTools: ["write"] });
|
|
165
|
+
expect(isToolAllowedByPolicy("Write", policy)).toBe(false);
|
|
166
|
+
expect(isToolAllowedByPolicy("WRITE", policy)).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("Bash filters in deny list do not block non-Bash tool matching", () => {
|
|
170
|
+
// Bash(rm:*) should only affect bash command filtering, not block the Bash tool itself
|
|
171
|
+
const policy = buildToolPolicy({ disallowedTools: ["Bash(rm:*)"] });
|
|
172
|
+
expect(isToolAllowedByPolicy("Bash", policy)).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("enforceBashCommandPolicy", () => {
|
|
177
|
+
test("allows empty command", () => {
|
|
178
|
+
const policy: BashCommandPolicy = {
|
|
179
|
+
allowAll: false,
|
|
180
|
+
allowPrefixes: [],
|
|
181
|
+
denyPrefixes: [],
|
|
182
|
+
};
|
|
183
|
+
expect(() => enforceBashCommandPolicy("", policy)).not.toThrow();
|
|
184
|
+
expect(() => enforceBashCommandPolicy(" ", policy)).not.toThrow();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("throws on denied prefix match", () => {
|
|
188
|
+
const policy: BashCommandPolicy = {
|
|
189
|
+
allowAll: true,
|
|
190
|
+
allowPrefixes: [],
|
|
191
|
+
denyPrefixes: ["rm"],
|
|
192
|
+
};
|
|
193
|
+
expect(() => enforceBashCommandPolicy("rm -rf /", policy)).toThrow(
|
|
194
|
+
"Bash command denied by policy"
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("deny check is case-insensitive", () => {
|
|
199
|
+
const policy: BashCommandPolicy = {
|
|
200
|
+
allowAll: true,
|
|
201
|
+
allowPrefixes: [],
|
|
202
|
+
denyPrefixes: ["rm"],
|
|
203
|
+
};
|
|
204
|
+
expect(() => enforceBashCommandPolicy("RM -rf /", policy)).toThrow(
|
|
205
|
+
"Bash command denied by policy"
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("package manager commands are blocked", () => {
|
|
210
|
+
const policy = buildToolPolicy({});
|
|
211
|
+
expect(() =>
|
|
212
|
+
enforceBashCommandPolicy("apt-get install -y ffmpeg", policy.bashPolicy)
|
|
213
|
+
).toThrow("Bash command denied by policy");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("allows all when allowAll is true", () => {
|
|
217
|
+
const policy: BashCommandPolicy = {
|
|
218
|
+
allowAll: true,
|
|
219
|
+
allowPrefixes: [],
|
|
220
|
+
denyPrefixes: [],
|
|
221
|
+
};
|
|
222
|
+
expect(() =>
|
|
223
|
+
enforceBashCommandPolicy("any command here", policy)
|
|
224
|
+
).not.toThrow();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("allows when no allow prefixes (no filter)", () => {
|
|
228
|
+
const policy: BashCommandPolicy = {
|
|
229
|
+
allowAll: false,
|
|
230
|
+
allowPrefixes: [],
|
|
231
|
+
denyPrefixes: [],
|
|
232
|
+
};
|
|
233
|
+
expect(() =>
|
|
234
|
+
enforceBashCommandPolicy("some command", policy)
|
|
235
|
+
).not.toThrow();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("allows commands matching allow prefixes", () => {
|
|
239
|
+
const policy: BashCommandPolicy = {
|
|
240
|
+
allowAll: false,
|
|
241
|
+
allowPrefixes: ["npm", "git"],
|
|
242
|
+
denyPrefixes: [],
|
|
243
|
+
};
|
|
244
|
+
expect(() => enforceBashCommandPolicy("npm install", policy)).not.toThrow();
|
|
245
|
+
expect(() => enforceBashCommandPolicy("git status", policy)).not.toThrow();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("rejects commands not matching allow prefixes", () => {
|
|
249
|
+
const policy: BashCommandPolicy = {
|
|
250
|
+
allowAll: false,
|
|
251
|
+
allowPrefixes: ["npm", "git"],
|
|
252
|
+
denyPrefixes: [],
|
|
253
|
+
};
|
|
254
|
+
expect(() =>
|
|
255
|
+
enforceBashCommandPolicy("curl http://example.com", policy)
|
|
256
|
+
).toThrow("Bash command not allowed by policy");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("deny takes priority over allow", () => {
|
|
260
|
+
const policy: BashCommandPolicy = {
|
|
261
|
+
allowAll: false,
|
|
262
|
+
allowPrefixes: ["rm"],
|
|
263
|
+
denyPrefixes: ["rm"],
|
|
264
|
+
};
|
|
265
|
+
expect(() => enforceBashCommandPolicy("rm file.txt", policy)).toThrow(
|
|
266
|
+
"Bash command denied by policy"
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for OpenClawWorker constructor validation and basic setup.
|
|
3
|
+
* Full execution tests require the OpenClaw runtime and are covered
|
|
4
|
+
* by integration tests via test-bot.sh.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
8
|
+
import { OpenClawWorker } from "../openclaw/worker";
|
|
9
|
+
import { mockWorkerConfig, TestHelpers } from "./setup";
|
|
10
|
+
|
|
11
|
+
describe("OpenClawWorker", () => {
|
|
12
|
+
let restoreFetch: () => void;
|
|
13
|
+
let originalDispatcherUrl: string | undefined;
|
|
14
|
+
let originalWorkerToken: string | undefined;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
originalDispatcherUrl = process.env.DISPATCHER_URL;
|
|
18
|
+
originalWorkerToken = process.env.WORKER_TOKEN;
|
|
19
|
+
process.env.DISPATCHER_URL = "https://test-dispatcher.example.com";
|
|
20
|
+
process.env.WORKER_TOKEN = "test-worker-token";
|
|
21
|
+
restoreFetch = TestHelpers.mockFetch();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
restoreFetch();
|
|
26
|
+
if (originalDispatcherUrl) {
|
|
27
|
+
process.env.DISPATCHER_URL = originalDispatcherUrl;
|
|
28
|
+
} else {
|
|
29
|
+
delete process.env.DISPATCHER_URL;
|
|
30
|
+
}
|
|
31
|
+
if (originalWorkerToken) {
|
|
32
|
+
process.env.WORKER_TOKEN = originalWorkerToken;
|
|
33
|
+
} else {
|
|
34
|
+
delete process.env.WORKER_TOKEN;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("constructor requires DISPATCHER_URL", () => {
|
|
39
|
+
const original = process.env.DISPATCHER_URL;
|
|
40
|
+
delete process.env.DISPATCHER_URL;
|
|
41
|
+
|
|
42
|
+
expect(
|
|
43
|
+
() => new OpenClawWorker({ ...mockWorkerConfig, sessionKey: "missing" })
|
|
44
|
+
).toThrow(
|
|
45
|
+
"DISPATCHER_URL and WORKER_TOKEN environment variables are required"
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
process.env.DISPATCHER_URL = original;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("constructor requires WORKER_TOKEN", () => {
|
|
52
|
+
const original = process.env.WORKER_TOKEN;
|
|
53
|
+
delete process.env.WORKER_TOKEN;
|
|
54
|
+
|
|
55
|
+
expect(
|
|
56
|
+
() => new OpenClawWorker({ ...mockWorkerConfig, sessionKey: "missing" })
|
|
57
|
+
).toThrow(
|
|
58
|
+
"DISPATCHER_URL and WORKER_TOKEN environment variables are required"
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
process.env.WORKER_TOKEN = original;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("constructor requires teamId", () => {
|
|
65
|
+
expect(
|
|
66
|
+
() => new OpenClawWorker({ ...mockWorkerConfig, teamId: undefined })
|
|
67
|
+
).toThrow("teamId is required for worker initialization");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("constructor requires conversationId", () => {
|
|
71
|
+
expect(
|
|
72
|
+
() =>
|
|
73
|
+
new OpenClawWorker({
|
|
74
|
+
...mockWorkerConfig,
|
|
75
|
+
conversationId: undefined as any,
|
|
76
|
+
})
|
|
77
|
+
).toThrow("conversationId is required for worker initialization");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("getWorkerTransport returns transport after construction", () => {
|
|
81
|
+
const worker = new OpenClawWorker(mockWorkerConfig);
|
|
82
|
+
expect(worker.getWorkerTransport()).not.toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("cleanup completes without error", async () => {
|
|
86
|
+
const worker = new OpenClawWorker(mockWorkerConfig);
|
|
87
|
+
await expect(worker.cleanup()).resolves.toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createLogger, type WorkerTransport } from "@lobu/core";
|
|
2
|
+
|
|
3
|
+
const logger = createLogger("worker");
|
|
4
|
+
|
|
5
|
+
function formatErrorMessage(error: unknown): string {
|
|
6
|
+
if (!(error instanceof Error)) {
|
|
7
|
+
return `💥 Worker crashed: Unknown error`;
|
|
8
|
+
}
|
|
9
|
+
const name = error.constructor.name;
|
|
10
|
+
const isGeneric = name === "Error" || name === "WorkspaceError";
|
|
11
|
+
return isGeneric
|
|
12
|
+
? `💥 Worker crashed: ${error.message}`
|
|
13
|
+
: `💥 Worker crashed (${name}): ${error.message}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function classifyError(error: unknown): string | undefined {
|
|
17
|
+
if (!(error instanceof Error)) return undefined;
|
|
18
|
+
if (
|
|
19
|
+
error.message.includes("No model configured") ||
|
|
20
|
+
error.message.includes("No provider specified")
|
|
21
|
+
)
|
|
22
|
+
return "NO_MODEL_CONFIGURED";
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function handleExecutionError(
|
|
27
|
+
error: unknown,
|
|
28
|
+
transport: WorkerTransport
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
logger.error("Worker execution failed:", error);
|
|
31
|
+
|
|
32
|
+
const code = classifyError(error);
|
|
33
|
+
const errorInstance =
|
|
34
|
+
error instanceof Error ? error : new Error(String(error));
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
if (code) {
|
|
38
|
+
await transport.signalError(errorInstance, code);
|
|
39
|
+
} else {
|
|
40
|
+
await transport.sendStreamDelta(formatErrorMessage(error), true, true);
|
|
41
|
+
await transport.signalError(errorInstance);
|
|
42
|
+
}
|
|
43
|
+
} catch (gatewayError) {
|
|
44
|
+
logger.error("Failed to send error via gateway:", gatewayError);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Scan a directory tree and find all project directories
|
|
5
|
+
* A project directory is one that contains a build config file
|
|
6
|
+
* (Makefile, package.json, pyproject.toml, etc.)
|
|
7
|
+
*
|
|
8
|
+
* Generic utility that works for any AI agent
|
|
9
|
+
*/
|
|
10
|
+
export function listAppDirectories(rootDirectory: string): string[] {
|
|
11
|
+
const foundDirectories: string[] = [];
|
|
12
|
+
const ignored = new Set([
|
|
13
|
+
"node_modules",
|
|
14
|
+
".git",
|
|
15
|
+
".next",
|
|
16
|
+
"dist",
|
|
17
|
+
"build",
|
|
18
|
+
"vendor",
|
|
19
|
+
"target",
|
|
20
|
+
".venv",
|
|
21
|
+
"venv",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const buildConfigFiles = new Set([
|
|
25
|
+
"Makefile",
|
|
26
|
+
"makefile",
|
|
27
|
+
"package.json",
|
|
28
|
+
"pyproject.toml",
|
|
29
|
+
"Cargo.toml",
|
|
30
|
+
"pom.xml",
|
|
31
|
+
"build.gradle",
|
|
32
|
+
"build.gradle.kts",
|
|
33
|
+
"CMakeLists.txt",
|
|
34
|
+
"go.mod",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const walk = (dir: string): void => {
|
|
38
|
+
let entries: fs.Dirent[] = [];
|
|
39
|
+
try {
|
|
40
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
41
|
+
} catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check if current directory has any build config files
|
|
46
|
+
const hasConfigFile = entries.some(
|
|
47
|
+
(entry) => entry.isFile() && buildConfigFiles.has(entry.name)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (hasConfigFile) {
|
|
51
|
+
foundDirectories.push(dir);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Recursively walk subdirectories
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const p = `${dir}/${entry.name}`;
|
|
57
|
+
if (entry.isDirectory() && !ignored.has(entry.name)) {
|
|
58
|
+
walk(p);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
walk(rootDirectory);
|
|
64
|
+
return foundDirectories;
|
|
65
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import type { WorkerTransport } from "@lobu/core";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Interface for worker executors. Allows different agent implementations.
|
|
7
|
+
*/
|
|
8
|
+
export interface WorkerExecutor {
|
|
9
|
+
execute(): Promise<void>;
|
|
10
|
+
cleanup(): Promise<void>;
|
|
11
|
+
getWorkerTransport(): WorkerTransport | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface WorkerConfig {
|
|
15
|
+
sessionKey: string;
|
|
16
|
+
userId: string;
|
|
17
|
+
agentId: string; // Space identifier for multi-tenant isolation
|
|
18
|
+
channelId: string;
|
|
19
|
+
conversationId: string;
|
|
20
|
+
userPrompt: string; // Base64 encoded
|
|
21
|
+
responseChannel: string; // Platform-agnostic response channel
|
|
22
|
+
responseId: string; // Platform-agnostic response message ID
|
|
23
|
+
botResponseId?: string; // Bot's response message ID for updates
|
|
24
|
+
agentOptions: string; // JSON string
|
|
25
|
+
teamId?: string; // Platform team/workspace ID (e.g., Slack team ID)
|
|
26
|
+
platform: string; // Platform identifier (e.g., "slack", "discord")
|
|
27
|
+
platformMetadata?: any; // Platform-specific metadata (e.g., files, user info)
|
|
28
|
+
workspace: {
|
|
29
|
+
baseDirectory: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface WorkspaceSetupConfig {
|
|
34
|
+
baseDirectory: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WorkspaceInfo {
|
|
38
|
+
baseDirectory: string;
|
|
39
|
+
userDirectory: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Progress update from AI agent execution
|
|
44
|
+
*/
|
|
45
|
+
export type ProgressUpdate =
|
|
46
|
+
| {
|
|
47
|
+
type: "output";
|
|
48
|
+
data: unknown; // Agent-specific message format
|
|
49
|
+
timestamp: number;
|
|
50
|
+
}
|
|
51
|
+
| {
|
|
52
|
+
type: "completion";
|
|
53
|
+
data: {
|
|
54
|
+
exitCode?: number;
|
|
55
|
+
message?: string;
|
|
56
|
+
success?: boolean;
|
|
57
|
+
sessionId?: string;
|
|
58
|
+
};
|
|
59
|
+
timestamp: number;
|
|
60
|
+
}
|
|
61
|
+
| {
|
|
62
|
+
type: "error";
|
|
63
|
+
data: Error | { message?: string; stack?: string; error?: string };
|
|
64
|
+
timestamp: number;
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
type: "status_update";
|
|
68
|
+
data: {
|
|
69
|
+
elapsedSeconds: number;
|
|
70
|
+
state: string;
|
|
71
|
+
};
|
|
72
|
+
timestamp: number;
|
|
73
|
+
}
|
|
74
|
+
| {
|
|
75
|
+
type: "custom_event";
|
|
76
|
+
data: {
|
|
77
|
+
name: string;
|
|
78
|
+
payload: Record<string, unknown>;
|
|
79
|
+
};
|
|
80
|
+
timestamp: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Result from session execution (includes session metadata)
|
|
85
|
+
*/
|
|
86
|
+
export interface SessionExecutionResult {
|
|
87
|
+
success: boolean;
|
|
88
|
+
exitCode: number;
|
|
89
|
+
output: string;
|
|
90
|
+
error?: string;
|
|
91
|
+
sessionKey: string;
|
|
92
|
+
persisted?: boolean;
|
|
93
|
+
storagePath?: string;
|
|
94
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import {
|
|
3
|
+
createLogger,
|
|
4
|
+
sanitizeConversationId,
|
|
5
|
+
WorkspaceError,
|
|
6
|
+
} from "@lobu/core";
|
|
7
|
+
import type { WorkspaceInfo, WorkspaceSetupConfig } from "./types";
|
|
8
|
+
|
|
9
|
+
const logger = createLogger("workspace");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Workspace layout:
|
|
13
|
+
* baseDirectory/ ← agent-level root (e.g. /workspace)
|
|
14
|
+
* baseDirectory/{conversationId}/ ← thread-specific working directory
|
|
15
|
+
*
|
|
16
|
+
* VCS operations (git, etc.) are handled by modules via hooks.
|
|
17
|
+
*/
|
|
18
|
+
export class WorkspaceManager {
|
|
19
|
+
private config: WorkspaceSetupConfig;
|
|
20
|
+
private workspaceInfo?: WorkspaceInfo;
|
|
21
|
+
|
|
22
|
+
constructor(config: WorkspaceSetupConfig) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async setupWorkspace(
|
|
27
|
+
username: string,
|
|
28
|
+
sessionKey?: string
|
|
29
|
+
): Promise<WorkspaceInfo> {
|
|
30
|
+
const conversationId =
|
|
31
|
+
process.env.CONVERSATION_ID || sessionKey || username || "default";
|
|
32
|
+
|
|
33
|
+
logger.info(
|
|
34
|
+
`Setting up workspace directory for ${username}, conversation: ${conversationId}...`
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const sanitized = sanitizeConversationId(conversationId);
|
|
38
|
+
const userDirectory = `${this.config.baseDirectory}/${sanitized}`;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await mkdir(this.config.baseDirectory, { recursive: true });
|
|
42
|
+
await mkdir(userDirectory, { recursive: true });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new WorkspaceError(
|
|
45
|
+
"setupWorkspace",
|
|
46
|
+
`Failed to setup workspace directory`,
|
|
47
|
+
error as Error
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.workspaceInfo = {
|
|
52
|
+
baseDirectory: this.config.baseDirectory,
|
|
53
|
+
userDirectory,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
logger.info(
|
|
57
|
+
`Workspace directory setup completed for ${username} (conversation: ${conversationId}) at ${userDirectory}`
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return this.workspaceInfo;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getCurrentWorkingDirectory(): string {
|
|
64
|
+
return this.workspaceInfo?.userDirectory || this.config.baseDirectory;
|
|
65
|
+
}
|
|
66
|
+
}
|