@openclaw/crabline 0.1.8 → 0.1.10
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 +52 -22
- package/assets/crabline-banner.svg +20 -0
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +25 -1
- package/dist/src/cli/program.js +579 -82
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +59 -6
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +116 -92
- package/dist/src/config/schema.js +179 -73
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.js +9 -2
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/exit-codes.js.map +1 -1
- package/dist/src/core/matcher.js +7 -6
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/message-template.js.map +1 -1
- package/dist/src/core/nonces.d.ts +1 -0
- package/dist/src/core/nonces.js +4 -2
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +1 -0
- package/dist/src/core/reporters.js +42 -4
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -0
- package/dist/src/core/run.js +415 -107
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/core/safe-regex.d.ts +3 -0
- package/dist/src/core/safe-regex.js +21 -0
- package/dist/src/core/safe-regex.js.map +1 -0
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/openclaw/artifact-generation.d.ts +38 -0
- package/dist/src/openclaw/artifact-generation.js +314 -0
- package/dist/src/openclaw/artifact-generation.js.map +1 -0
- package/dist/src/openclaw/bridges/matrix.js +97 -12
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +32 -10
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/signal.js +55 -18
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +85 -7
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +124 -30
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +78 -17
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +14 -5
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/outbound-contract.d.ts +5 -0
- package/dist/src/openclaw/outbound-contract.js +40 -0
- package/dist/src/openclaw/outbound-contract.js.map +1 -0
- package/dist/src/openclaw/private-file.d.ts +26 -0
- package/dist/src/openclaw/private-file.js +411 -0
- package/dist/src/openclaw/private-file.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +23 -2
- package/dist/src/openclaw/shared.js +115 -18
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw/smoke-lock.d.ts +63 -0
- package/dist/src/openclaw/smoke-lock.js +949 -0
- package/dist/src/openclaw/smoke-lock.js.map +1 -0
- package/dist/src/openclaw.d.ts +16 -8
- package/dist/src/openclaw.js +195 -52
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/providers/builtin/discord.d.ts +10 -1
- package/dist/src/providers/builtin/discord.js +144 -18
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +11 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +10 -0
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -0
- package/dist/src/providers/builtin/feishu.d.ts +19 -2
- package/dist/src/providers/builtin/feishu.js +307 -25
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +20 -1
- package/dist/src/providers/builtin/googlechat.js +178 -19
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.d.ts +4 -0
- package/dist/src/providers/builtin/imessage.js +26 -11
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +92 -30
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +11 -1
- package/dist/src/providers/builtin/matrix.js +53 -29
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +10 -0
- package/dist/src/providers/builtin/mattermost.js +38 -15
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +17 -1
- package/dist/src/providers/builtin/msteams.js +113 -14
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.d.ts +2 -9
- package/dist/src/providers/builtin/native-local-mock.js +12 -35
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.d.ts +11 -15
- package/dist/src/providers/builtin/script.js +566 -104
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +6 -0
- package/dist/src/providers/builtin/slack.js +99 -40
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +6 -0
- package/dist/src/providers/builtin/telegram.js +64 -43
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +28 -8
- package/dist/src/providers/builtin/whatsapp.js +567 -30
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.js +30 -19
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +3 -3
- package/dist/src/providers/catalog.js +3 -3
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +11 -2
- package/dist/src/providers/local-mock.js +322 -96
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +1 -0
- package/dist/src/providers/native-ids.js +6 -1
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +30 -2
- package/dist/src/providers/recorder.js +539 -70
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +25 -1
- package/dist/src/providers/registry.js +280 -40
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +29 -0
- package/dist/src/providers/signed-jwt.js +202 -0
- package/dist/src/providers/signed-jwt.js.map +1 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +26 -0
- package/dist/src/providers/target-normalizers.js +276 -0
- package/dist/src/providers/target-normalizers.js.map +1 -0
- package/dist/src/providers/types.d.ts +5 -0
- package/dist/src/providers/types.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +2 -0
- package/dist/src/providers/webhook-server.js +72 -25
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/http.d.ts +23 -3
- package/dist/src/servers/http.js +213 -20
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/index.js +20 -21
- package/dist/src/servers/index.js.map +1 -1
- package/dist/src/servers/matrix.d.ts +2 -0
- package/dist/src/servers/matrix.js +466 -52
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +7 -0
- package/dist/src/servers/mattermost.js +307 -59
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/pending-events.d.ts +2 -0
- package/dist/src/servers/pending-events.js +11 -0
- package/dist/src/servers/pending-events.js.map +1 -0
- package/dist/src/servers/recorder.d.ts +12 -0
- package/dist/src/servers/recorder.js +51 -0
- package/dist/src/servers/recorder.js.map +1 -0
- package/dist/src/servers/signal.d.ts +4 -0
- package/dist/src/servers/signal.js +445 -57
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +7 -0
- package/dist/src/servers/slack.js +516 -50
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram.d.ts +113 -0
- package/dist/src/servers/telegram.js +814 -155
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +40 -0
- package/dist/src/servers/webhook-target.js +349 -0
- package/dist/src/servers/webhook-target.js.map +1 -0
- package/dist/src/servers/websocket.d.ts +2 -0
- package/dist/src/servers/websocket.js +29 -0
- package/dist/src/servers/websocket.js.map +1 -0
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +65 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +723 -99
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +5 -0
- package/dist/src/servers/whatsapp-jid.js +32 -0
- package/dist/src/servers/whatsapp-jid.js.map +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +5 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +87 -20
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +6 -1
- package/dist/src/servers/whatsapp-wire/crypto.js +99 -45
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.js +39 -4
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +14 -7
- package/dist/src/servers/whatsapp.js +272 -110
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +16 -0
- package/dist/src/servers/zalo.js +524 -91
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +197 -19
- package/fixtures/examples/crabline.example.yaml +10 -0
- package/package.json +20 -15
|
@@ -1,39 +1,506 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
import { CrablineError, ensureErrorMessage } from "../../core/errors.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
const MAX_SCRIPT_OUTPUT_BYTES = 1024 * 1024;
|
|
6
|
+
const SCRIPT_WAIT_EXIT_GRACE_MS = 250;
|
|
7
|
+
const WINDOWS_TERMINATION_TIMEOUT_MS = 5_000;
|
|
8
|
+
const ScriptMessageSchema = z.object({
|
|
9
|
+
author: z.enum(["assistant", "system", "user"]),
|
|
10
|
+
id: z.string().min(1),
|
|
11
|
+
raw: z.unknown().optional(),
|
|
12
|
+
sentAt: z.string().min(1),
|
|
13
|
+
text: z.string(),
|
|
14
|
+
threadId: z.string().min(1),
|
|
15
|
+
});
|
|
16
|
+
const ScriptProbeResultSchema = z.object({
|
|
17
|
+
details: z.array(z.string()).optional(),
|
|
18
|
+
healthy: z.boolean(),
|
|
19
|
+
});
|
|
20
|
+
const ScriptSendResultSchema = z.object({
|
|
21
|
+
accepted: z.boolean(),
|
|
22
|
+
messageId: z.string().min(1),
|
|
23
|
+
threadId: z.string().min(1),
|
|
24
|
+
});
|
|
25
|
+
const ScriptInboundResultSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
message: ScriptMessageSchema.optional(),
|
|
28
|
+
timeout: z.boolean().optional(),
|
|
29
|
+
})
|
|
30
|
+
.refine((result) => (result.timeout === true && result.message === undefined) ||
|
|
31
|
+
(result.timeout !== true && result.message !== undefined), { message: "result must contain either a message or timeout: true" });
|
|
32
|
+
async function terminateChild(child) {
|
|
33
|
+
if (process.platform === "win32" && child.pid) {
|
|
34
|
+
await new Promise((resolve) => {
|
|
35
|
+
let taskkill;
|
|
36
|
+
try {
|
|
37
|
+
taskkill = spawn("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], {
|
|
38
|
+
stdio: "ignore",
|
|
39
|
+
windowsHide: true,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
resolve();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let settled = false;
|
|
47
|
+
const finish = () => {
|
|
48
|
+
if (settled) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
settled = true;
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
resolve();
|
|
54
|
+
};
|
|
55
|
+
const timeout = setTimeout(() => {
|
|
56
|
+
taskkill.kill("SIGKILL");
|
|
57
|
+
finish();
|
|
58
|
+
}, WINDOWS_TERMINATION_TIMEOUT_MS);
|
|
59
|
+
timeout.unref();
|
|
60
|
+
taskkill.once("close", finish);
|
|
61
|
+
taskkill.once("error", finish);
|
|
11
62
|
});
|
|
12
|
-
|
|
13
|
-
|
|
63
|
+
}
|
|
64
|
+
else if (child.pid) {
|
|
65
|
+
try {
|
|
66
|
+
process.kill(-child.pid, "SIGKILL");
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// The process group may have exited with the shell.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
73
|
+
child.kill("SIGKILL");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function formatValidationError(error) {
|
|
77
|
+
return error.issues
|
|
78
|
+
.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "result"}: ${issue.message}`)
|
|
79
|
+
.join("; ");
|
|
80
|
+
}
|
|
81
|
+
const sensitiveEnvironmentNameFragmentPattern = /(?:AUTH|BEARER|CREDENTIAL|KEY|PASS|PRIVATE|SECRET|TOKEN)/iu;
|
|
82
|
+
const nonSensitiveWorkingDirectoryNames = new Set(["PWD", "OLDPWD"]);
|
|
83
|
+
function isSensitiveEnvironmentName(name) {
|
|
84
|
+
const upperName = name.toUpperCase();
|
|
85
|
+
return (sensitiveEnvironmentNameFragmentPattern.test(upperName) ||
|
|
86
|
+
(!nonSensitiveWorkingDirectoryNames.has(upperName) && upperName.includes("PWD")) ||
|
|
87
|
+
/PAT(?!H)/u.test(upperName));
|
|
88
|
+
}
|
|
89
|
+
function commandContainsSensitiveValue(command) {
|
|
90
|
+
const assignments = [
|
|
91
|
+
...command.matchAll(/(?:^|[\s;&|])["']?([A-Za-z_][A-Za-z0-9_]*)\s*\+?=/gu),
|
|
92
|
+
...command.matchAll(/\$env:([A-Za-z_][A-Za-z0-9_]*)\s*\+?=/giu),
|
|
93
|
+
...command.matchAll(/\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}\s*\+?=/giu),
|
|
94
|
+
];
|
|
95
|
+
if (assignments.some((match) => isSensitiveEnvironmentName(match[1] ?? ""))) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return [...command.matchAll(/(?:^|[\s;&|])["']?--([A-Za-z0-9][A-Za-z0-9_-]*)(?:=|\s)/gu)].some((match) => isSensitiveEnvironmentName((match[1] ?? "").replaceAll("-", "_")));
|
|
99
|
+
}
|
|
100
|
+
function snapshotSensitiveEnvironmentValues() {
|
|
101
|
+
const representations = new Set();
|
|
102
|
+
const values = Object.entries(process.env)
|
|
103
|
+
.filter(([name, value]) => isSensitiveEnvironmentName(name) && value !== undefined && value.length > 0)
|
|
104
|
+
.map(([, value]) => value)
|
|
105
|
+
.sort((left, right) => right.length - left.length);
|
|
106
|
+
for (const value of values) {
|
|
107
|
+
representations.add(value);
|
|
108
|
+
representations.add(JSON.stringify(value));
|
|
109
|
+
representations.add(JSON.stringify(value).slice(1, -1));
|
|
110
|
+
}
|
|
111
|
+
return [...representations].sort((left, right) => right.length - left.length);
|
|
112
|
+
}
|
|
113
|
+
function redactSensitiveEnvironmentValues(detail, sensitiveEnvironmentValues) {
|
|
114
|
+
let redacted = detail;
|
|
115
|
+
for (const representation of sensitiveEnvironmentValues) {
|
|
116
|
+
redacted = redacted.split(representation).join("[redacted environment value]");
|
|
117
|
+
}
|
|
118
|
+
return redacted;
|
|
119
|
+
}
|
|
120
|
+
function collectSensitivePayloadValues(value, values, seen, sensitive = false) {
|
|
121
|
+
if (typeof value === "string") {
|
|
122
|
+
if (sensitive && value.length > 0) {
|
|
123
|
+
values.add(value);
|
|
124
|
+
const serialized = JSON.stringify(value);
|
|
125
|
+
values.add(serialized);
|
|
126
|
+
values.add(serialized.slice(1, -1));
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (sensitive && typeof value === "number" && Number.isFinite(value)) {
|
|
131
|
+
values.add(String(value));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (!value || typeof value !== "object") {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const visited = sensitive ? seen.sensitive : seen.nonSensitive;
|
|
138
|
+
if (visited.has(value)) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
visited.add(value);
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
for (const entry of value) {
|
|
144
|
+
collectSensitivePayloadValues(entry, values, seen, sensitive);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
149
|
+
collectSensitivePayloadValues(entry, values, seen, sensitive || isSensitiveEnvironmentName(name));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function snapshotSensitivePayloadValues(payload) {
|
|
153
|
+
const values = new Set();
|
|
154
|
+
collectSensitivePayloadValues(payload, values, {
|
|
155
|
+
nonSensitive: new WeakSet(),
|
|
156
|
+
sensitive: new WeakSet(),
|
|
157
|
+
});
|
|
158
|
+
return [...values].sort((left, right) => right.length - left.length);
|
|
159
|
+
}
|
|
160
|
+
function redactSensitivePayloadValues(detail, sensitivePayloadValues) {
|
|
161
|
+
let redacted = detail;
|
|
162
|
+
for (const value of sensitivePayloadValues) {
|
|
163
|
+
redacted = redacted.split(value).join("[redacted configured value]");
|
|
164
|
+
}
|
|
165
|
+
return redacted;
|
|
166
|
+
}
|
|
167
|
+
function formatScriptError(summary, detail, command, diagnostics) {
|
|
168
|
+
if (!detail.trim()) {
|
|
169
|
+
return summary;
|
|
170
|
+
}
|
|
171
|
+
if (commandContainsSensitiveValue(command)) {
|
|
172
|
+
return `${summary}\n[script diagnostics redacted]`;
|
|
173
|
+
}
|
|
174
|
+
let redacted = redactSensitivePayloadValues(redactSensitiveEnvironmentValues(detail, diagnostics.sensitiveEnvironmentValues), diagnostics.sensitivePayloadValues);
|
|
175
|
+
for (const configuredCommand of diagnostics.configuredCommands) {
|
|
176
|
+
redacted = redacted.split(configuredCommand).join("[configured script command]");
|
|
177
|
+
}
|
|
178
|
+
redacted = redacted.trim();
|
|
179
|
+
return redacted ? `${summary}\n${redacted}` : summary;
|
|
180
|
+
}
|
|
181
|
+
function createScriptDiagnosticsSnapshot(command, serializedPayload) {
|
|
182
|
+
const payload = JSON.parse(serializedPayload);
|
|
183
|
+
const configuredCommands = new Set([command]);
|
|
184
|
+
const commands = payload.provider?.config?.script?.commands;
|
|
185
|
+
for (const configuredCommand of Object.values(commands ?? {})) {
|
|
186
|
+
if (typeof configuredCommand === "string" && configuredCommand.length > 0) {
|
|
187
|
+
configuredCommands.add(configuredCommand);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
configuredCommands: [...configuredCommands].sort((left, right) => right.length - left.length),
|
|
192
|
+
sensitiveEnvironmentValues: snapshotSensitiveEnvironmentValues(),
|
|
193
|
+
sensitivePayloadValues: snapshotSensitivePayloadValues(payload),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function parseScriptJson(params) {
|
|
197
|
+
let parsed;
|
|
198
|
+
try {
|
|
199
|
+
parsed = JSON.parse(params.output);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
throw new CrablineError(formatScriptError("Script command did not return valid JSON.", ensureErrorMessage(error), params.command, params.diagnostics), { kind: "config" });
|
|
203
|
+
}
|
|
204
|
+
const result = params.schema.safeParse(parsed);
|
|
205
|
+
if (!result.success) {
|
|
206
|
+
throw new CrablineError(`Script command returned invalid result.\n${formatValidationError(result.error)}`, { kind: "config" });
|
|
207
|
+
}
|
|
208
|
+
return result.data;
|
|
209
|
+
}
|
|
210
|
+
function runScript(params) {
|
|
211
|
+
if (params.signal?.aborted) {
|
|
212
|
+
return Promise.reject(params.signal.reason ?? new Error("Script command aborted."));
|
|
213
|
+
}
|
|
214
|
+
const serializedPayload = JSON.stringify(params.payload);
|
|
215
|
+
const diagnostics = createScriptDiagnosticsSnapshot(params.command, serializedPayload);
|
|
216
|
+
return new Promise((resolve, reject) => {
|
|
217
|
+
let child;
|
|
218
|
+
try {
|
|
219
|
+
child = spawn(params.command, {
|
|
220
|
+
cwd: params.cwd ? path.resolve(params.cwd) : process.cwd(),
|
|
221
|
+
env: process.env,
|
|
222
|
+
shell: params.shell ?? true,
|
|
223
|
+
detached: process.platform !== "win32",
|
|
224
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
reject(new CrablineError(formatScriptError("Script command failed to start.", ensureErrorMessage(error), params.command, diagnostics), { kind: "connectivity" }));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const stdout = [];
|
|
232
|
+
const stderr = [];
|
|
233
|
+
let deadlineExceeded = false;
|
|
234
|
+
let outputBytes = 0;
|
|
235
|
+
let settled = false;
|
|
236
|
+
let timeoutGrace;
|
|
237
|
+
const abort = () => {
|
|
238
|
+
finish(async () => {
|
|
239
|
+
await terminateChild(child);
|
|
240
|
+
reject(params.signal?.reason ?? new Error("Script command aborted."));
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
const finish = (callback) => {
|
|
244
|
+
if (settled) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
settled = true;
|
|
248
|
+
clearTimeout(timeout);
|
|
249
|
+
clearTimeout(timeoutGrace);
|
|
250
|
+
params.signal?.removeEventListener("abort", abort);
|
|
251
|
+
void callback();
|
|
252
|
+
};
|
|
253
|
+
const failForOutputLimit = () => {
|
|
254
|
+
finish(async () => {
|
|
255
|
+
await terminateChild(child);
|
|
256
|
+
reject(new CrablineError(`Script command exceeded ${MAX_SCRIPT_OUTPUT_BYTES} bytes of output.`, {
|
|
257
|
+
kind: "connectivity",
|
|
258
|
+
}));
|
|
259
|
+
});
|
|
260
|
+
};
|
|
14
261
|
child.stdout.on("data", (chunk) => {
|
|
15
|
-
|
|
262
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
263
|
+
outputBytes += buffer.length;
|
|
264
|
+
if (outputBytes > MAX_SCRIPT_OUTPUT_BYTES) {
|
|
265
|
+
failForOutputLimit();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
stdout.push(buffer);
|
|
16
269
|
});
|
|
17
270
|
child.stderr.on("data", (chunk) => {
|
|
18
|
-
|
|
271
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
272
|
+
outputBytes += buffer.length;
|
|
273
|
+
if (outputBytes > MAX_SCRIPT_OUTPUT_BYTES) {
|
|
274
|
+
failForOutputLimit();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
stderr.push(buffer);
|
|
19
278
|
});
|
|
20
|
-
child.on("error",
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
279
|
+
child.stdin.on("error", () => {
|
|
280
|
+
// Child closure is reported through the process error/close handlers.
|
|
281
|
+
});
|
|
282
|
+
child.once("error", (error) => {
|
|
283
|
+
finish(() => {
|
|
284
|
+
reject(new CrablineError(formatScriptError("Script command failed to start.", ensureErrorMessage(error), params.command, diagnostics), { kind: "connectivity" }));
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
child.once("close", (code, signal) => {
|
|
288
|
+
finish(() => {
|
|
289
|
+
const stdoutText = Buffer.concat(stdout).toString("utf8");
|
|
290
|
+
const stderrText = Buffer.concat(stderr).toString("utf8");
|
|
291
|
+
if (code !== 0) {
|
|
292
|
+
reject(new CrablineError(formatScriptError(`Script command failed${signal ? ` (${signal})` : ""}.`, stderrText.trim() ? stderrText : stdoutText, params.command, diagnostics), { kind: "connectivity" }));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const result = parseScriptJson({
|
|
297
|
+
command: params.command,
|
|
298
|
+
diagnostics,
|
|
299
|
+
output: stdoutText,
|
|
300
|
+
schema: params.schema,
|
|
301
|
+
});
|
|
302
|
+
if (deadlineExceeded && !params.acceptResultDuringTimeoutGrace?.(result)) {
|
|
303
|
+
reject(new CrablineError(`Script command timed out after ${params.timeoutMs}ms.`, {
|
|
304
|
+
kind: "timeout",
|
|
305
|
+
}));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
resolve(result);
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
reject(error);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
const failForTimeout = () => {
|
|
316
|
+
finish(async () => {
|
|
317
|
+
await terminateChild(child);
|
|
318
|
+
reject(new CrablineError(`Script command timed out after ${params.timeoutMs}ms.`, {
|
|
319
|
+
kind: "timeout",
|
|
25
320
|
}));
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
const timeout = setTimeout(() => {
|
|
324
|
+
const timeoutGraceMs = params.timeoutGraceMs ?? 0;
|
|
325
|
+
if (timeoutGraceMs <= 0) {
|
|
326
|
+
failForTimeout();
|
|
26
327
|
return;
|
|
27
328
|
}
|
|
28
|
-
|
|
29
|
-
|
|
329
|
+
deadlineExceeded = true;
|
|
330
|
+
timeoutGrace = setTimeout(failForTimeout, timeoutGraceMs);
|
|
331
|
+
timeoutGrace.unref();
|
|
332
|
+
}, params.timeoutMs);
|
|
333
|
+
timeout.unref();
|
|
334
|
+
if (params.signal?.aborted) {
|
|
335
|
+
abort();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
params.signal?.addEventListener("abort", abort, { once: true });
|
|
339
|
+
child.stdin.end(serializedPayload);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
function watchScript(params) {
|
|
343
|
+
return (async function* () {
|
|
344
|
+
if (params.cancelSignal.aborted) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (params.context.signal?.aborted) {
|
|
348
|
+
throw params.context.signal.reason ?? new Error("Script watch command aborted.");
|
|
349
|
+
}
|
|
350
|
+
const payload = {
|
|
351
|
+
...createPayload(params.context),
|
|
352
|
+
watch: {
|
|
353
|
+
since: params.context.since,
|
|
354
|
+
target: params.normalizeTarget(params.context.fixture.target),
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
const serializedPayload = JSON.stringify(payload);
|
|
358
|
+
const diagnostics = createScriptDiagnosticsSnapshot(params.command, serializedPayload);
|
|
359
|
+
let child;
|
|
360
|
+
try {
|
|
361
|
+
child = spawn(params.command, {
|
|
362
|
+
cwd: params.cwd ? path.resolve(params.cwd) : process.cwd(),
|
|
363
|
+
env: process.env,
|
|
364
|
+
shell: params.shell ?? true,
|
|
365
|
+
detached: process.platform !== "win32",
|
|
366
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
throw new CrablineError(formatScriptError("Script watch command failed to start.", ensureErrorMessage(error), params.command, diagnostics), { kind: "connectivity" });
|
|
371
|
+
}
|
|
372
|
+
let buffer = "";
|
|
373
|
+
let stderr = "";
|
|
374
|
+
let childError;
|
|
375
|
+
let outputLimitError;
|
|
376
|
+
const stopChild = () => {
|
|
377
|
+
void terminateChild(child);
|
|
378
|
+
};
|
|
379
|
+
params.cancelSignal.addEventListener("abort", stopChild, { once: true });
|
|
380
|
+
params.context.signal?.addEventListener("abort", stopChild, { once: true });
|
|
381
|
+
if (params.cancelSignal.aborted || params.context.signal?.aborted) {
|
|
382
|
+
stopChild();
|
|
383
|
+
}
|
|
384
|
+
child.stdout.setEncoding("utf8");
|
|
385
|
+
child.stderr.setEncoding("utf8");
|
|
386
|
+
child.stdin.on("error", () => {
|
|
387
|
+
// Child closure is reported through the process error/close handlers.
|
|
388
|
+
});
|
|
389
|
+
child.stderr.on("data", (chunk) => {
|
|
390
|
+
if (outputLimitError) {
|
|
391
|
+
return;
|
|
30
392
|
}
|
|
31
|
-
|
|
32
|
-
|
|
393
|
+
stderr += chunk;
|
|
394
|
+
if (Buffer.byteLength(stderr) > MAX_SCRIPT_OUTPUT_BYTES) {
|
|
395
|
+
outputLimitError = new CrablineError(`Script watch command exceeded ${MAX_SCRIPT_OUTPUT_BYTES} bytes of stderr.`, { kind: "connectivity" });
|
|
396
|
+
void terminateChild(child);
|
|
33
397
|
}
|
|
34
398
|
});
|
|
35
|
-
child.
|
|
36
|
-
|
|
399
|
+
child.once("error", (error) => {
|
|
400
|
+
childError = error;
|
|
401
|
+
});
|
|
402
|
+
let childCloseObserved = false;
|
|
403
|
+
const childClosed = new Promise((resolve) => {
|
|
404
|
+
child.once("close", (code, signal) => {
|
|
405
|
+
childCloseObserved = true;
|
|
406
|
+
resolve({ code, signal });
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
child.stdin.end(serializedPayload);
|
|
410
|
+
try {
|
|
411
|
+
for await (const chunk of child.stdout) {
|
|
412
|
+
buffer += chunk;
|
|
413
|
+
if (Buffer.byteLength(buffer) > MAX_SCRIPT_OUTPUT_BYTES && !buffer.includes("\n")) {
|
|
414
|
+
throw new CrablineError(`Script watch command exceeded ${MAX_SCRIPT_OUTPUT_BYTES} bytes without a newline.`, { kind: "config" });
|
|
415
|
+
}
|
|
416
|
+
const lines = buffer.split("\n");
|
|
417
|
+
buffer = lines.pop() ?? "";
|
|
418
|
+
for (const line of lines) {
|
|
419
|
+
if (!line.trim()) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (Buffer.byteLength(line) > MAX_SCRIPT_OUTPUT_BYTES) {
|
|
423
|
+
throw new CrablineError(`Script watch command emitted a JSON line larger than ${MAX_SCRIPT_OUTPUT_BYTES} bytes.`, { kind: "config" });
|
|
424
|
+
}
|
|
425
|
+
const parsed = parseScriptJson({
|
|
426
|
+
command: params.command,
|
|
427
|
+
diagnostics,
|
|
428
|
+
output: line,
|
|
429
|
+
schema: ScriptMessageSchema,
|
|
430
|
+
});
|
|
431
|
+
yield {
|
|
432
|
+
...parsed,
|
|
433
|
+
provider: params.id,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (params.context.signal?.aborted) {
|
|
438
|
+
throw params.context.signal.reason ?? new Error("Script watch command aborted.");
|
|
439
|
+
}
|
|
440
|
+
if (params.cancelSignal.aborted) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (buffer.trim()) {
|
|
444
|
+
const parsed = parseScriptJson({
|
|
445
|
+
command: params.command,
|
|
446
|
+
diagnostics,
|
|
447
|
+
output: buffer,
|
|
448
|
+
schema: ScriptMessageSchema,
|
|
449
|
+
});
|
|
450
|
+
yield {
|
|
451
|
+
...parsed,
|
|
452
|
+
provider: params.id,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
const exit = await childClosed;
|
|
456
|
+
if (outputLimitError) {
|
|
457
|
+
throw outputLimitError;
|
|
458
|
+
}
|
|
459
|
+
if (childError) {
|
|
460
|
+
throw new CrablineError(formatScriptError("Script watch command failed to start.", ensureErrorMessage(childError), params.command, diagnostics), { kind: "connectivity" });
|
|
461
|
+
}
|
|
462
|
+
if (exit.code !== 0) {
|
|
463
|
+
throw new CrablineError(formatScriptError(`Script watch command failed${exit.signal ? ` (${exit.signal})` : ""}.`, stderr, params.command, diagnostics), { kind: "connectivity" });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
if (params.cancelSignal.aborted) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (outputLimitError) {
|
|
471
|
+
throw outputLimitError;
|
|
472
|
+
}
|
|
473
|
+
if (childError) {
|
|
474
|
+
throw new CrablineError(formatScriptError("Script watch command failed to start.", ensureErrorMessage(childError), params.command, diagnostics), { kind: "connectivity" });
|
|
475
|
+
}
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
finally {
|
|
479
|
+
params.cancelSignal.removeEventListener("abort", stopChild);
|
|
480
|
+
params.context.signal?.removeEventListener("abort", stopChild);
|
|
481
|
+
child.stdin.destroy();
|
|
482
|
+
if (!childCloseObserved) {
|
|
483
|
+
await terminateChild(child);
|
|
484
|
+
}
|
|
485
|
+
await childClosed;
|
|
486
|
+
}
|
|
487
|
+
})();
|
|
488
|
+
}
|
|
489
|
+
function failedScriptWatch(error) {
|
|
490
|
+
return {
|
|
491
|
+
[Symbol.asyncIterator]() {
|
|
492
|
+
return this;
|
|
493
|
+
},
|
|
494
|
+
next() {
|
|
495
|
+
return Promise.reject(error);
|
|
496
|
+
},
|
|
497
|
+
return() {
|
|
498
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
499
|
+
},
|
|
500
|
+
throw(thrown) {
|
|
501
|
+
return Promise.reject(thrown);
|
|
502
|
+
},
|
|
503
|
+
};
|
|
37
504
|
}
|
|
38
505
|
export class ScriptProviderAdapter {
|
|
39
506
|
id;
|
|
@@ -73,7 +540,15 @@ export class ScriptProviderAdapter {
|
|
|
73
540
|
healthy: false,
|
|
74
541
|
};
|
|
75
542
|
}
|
|
76
|
-
const result = await runScript(
|
|
543
|
+
const result = await runScript({
|
|
544
|
+
command,
|
|
545
|
+
cwd: this.#config.cwd,
|
|
546
|
+
payload: createPayload(context),
|
|
547
|
+
schema: ScriptProbeResultSchema,
|
|
548
|
+
shell: this.#config.shell,
|
|
549
|
+
signal: context.signal,
|
|
550
|
+
timeoutMs: context.fixture.timeoutMs,
|
|
551
|
+
});
|
|
77
552
|
return {
|
|
78
553
|
details: result.details ?? [],
|
|
79
554
|
healthy: result.healthy,
|
|
@@ -86,30 +561,50 @@ export class ScriptProviderAdapter {
|
|
|
86
561
|
kind: "config",
|
|
87
562
|
});
|
|
88
563
|
}
|
|
89
|
-
return runScript(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
564
|
+
return runScript({
|
|
565
|
+
command,
|
|
566
|
+
cwd: this.#config.cwd,
|
|
567
|
+
payload: {
|
|
568
|
+
...createPayload(context),
|
|
569
|
+
outbound: {
|
|
570
|
+
mode: context.mode,
|
|
571
|
+
nonce: context.nonce,
|
|
572
|
+
target: this.normalizeTarget(context.fixture.target),
|
|
573
|
+
text: context.text,
|
|
574
|
+
},
|
|
96
575
|
},
|
|
97
|
-
|
|
576
|
+
schema: ScriptSendResultSchema,
|
|
577
|
+
shell: this.#config.shell,
|
|
578
|
+
signal: context.signal,
|
|
579
|
+
timeoutMs: context.fixture.timeoutMs,
|
|
580
|
+
});
|
|
98
581
|
}
|
|
99
582
|
async waitForInbound(context) {
|
|
100
583
|
const command = this.#config.commands.waitForInbound;
|
|
101
584
|
if (!command) {
|
|
102
585
|
return null;
|
|
103
586
|
}
|
|
104
|
-
const result = await runScript(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
587
|
+
const result = await runScript({
|
|
588
|
+
acceptResultDuringTimeoutGrace: (candidate) => candidate.timeout === true,
|
|
589
|
+
command,
|
|
590
|
+
cwd: this.#config.cwd,
|
|
591
|
+
payload: {
|
|
592
|
+
...createPayload(context),
|
|
593
|
+
wait: {
|
|
594
|
+
excludeIds: context.excludeIds ?? [],
|
|
595
|
+
nonce: context.nonce,
|
|
596
|
+
since: context.since,
|
|
597
|
+
target: this.normalizeTarget(context.fixture.target),
|
|
598
|
+
threadId: context.threadId,
|
|
599
|
+
timeoutMs: context.timeoutMs,
|
|
600
|
+
},
|
|
111
601
|
},
|
|
112
|
-
|
|
602
|
+
schema: ScriptInboundResultSchema,
|
|
603
|
+
shell: this.#config.shell,
|
|
604
|
+
signal: context.signal,
|
|
605
|
+
timeoutGraceMs: SCRIPT_WAIT_EXIT_GRACE_MS,
|
|
606
|
+
timeoutMs: context.timeoutMs,
|
|
607
|
+
});
|
|
113
608
|
if (result.timeout || !result.message) {
|
|
114
609
|
return null;
|
|
115
610
|
}
|
|
@@ -118,79 +613,46 @@ export class ScriptProviderAdapter {
|
|
|
118
613
|
provider: this.id,
|
|
119
614
|
};
|
|
120
615
|
}
|
|
121
|
-
|
|
616
|
+
watch(context) {
|
|
122
617
|
const command = this.#config.commands.watch;
|
|
123
618
|
if (!command) {
|
|
124
|
-
|
|
619
|
+
return failedScriptWatch(new CrablineError(`Provider "${this.id}" is missing watch command.`, {
|
|
125
620
|
kind: "config",
|
|
126
|
-
});
|
|
621
|
+
}));
|
|
127
622
|
}
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
623
|
+
const controller = new AbortController();
|
|
624
|
+
const source = watchScript({
|
|
625
|
+
cancelSignal: controller.signal,
|
|
626
|
+
command,
|
|
627
|
+
context,
|
|
628
|
+
cwd: this.#config.cwd,
|
|
629
|
+
id: this.id,
|
|
630
|
+
normalizeTarget: (target) => this.normalizeTarget(target),
|
|
631
|
+
shell: this.#config.shell,
|
|
134
632
|
});
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
since: context.since,
|
|
139
|
-
target: this.normalizeTarget(context.fixture.target),
|
|
633
|
+
const iterator = {
|
|
634
|
+
[Symbol.asyncIterator]() {
|
|
635
|
+
return this;
|
|
140
636
|
},
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
child.once("close", () => resolve());
|
|
149
|
-
});
|
|
150
|
-
try {
|
|
151
|
-
for await (const chunk of child.stdout) {
|
|
152
|
-
buffer += String(chunk);
|
|
153
|
-
const lines = buffer.split("\n");
|
|
154
|
-
buffer = lines.pop() ?? "";
|
|
155
|
-
for (const line of lines) {
|
|
156
|
-
if (!line.trim()) {
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
const parsed = JSON.parse(line);
|
|
160
|
-
if (!parsed) {
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
yield {
|
|
164
|
-
...parsed,
|
|
165
|
-
provider: this.id,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (buffer.trim()) {
|
|
170
|
-
const parsed = JSON.parse(buffer);
|
|
171
|
-
if (parsed) {
|
|
172
|
-
yield {
|
|
173
|
-
...parsed,
|
|
174
|
-
provider: this.id,
|
|
175
|
-
};
|
|
637
|
+
next() {
|
|
638
|
+
return source.next();
|
|
639
|
+
},
|
|
640
|
+
async return() {
|
|
641
|
+
if (!controller.signal.aborted) {
|
|
642
|
+
controller.abort();
|
|
176
643
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (process.platform !== "win32" && child.pid) {
|
|
184
|
-
try {
|
|
185
|
-
process.kill(-child.pid, "SIGKILL");
|
|
186
|
-
}
|
|
187
|
-
catch {
|
|
188
|
-
// The process group may have already exited with the shell.
|
|
189
|
-
}
|
|
644
|
+
await source.return(undefined);
|
|
645
|
+
return { done: true, value: undefined };
|
|
646
|
+
},
|
|
647
|
+
async throw(error) {
|
|
648
|
+
if (!controller.signal.aborted) {
|
|
649
|
+
controller.abort();
|
|
190
650
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
651
|
+
await source.return(undefined);
|
|
652
|
+
throw error;
|
|
653
|
+
},
|
|
654
|
+
};
|
|
655
|
+
return iterator;
|
|
194
656
|
}
|
|
195
657
|
}
|
|
196
658
|
function createPayload(context) {
|