@lobu/worker 7.0.0 → 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 +34 -4
- 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/sse-client.d.ts.map +1 -1
- package/dist/gateway/sse-client.js +44 -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.map +1 -1
- package/dist/openclaw/worker.js +1 -10
- 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 +62 -24
- package/dist/shared/tool-implementations.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/processor-harden.test.ts +6 -16
- package/src/core/error-handler.ts +5 -20
- package/src/core/types.ts +1 -35
- package/src/core/workspace.ts +22 -45
- package/src/embedded/just-bash-bootstrap.ts +36 -4
- package/src/embedded/mcp-cli-commands.ts +9 -6
- package/src/gateway/sse-client.ts +45 -8
- package/src/index.ts +8 -26
- package/src/instructions/builder.ts +2 -3
- package/src/openclaw/plugin-loader.ts +15 -19
- package/src/openclaw/processor.ts +1 -0
- package/src/openclaw/sandbox-leak.ts +1 -6
- package/src/openclaw/session-context.ts +3 -0
- package/src/openclaw/tool-policy.ts +5 -12
- package/src/openclaw/worker.ts +2 -13
- package/src/server.ts +1 -5
- package/src/shared/audio-provider-suggestions.ts +4 -6
- package/src/shared/tool-implementations.ts +57 -16
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
extractTraceId,
|
|
10
10
|
flushTracing,
|
|
11
11
|
SpanStatusCode,
|
|
12
|
+
stripEnv,
|
|
12
13
|
} from "@lobu/core";
|
|
13
14
|
import { z } from "zod";
|
|
14
15
|
import type { WorkerConfig, WorkerExecutor } from "../core/types";
|
|
16
|
+
import { SENSITIVE_WORKER_ENV_KEYS } from "../shared/worker-env-keys";
|
|
15
17
|
import { HttpWorkerTransport } from "./gateway-integration";
|
|
16
18
|
import { MessageBatcher } from "./message-batcher";
|
|
17
19
|
import type { MessagePayload, QueuedMessage } from "./types";
|
|
@@ -574,16 +576,27 @@ export class GatewayClient {
|
|
|
574
576
|
});
|
|
575
577
|
|
|
576
578
|
let completed = false;
|
|
579
|
+
let sigkillTimer: NodeJS.Timeout | null = null;
|
|
577
580
|
|
|
578
581
|
try {
|
|
579
|
-
//
|
|
582
|
+
// Strip the worker's own gateway credentials before handing the shell
|
|
583
|
+
// its env. An `exec` command is an arbitrary string from the gateway
|
|
584
|
+
// that ends up under `sh -c`; leaking WORKER_TOKEN / DISPATCHER_URL
|
|
585
|
+
// into that environment would let a malicious or buggy exec impersonate
|
|
586
|
+
// the worker against its own gateway. The bash-tool and just-bash
|
|
587
|
+
// spawners already apply the same filter (see openclaw/tools.ts and
|
|
588
|
+
// embedded/just-bash-bootstrap.ts) — keep parity here.
|
|
589
|
+
const baseEnv = stripEnv(process.env, SENSITIVE_WORKER_ENV_KEYS);
|
|
580
590
|
const proc = spawn("sh", ["-c", execCommand], {
|
|
581
591
|
cwd: workingDir,
|
|
582
|
-
env: { ...
|
|
592
|
+
env: { ...baseEnv, ...execEnv },
|
|
583
593
|
stdio: ["ignore", "pipe", "pipe"],
|
|
584
594
|
});
|
|
585
595
|
|
|
586
|
-
// Setup timeout
|
|
596
|
+
// Setup timeout. The SIGKILL escalation timer is tracked so the `close`
|
|
597
|
+
// handler can clear it when the child exits between SIGTERM and SIGKILL;
|
|
598
|
+
// otherwise the timer pins the event loop for an extra 5s after every
|
|
599
|
+
// timed-out exec and (worse) leaks if `close`/`error` never fires.
|
|
587
600
|
const timeoutId = setTimeout(() => {
|
|
588
601
|
if (!completed) {
|
|
589
602
|
logger.warn(
|
|
@@ -591,7 +604,8 @@ export class GatewayClient {
|
|
|
591
604
|
"Exec timeout reached, killing process"
|
|
592
605
|
);
|
|
593
606
|
proc.kill("SIGTERM");
|
|
594
|
-
setTimeout(() => {
|
|
607
|
+
sigkillTimer = setTimeout(() => {
|
|
608
|
+
sigkillTimer = null;
|
|
595
609
|
if (!completed) {
|
|
596
610
|
proc.kill("SIGKILL");
|
|
597
611
|
}
|
|
@@ -600,7 +614,7 @@ export class GatewayClient {
|
|
|
600
614
|
}, timeout);
|
|
601
615
|
|
|
602
616
|
// Stream stdout
|
|
603
|
-
|
|
617
|
+
const onStdout = (chunk: Buffer) => {
|
|
604
618
|
const content = chunk.toString();
|
|
605
619
|
transport.sendExecOutput(execId, "stdout", content).catch((err) => {
|
|
606
620
|
logger.error(
|
|
@@ -608,10 +622,11 @@ export class GatewayClient {
|
|
|
608
622
|
"Failed to send stdout"
|
|
609
623
|
);
|
|
610
624
|
});
|
|
611
|
-
}
|
|
625
|
+
};
|
|
626
|
+
proc.stdout?.on("data", onStdout);
|
|
612
627
|
|
|
613
628
|
// Stream stderr
|
|
614
|
-
|
|
629
|
+
const onStderr = (chunk: Buffer) => {
|
|
615
630
|
const content = chunk.toString();
|
|
616
631
|
transport.sendExecOutput(execId, "stderr", content).catch((err) => {
|
|
617
632
|
logger.error(
|
|
@@ -619,19 +634,34 @@ export class GatewayClient {
|
|
|
619
634
|
"Failed to send stderr"
|
|
620
635
|
);
|
|
621
636
|
});
|
|
622
|
-
}
|
|
637
|
+
};
|
|
638
|
+
proc.stderr?.on("data", onStderr);
|
|
623
639
|
|
|
624
640
|
// Wait for process to complete
|
|
625
641
|
const exitCode = await new Promise<number>((resolve, reject) => {
|
|
626
642
|
proc.on("close", (code) => {
|
|
627
643
|
completed = true;
|
|
628
644
|
clearTimeout(timeoutId);
|
|
645
|
+
if (sigkillTimer) {
|
|
646
|
+
clearTimeout(sigkillTimer);
|
|
647
|
+
sigkillTimer = null;
|
|
648
|
+
}
|
|
649
|
+
// Stop accepting late `data` events so a chunk buffered after exit
|
|
650
|
+
// can't fire `sendExecOutput` AFTER we've signalled completion.
|
|
651
|
+
proc.stdout?.removeListener("data", onStdout);
|
|
652
|
+
proc.stderr?.removeListener("data", onStderr);
|
|
629
653
|
resolve(code ?? 0);
|
|
630
654
|
});
|
|
631
655
|
|
|
632
656
|
proc.on("error", (error) => {
|
|
633
657
|
completed = true;
|
|
634
658
|
clearTimeout(timeoutId);
|
|
659
|
+
if (sigkillTimer) {
|
|
660
|
+
clearTimeout(sigkillTimer);
|
|
661
|
+
sigkillTimer = null;
|
|
662
|
+
}
|
|
663
|
+
proc.stdout?.removeListener("data", onStdout);
|
|
664
|
+
proc.stderr?.removeListener("data", onStderr);
|
|
635
665
|
reject(error);
|
|
636
666
|
});
|
|
637
667
|
});
|
|
@@ -663,6 +693,13 @@ export class GatewayClient {
|
|
|
663
693
|
|
|
664
694
|
logger.error({ traceId, execId, error: errorMessage }, "Exec failed");
|
|
665
695
|
} finally {
|
|
696
|
+
// Defensive: if we threw before `close`/`error` fired (e.g. transport
|
|
697
|
+
// throwing during sendExecOutput on a long-running child), make sure
|
|
698
|
+
// the SIGKILL escalation timer doesn't outlive this exec.
|
|
699
|
+
if (sigkillTimer) {
|
|
700
|
+
clearTimeout(sigkillTimer);
|
|
701
|
+
sigkillTimer = null;
|
|
702
|
+
}
|
|
666
703
|
this.currentJobId = undefined;
|
|
667
704
|
}
|
|
668
705
|
}
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,6 @@ import { startWorkerHttpServer, stopWorkerHttpServer } from "./server";
|
|
|
16
16
|
* Main entry point for gateway-based persistent worker
|
|
17
17
|
*/
|
|
18
18
|
async function main() {
|
|
19
|
-
// Register global rejection/exception handlers early
|
|
20
19
|
process.on("unhandledRejection", (reason) => {
|
|
21
20
|
logger.error("Unhandled rejection:", reason);
|
|
22
21
|
process.exit(1);
|
|
@@ -29,10 +28,8 @@ async function main() {
|
|
|
29
28
|
|
|
30
29
|
logger.info("Starting worker...");
|
|
31
30
|
|
|
32
|
-
// Initialize Sentry for error tracking
|
|
33
31
|
await initSentry();
|
|
34
32
|
|
|
35
|
-
// Initialize OpenTelemetry tracing for distributed tracing
|
|
36
33
|
const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
37
34
|
if (otlpEndpoint) {
|
|
38
35
|
initTracing({
|
|
@@ -42,16 +39,12 @@ async function main() {
|
|
|
42
39
|
logger.info(`Tracing initialized: lobu-worker -> ${otlpEndpoint}`);
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
// Discover and register available modules
|
|
46
42
|
await moduleRegistry.registerAvailableModules();
|
|
47
|
-
|
|
48
|
-
// Initialize all registered modules
|
|
49
43
|
await moduleRegistry.initAll();
|
|
50
44
|
logger.info("✅ Modules initialized");
|
|
51
45
|
|
|
52
46
|
logger.info("🔄 Starting in gateway mode (SSE/HTTP-based persistent worker)");
|
|
53
47
|
|
|
54
|
-
// Get user ID from environment
|
|
55
48
|
const userId = process.env.USER_ID;
|
|
56
49
|
|
|
57
50
|
if (!userId) {
|
|
@@ -62,7 +55,6 @@ async function main() {
|
|
|
62
55
|
}
|
|
63
56
|
|
|
64
57
|
try {
|
|
65
|
-
// Get required environment variables
|
|
66
58
|
const deploymentName = process.env.DEPLOYMENT_NAME;
|
|
67
59
|
const dispatcherUrl = process.env.DISPATCHER_URL;
|
|
68
60
|
const workerToken = process.env.WORKER_TOKEN;
|
|
@@ -80,11 +72,9 @@ async function main() {
|
|
|
80
72
|
process.exit(1);
|
|
81
73
|
}
|
|
82
74
|
|
|
83
|
-
// Start HTTP server before connecting to gateway
|
|
84
75
|
const httpPort = await startWorkerHttpServer();
|
|
85
76
|
logger.info(`Worker HTTP server started on port ${httpPort}`);
|
|
86
77
|
|
|
87
|
-
// Initialize gateway client directly
|
|
88
78
|
logger.info(`🚀 Starting Gateway-based Persistent Worker`);
|
|
89
79
|
logger.info(`- User ID: ${userId}`);
|
|
90
80
|
logger.info(`- Deployment: ${deploymentName}`);
|
|
@@ -100,33 +90,25 @@ async function main() {
|
|
|
100
90
|
|
|
101
91
|
// Register signal handlers before async operations
|
|
102
92
|
let isShuttingDown = false;
|
|
103
|
-
|
|
104
|
-
process.on("SIGTERM", async () => {
|
|
93
|
+
const shutdown = async (signal: NodeJS.Signals) => {
|
|
105
94
|
if (isShuttingDown) return;
|
|
106
95
|
isShuttingDown = true;
|
|
107
|
-
logger.info(
|
|
96
|
+
logger.info(`Received ${signal}, shutting down gateway worker...`);
|
|
108
97
|
await gatewayClient.stop();
|
|
109
98
|
await stopWorkerHttpServer();
|
|
110
99
|
process.exit(0);
|
|
111
|
-
}
|
|
100
|
+
};
|
|
112
101
|
|
|
113
|
-
process.on("
|
|
114
|
-
|
|
115
|
-
isShuttingDown = true;
|
|
116
|
-
logger.info("Received SIGINT, shutting down gateway worker...");
|
|
117
|
-
await gatewayClient.stop();
|
|
118
|
-
await stopWorkerHttpServer();
|
|
119
|
-
process.exit(0);
|
|
120
|
-
});
|
|
102
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
103
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
121
104
|
|
|
122
105
|
logger.info("🔌 Connecting to dispatcher...");
|
|
123
106
|
await gatewayClient.start();
|
|
124
107
|
logger.info("✅ Gateway worker started successfully");
|
|
125
108
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}); // Wait forever
|
|
109
|
+
await new Promise<never>(() => {
|
|
110
|
+
// Intentionally never resolves — block process until signal.
|
|
111
|
+
});
|
|
130
112
|
} catch (error) {
|
|
131
113
|
logger.error("❌ Gateway worker failed:", error);
|
|
132
114
|
process.exit(1);
|
|
@@ -20,10 +20,9 @@ export async function generateCustomInstructions(
|
|
|
20
20
|
context: InstructionContext
|
|
21
21
|
): Promise<string> {
|
|
22
22
|
try {
|
|
23
|
+
const sorted = [...providers].sort((a, b) => a.priority - b.priority);
|
|
23
24
|
const sections: string[] = [];
|
|
24
|
-
for (const provider of
|
|
25
|
-
(a, b) => a.priority - b.priority
|
|
26
|
-
)) {
|
|
25
|
+
for (const provider of sorted) {
|
|
27
26
|
const instructions = await provider.getInstructions(context);
|
|
28
27
|
if (instructions?.trim()) {
|
|
29
28
|
sections.push(instructions.trim());
|
|
@@ -99,16 +99,15 @@ async function loadSinglePlugin(
|
|
|
99
99
|
): Promise<LoadedPlugin | null> {
|
|
100
100
|
const { source, slot, config: pluginConfig } = config;
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
mod = (await import(source)) as Record<string, unknown>;
|
|
105
|
-
} catch (err) {
|
|
102
|
+
const mod = await import(source).catch((err) => {
|
|
106
103
|
throw new Error(
|
|
107
104
|
`Cannot import "${source}": ${err instanceof Error ? err.message : String(err)}`
|
|
108
105
|
);
|
|
109
|
-
}
|
|
106
|
+
});
|
|
110
107
|
|
|
111
|
-
const pluginEntrypoint = resolvePluginEntrypoint(
|
|
108
|
+
const pluginEntrypoint = resolvePluginEntrypoint(
|
|
109
|
+
mod as Record<string, unknown>
|
|
110
|
+
);
|
|
112
111
|
if (!pluginEntrypoint) {
|
|
113
112
|
logger.warn(`Plugin "${source}" has no registerable entrypoint - skipping`);
|
|
114
113
|
return null;
|
|
@@ -224,22 +223,19 @@ function createShimApi(params: {
|
|
|
224
223
|
cwd,
|
|
225
224
|
} = params;
|
|
226
225
|
const noop = () => {
|
|
227
|
-
|
|
226
|
+
// No-op stub for shim plugin APIs that this loader does not implement.
|
|
228
227
|
};
|
|
229
228
|
|
|
229
|
+
const prefix = `[plugin:${extractPluginName(source)}]`;
|
|
230
230
|
const shimLogger = {
|
|
231
|
-
info(message: string, ...args: unknown[])
|
|
232
|
-
logger.info(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
logger.
|
|
239
|
-
},
|
|
240
|
-
debug(message: string, ...args: unknown[]) {
|
|
241
|
-
logger.debug(`[plugin:${extractPluginName(source)}] ${message}`, ...args);
|
|
242
|
-
},
|
|
231
|
+
info: (message: string, ...args: unknown[]) =>
|
|
232
|
+
logger.info(`${prefix} ${message}`, ...args),
|
|
233
|
+
warn: (message: string, ...args: unknown[]) =>
|
|
234
|
+
logger.warn(`${prefix} ${message}`, ...args),
|
|
235
|
+
error: (message: string, ...args: unknown[]) =>
|
|
236
|
+
logger.error(`${prefix} ${message}`, ...args),
|
|
237
|
+
debug: (message: string, ...args: unknown[]) =>
|
|
238
|
+
logger.debug(`${prefix} ${message}`, ...args),
|
|
243
239
|
};
|
|
244
240
|
|
|
245
241
|
return {
|
|
@@ -89,12 +89,7 @@ export function checkSandboxLeak(
|
|
|
89
89
|
);
|
|
90
90
|
redacted = redacted.replace(
|
|
91
91
|
DELIVERY_PHRASE_RE,
|
|
92
|
-
|
|
93
|
-
// Reconstruct the phrase prefix (everything before the path) by
|
|
94
|
-
// re-matching on the original substring. Simpler: replace the whole
|
|
95
|
-
// match with a generic note.
|
|
96
|
-
return "[file was created but not uploaded — use `UploadUserFile` to deliver it]";
|
|
97
|
-
}
|
|
92
|
+
"[file was created but not uploaded — use `UploadUserFile` to deliver it]"
|
|
98
93
|
);
|
|
99
94
|
|
|
100
95
|
const note =
|
|
@@ -231,6 +231,9 @@ export async function getOpenClawSessionContext(
|
|
|
231
231
|
headers: {
|
|
232
232
|
Authorization: `Bearer ${workerToken}`,
|
|
233
233
|
},
|
|
234
|
+
// Session context is fetched once per turn; a stalled gateway here would
|
|
235
|
+
// otherwise hang the worker before the agent ever sees the prompt.
|
|
236
|
+
signal: AbortSignal.timeout(30_000),
|
|
234
237
|
});
|
|
235
238
|
|
|
236
239
|
if (!response.ok) {
|
|
@@ -86,10 +86,6 @@ export function isDirectPackageInstallCommand(command: string): boolean {
|
|
|
86
86
|
);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
function normalizePattern(pattern: string): string {
|
|
90
|
-
return pattern.trim();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
89
|
function normalizeToolName(name: string): string {
|
|
94
90
|
return name.trim().toLowerCase();
|
|
95
91
|
}
|
|
@@ -108,16 +104,13 @@ export function normalizeToolList(value?: string | string[]): string[] {
|
|
|
108
104
|
|
|
109
105
|
function parseBashFilter(pattern: string): string | null {
|
|
110
106
|
const match = pattern.match(/^Bash\(([^:]+):\*\)$/i);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
const prefix = match[1]?.trim();
|
|
115
|
-
return prefix ? prefix : null;
|
|
107
|
+
const prefix = match?.[1]?.trim();
|
|
108
|
+
return prefix || null;
|
|
116
109
|
}
|
|
117
110
|
|
|
118
111
|
function matchesToolPattern(toolName: string, pattern: string): boolean {
|
|
119
112
|
const normalizedTool = normalizeToolName(toolName);
|
|
120
|
-
const normalizedPattern =
|
|
113
|
+
const normalizedPattern = pattern.trim();
|
|
121
114
|
const normalizedPatternLower = normalizedPattern.toLowerCase();
|
|
122
115
|
|
|
123
116
|
if (normalizedPattern === "*") {
|
|
@@ -145,11 +138,11 @@ export function buildToolPolicy(params: {
|
|
|
145
138
|
const mergedAllowed = [
|
|
146
139
|
...(toolsConfig?.allowedTools ?? []),
|
|
147
140
|
...allowedPatterns,
|
|
148
|
-
].map(
|
|
141
|
+
].map((p) => p.trim());
|
|
149
142
|
const mergedDenied = [
|
|
150
143
|
...(toolsConfig?.deniedTools ?? []),
|
|
151
144
|
...deniedPatterns,
|
|
152
|
-
].map(
|
|
145
|
+
].map((p) => p.trim());
|
|
153
146
|
|
|
154
147
|
const bashAllowPrefixes = mergedAllowed
|
|
155
148
|
.map((pattern) => parseBashFilter(pattern))
|
package/src/openclaw/worker.ts
CHANGED
|
@@ -281,22 +281,20 @@ export class OpenClawWorker implements WorkerExecutor {
|
|
|
281
281
|
this.workspaceManager = new WorkspaceManager(config.workspace);
|
|
282
282
|
this.progressProcessor = new OpenClawProgressProcessor();
|
|
283
283
|
|
|
284
|
-
// Verify required environment variables
|
|
285
284
|
const gatewayUrl = process.env.DISPATCHER_URL;
|
|
286
285
|
const workerToken = process.env.WORKER_TOKEN;
|
|
287
|
-
|
|
288
286
|
if (!gatewayUrl || !workerToken) {
|
|
289
287
|
throw new Error(
|
|
290
288
|
"DISPATCHER_URL and WORKER_TOKEN environment variables are required"
|
|
291
289
|
);
|
|
292
290
|
}
|
|
293
|
-
|
|
294
291
|
if (!config.teamId) {
|
|
295
292
|
throw new Error("teamId is required for worker initialization");
|
|
296
293
|
}
|
|
297
294
|
if (!config.conversationId) {
|
|
298
295
|
throw new Error("conversationId is required for worker initialization");
|
|
299
296
|
}
|
|
297
|
+
|
|
300
298
|
this.workerTransport = new HttpWorkerTransport({
|
|
301
299
|
gatewayUrl,
|
|
302
300
|
workerToken,
|
|
@@ -327,13 +325,11 @@ export class OpenClawWorker implements WorkerExecutor {
|
|
|
327
325
|
`[TIMING] Worker execute() started at: ${new Date(executeStartTime).toISOString()}`
|
|
328
326
|
);
|
|
329
327
|
|
|
330
|
-
// Decode user prompt
|
|
331
328
|
const userPrompt = Buffer.from(this.config.userPrompt, "base64").toString(
|
|
332
329
|
"utf-8"
|
|
333
330
|
);
|
|
334
331
|
logger.info(`User prompt: ${userPrompt.substring(0, 100)}...`);
|
|
335
332
|
|
|
336
|
-
// Setup workspace
|
|
337
333
|
logger.info("Setting up workspace...");
|
|
338
334
|
|
|
339
335
|
await Sentry.startSpan(
|
|
@@ -360,13 +356,9 @@ export class OpenClawWorker implements WorkerExecutor {
|
|
|
360
356
|
}
|
|
361
357
|
);
|
|
362
358
|
|
|
363
|
-
// Setup I/O directories for file handling
|
|
364
359
|
await this.setupIODirectories();
|
|
365
|
-
|
|
366
|
-
// Download input files if any
|
|
367
360
|
await this.downloadInputFiles();
|
|
368
361
|
|
|
369
|
-
// Generate custom instructions
|
|
370
362
|
let customInstructions = await generateCustomInstructions(
|
|
371
363
|
[
|
|
372
364
|
new OpenClawCoreInstructionProvider(),
|
|
@@ -385,7 +377,7 @@ export class OpenClawWorker implements WorkerExecutor {
|
|
|
385
377
|
}
|
|
386
378
|
);
|
|
387
379
|
|
|
388
|
-
//
|
|
380
|
+
// Module hooks may modify the system prompt before agent execution.
|
|
389
381
|
try {
|
|
390
382
|
const { onSessionStart } = await import("../modules/lifecycle");
|
|
391
383
|
const moduleContext = await onSessionStart({
|
|
@@ -407,7 +399,6 @@ export class OpenClawWorker implements WorkerExecutor {
|
|
|
407
399
|
// Add file I/O instructions AFTER module hooks so they aren't overwritten
|
|
408
400
|
customInstructions += this.getFileIOInstructions();
|
|
409
401
|
|
|
410
|
-
// Execute AI session
|
|
411
402
|
logger.info(
|
|
412
403
|
`[TIMING] Starting OpenClaw session at: ${new Date().toISOString()}`
|
|
413
404
|
);
|
|
@@ -468,7 +459,6 @@ export class OpenClawWorker implements WorkerExecutor {
|
|
|
468
459
|
}
|
|
469
460
|
);
|
|
470
461
|
|
|
471
|
-
// Collect module data before sending final response
|
|
472
462
|
const { collectModuleData } = await import("../modules/lifecycle");
|
|
473
463
|
const moduleData = await collectModuleData({
|
|
474
464
|
workspaceDir: this.workspaceManager.getCurrentWorkingDirectory(),
|
|
@@ -477,7 +467,6 @@ export class OpenClawWorker implements WorkerExecutor {
|
|
|
477
467
|
});
|
|
478
468
|
this.workerTransport.setModuleData(moduleData);
|
|
479
469
|
|
|
480
|
-
// Handle result
|
|
481
470
|
if (result.success) {
|
|
482
471
|
const outputSnapshot = this.progressProcessor.getOutputSnapshot();
|
|
483
472
|
const hintGatewayUrl = process.env.DISPATCHER_URL;
|
package/src/server.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Lightweight Hono server started before SSE gateway connection.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
7
7
|
import { createServer } from "node:http";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { getRequestListener } from "@hono/node-server";
|
|
@@ -20,7 +20,6 @@ const logger = createLogger("worker-http");
|
|
|
20
20
|
const app = new Hono();
|
|
21
21
|
|
|
22
22
|
async function findSessionFile(): Promise<string | null> {
|
|
23
|
-
const { readdir, stat } = await import("node:fs/promises");
|
|
24
23
|
const workspaceDir = getOptionalEnv("WORKSPACE_DIR", "/workspace");
|
|
25
24
|
|
|
26
25
|
// Direct path: {WORKSPACE_DIR}/.openclaw/session.jsonl
|
|
@@ -163,10 +162,8 @@ function entryToMessage(entry: SessionEntry): ParsedMessage | null {
|
|
|
163
162
|
return null;
|
|
164
163
|
}
|
|
165
164
|
|
|
166
|
-
// Health check
|
|
167
165
|
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
168
166
|
|
|
169
|
-
// Full session messages with cursor-based pagination
|
|
170
167
|
app.get("/session/messages", async (c) => {
|
|
171
168
|
const cursor = c.req.query("cursor");
|
|
172
169
|
const limit = Math.min(parseInt(c.req.query("limit") || "50", 10), 200);
|
|
@@ -230,7 +227,6 @@ app.get("/session/messages", async (c) => {
|
|
|
230
227
|
}
|
|
231
228
|
});
|
|
232
229
|
|
|
233
|
-
// Session stats
|
|
234
230
|
app.get("/session/stats", async (c) => {
|
|
235
231
|
try {
|
|
236
232
|
const sessionPath = await findSessionFile();
|
|
@@ -5,11 +5,7 @@ interface AudioProviderSuggestions {
|
|
|
5
5
|
usedFallback: boolean;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
{ id: "chatgpt" },
|
|
10
|
-
{ id: "gemini" },
|
|
11
|
-
{ id: "elevenlabs" },
|
|
12
|
-
] as const;
|
|
8
|
+
const FALLBACK_PROVIDER_IDS = ["chatgpt", "gemini", "elevenlabs"] as const;
|
|
13
9
|
|
|
14
10
|
const KNOWN_PROVIDER_LABELS: Record<string, string> = {
|
|
15
11
|
chatgpt: "ChatGPT/OpenAI",
|
|
@@ -48,7 +44,7 @@ function getFallbackSuggestions(
|
|
|
48
44
|
available: boolean | null
|
|
49
45
|
): AudioProviderSuggestions {
|
|
50
46
|
return {
|
|
51
|
-
providerIds:
|
|
47
|
+
providerIds: [...FALLBACK_PROVIDER_IDS],
|
|
52
48
|
providerDisplayList: "",
|
|
53
49
|
available,
|
|
54
50
|
usedFallback: true,
|
|
@@ -119,6 +115,8 @@ export async function fetchAudioProviderSuggestions(params: {
|
|
|
119
115
|
`${params.gatewayUrl}/internal/audio/capabilities`,
|
|
120
116
|
{
|
|
121
117
|
headers: { Authorization: `Bearer ${params.workerToken}` },
|
|
118
|
+
// Capability probing is best-effort; never block the agent turn on it.
|
|
119
|
+
signal: AbortSignal.timeout(15_000),
|
|
122
120
|
}
|
|
123
121
|
);
|
|
124
122
|
if (!response.ok) {
|
|
@@ -285,17 +285,30 @@ export async function uploadUserFile(
|
|
|
285
285
|
const formDataBuffer = await formDataToBuffer(formData);
|
|
286
286
|
const fdHeaders = formData.getHeaders();
|
|
287
287
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
288
|
+
let response: Response;
|
|
289
|
+
try {
|
|
290
|
+
response = await fetch(`${gw.gatewayUrl}/internal/files/upload`, {
|
|
291
|
+
method: "POST",
|
|
292
|
+
headers: {
|
|
293
|
+
Authorization: `Bearer ${gw.workerToken}`,
|
|
294
|
+
"X-Channel-Id": gw.channelId,
|
|
295
|
+
"X-Conversation-Id": gw.conversationId,
|
|
296
|
+
...fdHeaders,
|
|
297
|
+
"Content-Length": formDataBuffer.length.toString(),
|
|
298
|
+
},
|
|
299
|
+
body: formDataBuffer,
|
|
300
|
+
// A stalled gateway upload must not wedge the agent turn forever —
|
|
301
|
+
// a 5-minute ceiling is well above any legitimate file delivery.
|
|
302
|
+
signal: AbortSignal.timeout(300_000),
|
|
303
|
+
});
|
|
304
|
+
} catch (err) {
|
|
305
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
306
|
+
return textResult(
|
|
307
|
+
`Error: Failed to show file to user: upload timed out`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
throw err;
|
|
311
|
+
}
|
|
299
312
|
|
|
300
313
|
if (!response.ok) {
|
|
301
314
|
const error = await response.text();
|
|
@@ -582,9 +595,9 @@ async function uploadGeneratedFile(
|
|
|
582
595
|
const formDataBuffer = await formDataToBuffer(formData);
|
|
583
596
|
const fdHeaders = formData.getHeaders();
|
|
584
597
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
{
|
|
598
|
+
let uploadResponse: Response;
|
|
599
|
+
try {
|
|
600
|
+
uploadResponse = await fetch(`${gw.gatewayUrl}/internal/files/upload`, {
|
|
588
601
|
method: "POST",
|
|
589
602
|
headers: {
|
|
590
603
|
Authorization: `Bearer ${gw.workerToken}`,
|
|
@@ -595,8 +608,14 @@ async function uploadGeneratedFile(
|
|
|
595
608
|
...extraHeaders,
|
|
596
609
|
},
|
|
597
610
|
body: formDataBuffer,
|
|
611
|
+
signal: AbortSignal.timeout(300_000),
|
|
612
|
+
});
|
|
613
|
+
} catch (err) {
|
|
614
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
615
|
+
return textResult(`Generated content but upload timed out`);
|
|
598
616
|
}
|
|
599
|
-
|
|
617
|
+
throw err;
|
|
618
|
+
}
|
|
600
619
|
|
|
601
620
|
if (!uploadResponse.ok) {
|
|
602
621
|
const uploadError = await uploadResponse.text();
|
|
@@ -638,6 +657,7 @@ export async function generateImage(
|
|
|
638
657
|
`${gw.gatewayUrl}/internal/images/capabilities`,
|
|
639
658
|
{
|
|
640
659
|
headers: { Authorization: `Bearer ${gw.workerToken}` },
|
|
660
|
+
signal: AbortSignal.timeout(30_000),
|
|
641
661
|
}
|
|
642
662
|
);
|
|
643
663
|
|
|
@@ -668,6 +688,9 @@ export async function generateImage(
|
|
|
668
688
|
background: args.background,
|
|
669
689
|
format: args.format,
|
|
670
690
|
}),
|
|
691
|
+
// Image gen can take a while at high quality, but never minutes — cap
|
|
692
|
+
// the wait so a stalled upstream provider doesn't hang the agent turn.
|
|
693
|
+
signal: AbortSignal.timeout(120_000),
|
|
671
694
|
});
|
|
672
695
|
|
|
673
696
|
if (!response.ok) {
|
|
@@ -758,6 +781,7 @@ export async function generateAudio(
|
|
|
758
781
|
voice: args.voice,
|
|
759
782
|
speed: args.speed,
|
|
760
783
|
}),
|
|
784
|
+
signal: AbortSignal.timeout(120_000),
|
|
761
785
|
});
|
|
762
786
|
|
|
763
787
|
if (!response.ok) {
|
|
@@ -915,11 +939,28 @@ export async function callMcpTool(
|
|
|
915
939
|
throw err;
|
|
916
940
|
}
|
|
917
941
|
|
|
918
|
-
|
|
942
|
+
// MCP proxy returns JSON on success, but a misbehaving upstream (502
|
|
943
|
+
// HTML, plain-text 500, empty body) would otherwise crash the tool call
|
|
944
|
+
// with "Unexpected token < in JSON". Treat parse failure as a transport
|
|
945
|
+
// error message instead of letting it bubble out as an unhandled throw.
|
|
946
|
+
let data: {
|
|
919
947
|
content?: Array<{ type: string; text: string }>;
|
|
920
948
|
error?: string;
|
|
921
949
|
isError?: boolean;
|
|
922
950
|
};
|
|
951
|
+
try {
|
|
952
|
+
data = (await response.json()) as {
|
|
953
|
+
content?: Array<{ type: string; text: string }>;
|
|
954
|
+
error?: string;
|
|
955
|
+
isError?: boolean;
|
|
956
|
+
};
|
|
957
|
+
} catch (parseErr) {
|
|
958
|
+
const parseMsg =
|
|
959
|
+
parseErr instanceof Error ? parseErr.message : String(parseErr);
|
|
960
|
+
return textResult(
|
|
961
|
+
`Error: ${toolName} returned a non-JSON response (status ${response.status}): ${parseMsg}`
|
|
962
|
+
);
|
|
963
|
+
}
|
|
923
964
|
|
|
924
965
|
if (!response.ok || data.isError) {
|
|
925
966
|
const contentText = data.content
|