@syengup/friday-channel-next 0.1.36 → 0.1.38
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/index.js +1 -1
- package/dist/src/agent/dispatch-bridge.d.ts +1 -1
- package/dist/src/agent/node-pairing-bridge.d.ts +11 -8
- package/dist/src/agent/node-pairing-bridge.js +6 -2
- package/dist/src/agent/operator-scope.d.ts +19 -0
- package/dist/src/agent/operator-scope.js +54 -0
- package/dist/src/agent/subagent-registry.js +0 -3
- package/dist/src/channel-actions.js +3 -1
- package/dist/src/channel.js +0 -2
- package/dist/src/collect-message-media-paths.js +10 -1
- package/dist/src/friday-session.js +34 -10
- package/dist/src/history/normalize-message.js +22 -8
- package/dist/src/http/handlers/agent-config.js +10 -4
- package/dist/src/http/handlers/cancel.js +4 -2
- package/dist/src/http/handlers/device-approve.js +3 -1
- package/dist/src/http/handlers/files-download.js +6 -8
- package/dist/src/http/handlers/files.js +1 -1
- package/dist/src/http/handlers/health.js +18 -4
- package/dist/src/http/handlers/history-messages.js +1 -1
- package/dist/src/http/handlers/history-sessions.js +5 -3
- package/dist/src/http/handlers/messages.js +34 -11
- package/dist/src/http/handlers/models-list.js +1 -1
- package/dist/src/http/handlers/nodes-approve.js +1 -6
- package/dist/src/http/handlers/plugin-info.js +1 -1
- package/dist/src/http/server.js +4 -2
- package/dist/src/link-preview/og-parse.js +3 -1
- package/dist/src/plugin-install-info.js +4 -1
- package/dist/src/session/session-manager.js +9 -3
- package/dist/src/session-usage-store.js +3 -1
- package/dist/src/skills-discovery.d.ts +5 -4
- package/dist/src/skills-discovery.js +27 -22
- package/dist/src/sse/offline-queue.js +4 -1
- package/dist/src/tool-catalog.js +2 -3
- package/dist/src/upgrade-runtime.d.ts +1 -1
- package/dist/src/version.js +3 -1
- package/index.ts +43 -35
- package/install.js +131 -43
- package/package.json +10 -1
- package/src/agent/abort-run.ts +2 -3
- package/src/agent/dispatch-bridge.ts +2 -1
- package/src/agent/media-bridge.ts +9 -2
- package/src/agent/node-pairing-bridge.ts +29 -15
- package/src/agent/operator-scope.test.ts +66 -0
- package/src/agent/operator-scope.ts +63 -0
- package/src/agent/run-usage-accumulator.ts +4 -2
- package/src/agent/subagent-registry.ts +0 -4
- package/src/agent-run-context-bridge.ts +3 -1
- package/src/channel-actions.test.ts +10 -4
- package/src/channel-actions.ts +3 -1
- package/src/channel.outbound.test.ts +18 -4
- package/src/channel.ts +121 -123
- package/src/collect-message-media-paths.ts +15 -6
- package/src/config.ts +1 -4
- package/src/e2e/agents-list.e2e.test.ts +9 -2
- package/src/e2e/attachments-inbound.e2e.test.ts +5 -1
- package/src/e2e/attachments-outbound.e2e.test.ts +7 -2
- package/src/e2e/auto-approve.integration.test.ts +13 -7
- package/src/e2e/cancel-reconnect-errors.e2e.test.ts +18 -3
- package/src/e2e/connect-and-connected.e2e.test.ts +5 -1
- package/src/e2e/offline-replay.e2e.test.ts +17 -3
- package/src/e2e/send-text.e2e.test.ts +11 -2
- package/src/e2e/slash-commands.e2e.test.ts +5 -1
- package/src/e2e/status-cors-auth.e2e.test.ts +11 -2
- package/src/e2e/subagent-smoke.e2e.test.ts +68 -28
- package/src/e2e/subagent.e2e.test.ts +136 -53
- package/src/e2e/tool-lifecycle.e2e.test.ts +5 -1
- package/src/friday-session.forward-agent.test.ts +44 -12
- package/src/friday-session.ts +44 -20
- package/src/history/normalize-message.test.ts +35 -8
- package/src/history/normalize-message.ts +24 -12
- package/src/history/read-transcript.ts +1 -4
- package/src/http/handlers/agent-config.test.ts +10 -3
- package/src/http/handlers/agent-config.ts +22 -8
- package/src/http/handlers/agents-list.test.ts +1 -5
- package/src/http/handlers/cancel.test.ts +12 -3
- package/src/http/handlers/cancel.ts +4 -2
- package/src/http/handlers/device-approve.test.ts +12 -3
- package/src/http/handlers/device-approve.ts +33 -21
- package/src/http/handlers/files-download.ts +17 -13
- package/src/http/handlers/files.test.ts +8 -2
- package/src/http/handlers/files.ts +21 -7
- package/src/http/handlers/health.test.ts +43 -11
- package/src/http/handlers/health.ts +22 -6
- package/src/http/handlers/history-messages.test.ts +51 -9
- package/src/http/handlers/history-messages.ts +4 -1
- package/src/http/handlers/history-sessions.test.ts +46 -9
- package/src/http/handlers/history-sessions.ts +5 -3
- package/src/http/handlers/history-set-title.test.ts +14 -5
- package/src/http/handlers/link-preview.test.ts +57 -16
- package/src/http/handlers/link-preview.ts +4 -1
- package/src/http/handlers/messages.test.ts +12 -8
- package/src/http/handlers/messages.ts +67 -19
- package/src/http/handlers/models-list.ts +14 -8
- package/src/http/handlers/nodes-approve.test.ts +15 -4
- package/src/http/handlers/nodes-approve.ts +38 -40
- package/src/http/handlers/plugin-info.ts +5 -6
- package/src/http/handlers/plugin-upgrade.ts +4 -1
- package/src/http/handlers/sse.ts +3 -1
- package/src/http/server.ts +9 -6
- package/src/link-preview/og-parse.test.ts +6 -2
- package/src/link-preview/og-parse.ts +10 -3
- package/src/link-preview/preview-service.ts +4 -1
- package/src/link-preview/ssrf-guard.test.ts +72 -15
- package/src/link-preview/ssrf-guard.ts +2 -1
- package/src/media-fetch.test.ts +7 -2
- package/src/media-fetch.ts +1 -2
- package/src/openclaw.d.ts +26 -9
- package/src/plugin-install-info.ts +20 -9
- package/src/run-metadata.ts +2 -1
- package/src/session/session-manager.ts +19 -11
- package/src/session-usage-snapshot.ts +3 -1
- package/src/session-usage-store.ts +3 -1
- package/src/skills-discovery.test.ts +14 -10
- package/src/skills-discovery.ts +43 -27
- package/src/sse/emitter.test.ts +1 -1
- package/src/sse/emitter.ts +9 -3
- package/src/sse/offline-queue.ts +17 -8
- package/src/test-support/app-simulator.ts +17 -3
- package/src/test-support/mock-dispatch.ts +17 -4
- package/src/thinking-levels.ts +3 -1
- package/src/tool-catalog.ts +16 -7
- package/src/upgrade-runtime.ts +4 -2
- package/src/version.ts +5 -1
- package/tsconfig.json +1 -1
package/install.js
CHANGED
|
@@ -14,11 +14,7 @@ function realHome() {
|
|
|
14
14
|
const h = execSync(`sh -c 'echo ~${sudoUser}'`, { encoding: "utf8" }).trim();
|
|
15
15
|
if (h && !h.startsWith("~") && existsSync(h)) return h;
|
|
16
16
|
} catch {}
|
|
17
|
-
for (const g of [
|
|
18
|
-
`/home/${sudoUser}`,
|
|
19
|
-
`/Users/${sudoUser}`,
|
|
20
|
-
`C:\\Users\\${sudoUser}`,
|
|
21
|
-
]) {
|
|
17
|
+
for (const g of [`/home/${sudoUser}`, `/Users/${sudoUser}`, `C:\\Users\\${sudoUser}`]) {
|
|
22
18
|
if (existsSync(g)) return g;
|
|
23
19
|
}
|
|
24
20
|
return current;
|
|
@@ -30,13 +26,23 @@ const OPENCLAW_CONFIG = join(USER_HOME, ".openclaw", "openclaw.json");
|
|
|
30
26
|
const G = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
31
27
|
const Y = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
32
28
|
const R = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
33
|
-
function log(msg) {
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
function log(msg) {
|
|
30
|
+
console.log(` ${msg}`);
|
|
31
|
+
}
|
|
32
|
+
function warn(msg) {
|
|
33
|
+
console.log(` ${Y("!")} ${msg}`);
|
|
34
|
+
}
|
|
35
|
+
function err(msg) {
|
|
36
|
+
console.error(` ${R("X")} ${msg}`);
|
|
37
|
+
}
|
|
36
38
|
|
|
37
39
|
function has(cmd) {
|
|
38
|
-
try {
|
|
39
|
-
|
|
40
|
+
try {
|
|
41
|
+
execSync(`${cmd} --version`, { stdio: "ignore" });
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
let openclawCmd = "openclaw";
|
|
@@ -79,7 +85,10 @@ if (!hasOpenclaw()) {
|
|
|
79
85
|
let tooOld = false;
|
|
80
86
|
for (let i = 0; i < 3; i++) {
|
|
81
87
|
if (cur[i] > MIN_OPENCLAW[i]) break;
|
|
82
|
-
if (cur[i] < MIN_OPENCLAW[i]) {
|
|
88
|
+
if (cur[i] < MIN_OPENCLAW[i]) {
|
|
89
|
+
tooOld = true;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
83
92
|
}
|
|
84
93
|
if (tooOld) {
|
|
85
94
|
err(`OpenClaw version ${m[0]} is too old.`);
|
|
@@ -100,7 +109,7 @@ log("Installing Friday Next channel plugin...");
|
|
|
100
109
|
try {
|
|
101
110
|
const out = execSync(
|
|
102
111
|
`${openclawCmd} plugins install @syengup/friday-channel-next@latest --force`,
|
|
103
|
-
{ encoding: "utf8", stdio: "pipe", timeout: 120000 }
|
|
112
|
+
{ encoding: "utf8", stdio: "pipe", timeout: 120000 },
|
|
104
113
|
);
|
|
105
114
|
if (out.trim()) console.log(out.trim());
|
|
106
115
|
log("Plugin registered with install record — auto-upgrade enabled.");
|
|
@@ -108,8 +117,12 @@ try {
|
|
|
108
117
|
// Remove old manual install to avoid "duplicate plugin id" warning.
|
|
109
118
|
const legacyDir = join(USER_HOME, ".openclaw", "extensions", "friday-channel-next");
|
|
110
119
|
if (existsSync(legacyDir)) {
|
|
111
|
-
try {
|
|
112
|
-
|
|
120
|
+
try {
|
|
121
|
+
rmSync(legacyDir, { recursive: true, force: true });
|
|
122
|
+
log("Removed legacy manual install.");
|
|
123
|
+
} catch {
|
|
124
|
+
/* non-critical */
|
|
125
|
+
}
|
|
113
126
|
}
|
|
114
127
|
} catch (e) {
|
|
115
128
|
const msg = (e.stderr || e.stdout || e.message || "").toString();
|
|
@@ -150,7 +163,10 @@ function setConfig(path, value) {
|
|
|
150
163
|
}
|
|
151
164
|
|
|
152
165
|
function ensureArrayContains(arr, item) {
|
|
153
|
-
if (!arr.includes(item)) {
|
|
166
|
+
if (!arr.includes(item)) {
|
|
167
|
+
arr.push(item);
|
|
168
|
+
configChanged = true;
|
|
169
|
+
}
|
|
154
170
|
}
|
|
155
171
|
|
|
156
172
|
// Plugins
|
|
@@ -161,30 +177,58 @@ ensureArrayContains(config.plugins.allow, "canvas");
|
|
|
161
177
|
|
|
162
178
|
if (!config.plugins.entries) config.plugins.entries = {};
|
|
163
179
|
for (const id of ["friday-next", "canvas"]) {
|
|
164
|
-
if (!config.plugins.entries[id]) {
|
|
165
|
-
|
|
180
|
+
if (!config.plugins.entries[id]) {
|
|
181
|
+
config.plugins.entries[id] = { enabled: true };
|
|
182
|
+
configChanged = true;
|
|
183
|
+
} else if (!config.plugins.entries[id].enabled) {
|
|
184
|
+
config.plugins.entries[id].enabled = true;
|
|
185
|
+
configChanged = true;
|
|
186
|
+
}
|
|
166
187
|
}
|
|
167
188
|
|
|
168
189
|
// llm_output hook requires allowConversationAccess for non-bundled plugins.
|
|
169
|
-
if (!config.plugins.entries["friday-next"].hooks) {
|
|
170
|
-
|
|
190
|
+
if (!config.plugins.entries["friday-next"].hooks) {
|
|
191
|
+
config.plugins.entries["friday-next"].hooks = {};
|
|
192
|
+
configChanged = true;
|
|
193
|
+
}
|
|
194
|
+
if (!config.plugins.entries["friday-next"].hooks.allowConversationAccess) {
|
|
195
|
+
config.plugins.entries["friday-next"].hooks.allowConversationAccess = true;
|
|
196
|
+
configChanged = true;
|
|
197
|
+
}
|
|
171
198
|
|
|
172
199
|
// Channel
|
|
173
200
|
if (!config.channels) config.channels = {};
|
|
174
|
-
if (!config.channels["friday-next"]) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
201
|
+
if (!config.channels["friday-next"]) {
|
|
202
|
+
config.channels["friday-next"] = { enabled: true, transport: "http+sse" };
|
|
203
|
+
configChanged = true;
|
|
204
|
+
} else {
|
|
205
|
+
if (!config.channels["friday-next"].enabled) {
|
|
206
|
+
config.channels["friday-next"].enabled = true;
|
|
207
|
+
configChanged = true;
|
|
208
|
+
}
|
|
209
|
+
if (!config.channels["friday-next"].transport) {
|
|
210
|
+
config.channels["friday-next"].transport = "http+sse";
|
|
211
|
+
configChanged = true;
|
|
212
|
+
}
|
|
178
213
|
}
|
|
179
214
|
|
|
180
215
|
// Gateway bind + nodes
|
|
181
216
|
if (!config.gateway) config.gateway = {};
|
|
182
|
-
if (config.gateway.bind !== "lan") {
|
|
217
|
+
if (config.gateway.bind !== "lan") {
|
|
218
|
+
config.gateway.bind = "lan";
|
|
219
|
+
configChanged = true;
|
|
220
|
+
}
|
|
183
221
|
if (!config.gateway.nodes) config.gateway.nodes = {};
|
|
184
222
|
if (!Array.isArray(config.gateway.nodes.allowCommands)) config.gateway.nodes.allowCommands = [];
|
|
185
223
|
for (const cmd of [
|
|
186
|
-
"canvas.navigate",
|
|
187
|
-
"canvas.
|
|
224
|
+
"canvas.navigate",
|
|
225
|
+
"canvas.present",
|
|
226
|
+
"canvas.hide",
|
|
227
|
+
"canvas.eval",
|
|
228
|
+
"canvas.snapshot",
|
|
229
|
+
"canvas.a2ui.push",
|
|
230
|
+
"canvas.a2ui.reset",
|
|
231
|
+
"canvas.a2ui.pushJSONL",
|
|
188
232
|
]) {
|
|
189
233
|
ensureArrayContains(config.gateway.nodes.allowCommands, cmd);
|
|
190
234
|
}
|
|
@@ -193,7 +237,11 @@ for (const cmd of [
|
|
|
193
237
|
if (!config.agents) config.agents = {};
|
|
194
238
|
if (!Array.isArray(config.agents.list)) config.agents.list = [];
|
|
195
239
|
let mainAgent = config.agents.list.find((a) => a.id === "main");
|
|
196
|
-
if (!mainAgent) {
|
|
240
|
+
if (!mainAgent) {
|
|
241
|
+
mainAgent = { id: "main" };
|
|
242
|
+
config.agents.list.push(mainAgent);
|
|
243
|
+
configChanged = true;
|
|
244
|
+
}
|
|
197
245
|
if (!mainAgent.tools) mainAgent.tools = {};
|
|
198
246
|
if (!Array.isArray(mainAgent.tools.alsoAllow)) mainAgent.tools.alsoAllow = [];
|
|
199
247
|
for (const tool of ["canvas", "nodes"]) {
|
|
@@ -202,7 +250,10 @@ for (const tool of ["canvas", "nodes"]) {
|
|
|
202
250
|
if (Array.isArray(mainAgent.tools.deny)) {
|
|
203
251
|
for (const tool of ["canvas", "nodes"]) {
|
|
204
252
|
const idx = mainAgent.tools.deny.indexOf(tool);
|
|
205
|
-
if (idx !== -1) {
|
|
253
|
+
if (idx !== -1) {
|
|
254
|
+
mainAgent.tools.deny.splice(idx, 1);
|
|
255
|
+
configChanged = true;
|
|
256
|
+
}
|
|
206
257
|
}
|
|
207
258
|
}
|
|
208
259
|
|
|
@@ -224,7 +275,11 @@ log("Restarting OpenClaw gateway... (this can take 20-30s)");
|
|
|
224
275
|
try {
|
|
225
276
|
// A full gateway restart commonly takes 20s+ on a fresh boot; give it plenty of room
|
|
226
277
|
// so we don't kill it mid-restart and report a false failure.
|
|
227
|
-
const out = execSync(`${openclawCmd} gateway restart`, {
|
|
278
|
+
const out = execSync(`${openclawCmd} gateway restart`, {
|
|
279
|
+
encoding: "utf8",
|
|
280
|
+
stdio: "pipe",
|
|
281
|
+
timeout: 90000,
|
|
282
|
+
});
|
|
228
283
|
if (out.trim()) console.log(out.trim());
|
|
229
284
|
} catch (e) {
|
|
230
285
|
if (e.stdout?.trim()) console.log(e.stdout.trim());
|
|
@@ -250,15 +305,18 @@ function getLanIp() {
|
|
|
250
305
|
return "127.0.0.1";
|
|
251
306
|
}
|
|
252
307
|
|
|
253
|
-
try {
|
|
308
|
+
try {
|
|
309
|
+
config = JSON.parse(readFileSync(OPENCLAW_CONFIG, "utf8"));
|
|
310
|
+
} catch {
|
|
311
|
+
config = {};
|
|
312
|
+
}
|
|
254
313
|
|
|
255
314
|
const gatewayPort = config.gateway?.port || 18789;
|
|
256
315
|
const gatewayToken = config.gateway?.auth?.token || "(not set)";
|
|
257
316
|
const bindMode = config.gateway?.bind || "localhost";
|
|
258
317
|
|
|
259
|
-
const gatewayUrl =
|
|
260
|
-
? `http://${getLanIp()}:${gatewayPort}`
|
|
261
|
-
: `http://127.0.0.1:${gatewayPort}`;
|
|
318
|
+
const gatewayUrl =
|
|
319
|
+
bindMode === "lan" ? `http://${getLanIp()}:${gatewayPort}` : `http://127.0.0.1:${gatewayPort}`;
|
|
262
320
|
|
|
263
321
|
// Always verify against loopback: the gateway binds 0.0.0.0 so it's reachable here,
|
|
264
322
|
// and this avoids false negatives from LAN/NAT routing of the advertised IP.
|
|
@@ -272,19 +330,38 @@ async function verifyGateway(url, token, retries = 30) {
|
|
|
272
330
|
try {
|
|
273
331
|
const res = await new Promise((resolve, reject) => {
|
|
274
332
|
const req = http.request(
|
|
275
|
-
{
|
|
276
|
-
|
|
277
|
-
|
|
333
|
+
{
|
|
334
|
+
hostname,
|
|
335
|
+
port,
|
|
336
|
+
path: "/friday-next/status",
|
|
337
|
+
method: "GET",
|
|
338
|
+
headers: { authorization: `Bearer ${token}` },
|
|
339
|
+
timeout: 5000,
|
|
340
|
+
},
|
|
341
|
+
(res) => {
|
|
342
|
+
let body = "";
|
|
343
|
+
res.on("data", (c) => (body += c));
|
|
344
|
+
res.on("end", () => resolve({ status: res.statusCode, body }));
|
|
345
|
+
},
|
|
278
346
|
);
|
|
279
347
|
req.on("error", reject);
|
|
280
|
-
req.on("timeout", () => {
|
|
348
|
+
req.on("timeout", () => {
|
|
349
|
+
req.destroy();
|
|
350
|
+
reject(new Error("timeout"));
|
|
351
|
+
});
|
|
281
352
|
req.end();
|
|
282
353
|
});
|
|
283
354
|
if (res.status === 200) {
|
|
284
355
|
try {
|
|
285
356
|
const data = JSON.parse(res.body);
|
|
286
357
|
if (data.ok) {
|
|
287
|
-
log(
|
|
358
|
+
log(
|
|
359
|
+
"Gateway verified OK (friday-next " +
|
|
360
|
+
data.version +
|
|
361
|
+
", " +
|
|
362
|
+
data.connections +
|
|
363
|
+
" connections).",
|
|
364
|
+
);
|
|
288
365
|
return true;
|
|
289
366
|
}
|
|
290
367
|
warn("Plugin responded but ok=false — " + JSON.stringify(data));
|
|
@@ -294,8 +371,14 @@ async function verifyGateway(url, token, retries = 30) {
|
|
|
294
371
|
continue;
|
|
295
372
|
}
|
|
296
373
|
}
|
|
297
|
-
if (res.status === 401) {
|
|
298
|
-
|
|
374
|
+
if (res.status === 401) {
|
|
375
|
+
warn("Auth token mismatch — check gateway.auth.token.");
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
if (res.status === 404) {
|
|
379
|
+
warn("Route not found — plugin may not be loaded.");
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
299
382
|
if (i < retries) warn(`Gateway responded ${res.status}, retrying (${i}/${retries})...`);
|
|
300
383
|
} catch {
|
|
301
384
|
if (i < retries) warn(`Gateway not reachable, retrying (${i}/${retries})...`);
|
|
@@ -404,14 +487,19 @@ async function detectPublicIp() {
|
|
|
404
487
|
const ipStr = await new Promise((resolve, reject) => {
|
|
405
488
|
const req = http.get(url, { timeout: 3000 }, (res) => {
|
|
406
489
|
let body = "";
|
|
407
|
-
res.on("data", (c) => body += c);
|
|
490
|
+
res.on("data", (c) => (body += c));
|
|
408
491
|
res.on("end", () => resolve(body.trim()));
|
|
409
492
|
});
|
|
410
493
|
req.on("error", reject);
|
|
411
|
-
req.on("timeout", () => {
|
|
494
|
+
req.on("timeout", () => {
|
|
495
|
+
req.destroy();
|
|
496
|
+
reject(new Error("timeout"));
|
|
497
|
+
});
|
|
412
498
|
});
|
|
413
499
|
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ipStr)) return ipStr;
|
|
414
|
-
} catch {
|
|
500
|
+
} catch {
|
|
501
|
+
/* try next */
|
|
502
|
+
}
|
|
415
503
|
}
|
|
416
504
|
return null;
|
|
417
505
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syengup/friday-channel-next",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"description": "OpenClaw Friday Next Apple channel plugin",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"lint": "eslint .",
|
|
18
|
+
"lint:fix": "eslint . --fix",
|
|
19
|
+
"format": "prettier --write .",
|
|
20
|
+
"format:check": "prettier --check .",
|
|
17
21
|
"prepublishOnly": "pnpm build && rm -rf dist/attachments",
|
|
18
22
|
"test": "npm run test:unit && npm run test:e2e",
|
|
19
23
|
"test:unit": "vitest run",
|
|
@@ -58,12 +62,17 @@
|
|
|
58
62
|
"qrcode-terminal": "^0.12.0"
|
|
59
63
|
},
|
|
60
64
|
"devDependencies": {
|
|
65
|
+
"@eslint/js": "^10.0.1",
|
|
61
66
|
"@types/node": "^25.6.0",
|
|
62
67
|
"chalk": "^5.6.2",
|
|
68
|
+
"eslint": "^10.5.0",
|
|
69
|
+
"eslint-config-prettier": "^10.1.8",
|
|
63
70
|
"jiti": "^2.6.1",
|
|
64
71
|
"json5": "^2.2.3",
|
|
72
|
+
"prettier": "^3.8.4",
|
|
65
73
|
"tslog": "^4.10.2",
|
|
66
74
|
"typescript": "^6.0.3",
|
|
75
|
+
"typescript-eslint": "^8.61.1",
|
|
67
76
|
"vitest": "^4.1.5",
|
|
68
77
|
"zod": "^4.3.6"
|
|
69
78
|
}
|
package/src/agent/abort-run.ts
CHANGED
|
@@ -12,9 +12,8 @@ export async function abortRunForSessionKey(sessionKey: string): Promise<AbortRu
|
|
|
12
12
|
const key = sessionKey.trim();
|
|
13
13
|
if (!key) return { aborted: false, drained: false };
|
|
14
14
|
try {
|
|
15
|
-
const { resolveActiveEmbeddedRunSessionId, abortAndDrainAgentHarnessRun } =
|
|
16
|
-
"openclaw/plugin-sdk/agent-harness"
|
|
17
|
-
);
|
|
15
|
+
const { resolveActiveEmbeddedRunSessionId, abortAndDrainAgentHarnessRun } =
|
|
16
|
+
await import("openclaw/plugin-sdk/agent-harness");
|
|
18
17
|
const sessionId = resolveActiveEmbeddedRunSessionId(key);
|
|
19
18
|
if (!sessionId) return { aborted: false, drained: false };
|
|
20
19
|
const result = await abortAndDrainAgentHarnessRun({ sessionId, sessionKey: key });
|
|
@@ -11,7 +11,8 @@ import crypto from "node:crypto";
|
|
|
11
11
|
*/
|
|
12
12
|
export async function resolveMediaMaxBytes(mimeType: string): Promise<number | undefined> {
|
|
13
13
|
try {
|
|
14
|
-
const { maxBytesForKind, mediaKindFromMime } =
|
|
14
|
+
const { maxBytesForKind, mediaKindFromMime } =
|
|
15
|
+
await import("openclaw/plugin-sdk/media-runtime");
|
|
15
16
|
return maxBytesForKind(mediaKindFromMime(mimeType) ?? "document");
|
|
16
17
|
} catch {
|
|
17
18
|
return undefined;
|
|
@@ -31,7 +32,13 @@ export async function saveInboundMediaBuffer(
|
|
|
31
32
|
// Pass the original filename (5th arg) so core's media-store preserves the
|
|
32
33
|
// name+extension instead of saving a bare uuid. Otherwise the agent receives
|
|
33
34
|
// `[media attached: file://.../inbound/<uuid>]` with no file-format signal.
|
|
34
|
-
const saved = await sdk.saveMediaBuffer(
|
|
35
|
+
const saved = await sdk.saveMediaBuffer(
|
|
36
|
+
buffer,
|
|
37
|
+
mimeType,
|
|
38
|
+
"inbound",
|
|
39
|
+
maxBytes,
|
|
40
|
+
originalFilename,
|
|
41
|
+
);
|
|
35
42
|
if (saved?.id && saved?.path) return { id: saved.id, path: saved.path };
|
|
36
43
|
} catch {
|
|
37
44
|
// fallback for tests or stripped runtime
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { existsSync, readdirSync, realpathSync } from "node:fs";
|
|
2
2
|
import { delimiter, dirname, join } from "node:path";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Results come from the untyped OpenClaw dist module, so the resolved shapes are
|
|
5
|
+
// `any` at this host boundary — callers read dynamic fields (.pending, .status, …).
|
|
6
|
+
type ListNodePairingFn = () => Promise<any>;
|
|
7
|
+
type ApproveNodePairingFn = (
|
|
8
|
+
requestId: string,
|
|
9
|
+
options: { callerScopes?: unknown },
|
|
10
|
+
) => Promise<any>;
|
|
11
|
+
type NodePairingModule = {
|
|
12
|
+
listNodePairing: ListNodePairingFn;
|
|
13
|
+
approveNodePairing: ApproveNodePairingFn;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let cache: NodePairingModule | null = null;
|
|
5
17
|
|
|
6
18
|
function resolveOpenClawDistFromPath(): string | null {
|
|
7
19
|
// Walk PATH looking for the openclaw binary, then resolve its real
|
|
@@ -16,7 +28,9 @@ function resolveOpenClawDistFromPath(): string | null {
|
|
|
16
28
|
const dist = join(dirname(real), "dist");
|
|
17
29
|
readdirSync(dist);
|
|
18
30
|
return dist;
|
|
19
|
-
} catch {
|
|
31
|
+
} catch {
|
|
32
|
+
// Not a real dist dir — keep walking PATH.
|
|
33
|
+
}
|
|
20
34
|
}
|
|
21
35
|
return null;
|
|
22
36
|
}
|
|
@@ -43,15 +57,17 @@ function resolveOpenClawDist(): string {
|
|
|
43
57
|
].filter((v): v is string => typeof v === "string" && v.length > 0);
|
|
44
58
|
|
|
45
59
|
for (const root of candidates) {
|
|
46
|
-
try {
|
|
60
|
+
try {
|
|
61
|
+
readdirSync(root);
|
|
62
|
+
return root;
|
|
63
|
+
} catch {
|
|
64
|
+
// Candidate dir doesn't exist — try the next one.
|
|
65
|
+
}
|
|
47
66
|
}
|
|
48
67
|
throw new Error("OpenClaw dist directory not found. Set OPENCLAW_DIST env var.");
|
|
49
68
|
}
|
|
50
69
|
|
|
51
|
-
export async function loadNodePairingModule(): Promise<{
|
|
52
|
-
listNodePairing: Function;
|
|
53
|
-
approveNodePairing: Function;
|
|
54
|
-
}> {
|
|
70
|
+
export async function loadNodePairingModule(): Promise<NodePairingModule> {
|
|
55
71
|
if (cache) return cache;
|
|
56
72
|
const dist = resolveOpenClawDist();
|
|
57
73
|
const file = readdirSync(dist).find(
|
|
@@ -63,12 +79,13 @@ export async function loadNodePairingModule(): Promise<{
|
|
|
63
79
|
// bundled module uses `export { listNodePairing as r, … }`. Resolve the
|
|
64
80
|
// correct functions by Function.name, which preserves the original name.
|
|
65
81
|
const mod = await import(join(dist, file));
|
|
66
|
-
let listNodePairing:
|
|
67
|
-
let approveNodePairing:
|
|
82
|
+
let listNodePairing: ListNodePairingFn | undefined;
|
|
83
|
+
let approveNodePairing: ApproveNodePairingFn | undefined;
|
|
68
84
|
for (const value of Object.values(mod)) {
|
|
69
85
|
if (typeof value === "function") {
|
|
70
|
-
if (value.name === "listNodePairing") listNodePairing = value;
|
|
71
|
-
else if (value.name === "approveNodePairing")
|
|
86
|
+
if (value.name === "listNodePairing") listNodePairing = value as ListNodePairingFn;
|
|
87
|
+
else if (value.name === "approveNodePairing")
|
|
88
|
+
approveNodePairing = value as ApproveNodePairingFn;
|
|
72
89
|
}
|
|
73
90
|
}
|
|
74
91
|
if (!listNodePairing || !approveNodePairing) {
|
|
@@ -79,9 +96,6 @@ export async function loadNodePairingModule(): Promise<{
|
|
|
79
96
|
}
|
|
80
97
|
|
|
81
98
|
/** Vitest-only: inject mock pairing functions. */
|
|
82
|
-
export function __setMockNodePairingForTests(mock: {
|
|
83
|
-
listNodePairing: Function;
|
|
84
|
-
approveNodePairing: Function;
|
|
85
|
-
}): void {
|
|
99
|
+
export function __setMockNodePairingForTests(mock: NodePairingModule): void {
|
|
86
100
|
cache = mock;
|
|
87
101
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// The helper imports the live scope getter from core; the pure function under test
|
|
4
|
+
// never calls it, but the module-level import must resolve. Mock it so the unit test
|
|
5
|
+
// does not depend on the OpenClaw dist runtime.
|
|
6
|
+
const getScope = vi.fn();
|
|
7
|
+
vi.mock("openclaw/plugin-sdk/plugin-runtime", () => ({
|
|
8
|
+
getPluginRuntimeGatewayRequestScope: () => getScope(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import { elevateScopeForSubagentSpawn, ensureSubagentSpawnScope } from "./operator-scope.js";
|
|
12
|
+
|
|
13
|
+
function makeScope(scopes: string[]) {
|
|
14
|
+
return { client: { connect: { role: "operator", scopes } } };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("elevateScopeForSubagentSpawn", () => {
|
|
18
|
+
it("adds operator.write + operator.read to an empty plugin-route scope", () => {
|
|
19
|
+
// friday-next routes register with auth:"plugin", which core gives EMPTY operator
|
|
20
|
+
// scopes. Subagent spawn re-enters the gateway `agent` method (requires
|
|
21
|
+
// operator.write) and fails with "missing scope: operator.write" without this.
|
|
22
|
+
const scope = makeScope([]);
|
|
23
|
+
const added = elevateScopeForSubagentSpawn(scope);
|
|
24
|
+
expect(scope.client.connect.scopes).toContain("operator.write");
|
|
25
|
+
expect(scope.client.connect.scopes).toContain("operator.read");
|
|
26
|
+
expect(added).toEqual(["operator.write", "operator.read"]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("is idempotent — does not duplicate already-present scopes", () => {
|
|
30
|
+
const scope = makeScope(["operator.write"]);
|
|
31
|
+
const added = elevateScopeForSubagentSpawn(scope);
|
|
32
|
+
expect(added).toEqual(["operator.read"]);
|
|
33
|
+
expect(scope.client.connect.scopes.filter((s) => s === "operator.write")).toHaveLength(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("preserves unrelated existing scopes", () => {
|
|
37
|
+
const scope = makeScope(["operator.admin"]);
|
|
38
|
+
elevateScopeForSubagentSpawn(scope);
|
|
39
|
+
expect(scope.client.connect.scopes).toContain("operator.admin");
|
|
40
|
+
expect(scope.client.connect.scopes).toContain("operator.write");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns [] and never throws when no scope/client is present", () => {
|
|
44
|
+
expect(elevateScopeForSubagentSpawn(undefined)).toEqual([]);
|
|
45
|
+
expect(elevateScopeForSubagentSpawn(null)).toEqual([]);
|
|
46
|
+
expect(elevateScopeForSubagentSpawn({})).toEqual([]);
|
|
47
|
+
expect(elevateScopeForSubagentSpawn({ client: { connect: {} } })).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("ensureSubagentSpawnScope", () => {
|
|
52
|
+
it("elevates the live scope returned by the SDK getter", () => {
|
|
53
|
+
const scope = makeScope([]);
|
|
54
|
+
getScope.mockReturnValue(scope);
|
|
55
|
+
const added = ensureSubagentSpawnScope();
|
|
56
|
+
expect(added).toEqual(["operator.write", "operator.read"]);
|
|
57
|
+
expect(scope.client.connect.scopes).toContain("operator.write");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("swallows errors from the getter and returns []", () => {
|
|
61
|
+
getScope.mockImplementation(() => {
|
|
62
|
+
throw new Error("no scope");
|
|
63
|
+
});
|
|
64
|
+
expect(ensureSubagentSpawnScope()).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Operator-scope elevation for friday-next agent dispatch.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists: friday-next registers all its routes with auth:"plugin" (it does
|
|
4
|
+
// its own device-token auth, not the gateway operator token). Core's
|
|
5
|
+
// createPluginRouteRuntimeScope gives auth!="gateway" routes an EMPTY operator-scope
|
|
6
|
+
// set. When an agent dispatched from such a route spawns a subagent, the spawn
|
|
7
|
+
// re-enters the in-process gateway `agent` method, which requires `operator.write`
|
|
8
|
+
// (core-descriptors: { name:"agent", scope:"operator.write" }). With an empty ambient
|
|
9
|
+
// scope the spawn fails with `{"error":"missing scope: operator.write"}`.
|
|
10
|
+
//
|
|
11
|
+
// The subagent spawn reads getPluginRuntimeGatewayRequestScope() at spawn time and
|
|
12
|
+
// uses that scope's client for authorization. Because AsyncLocalStorage returns the
|
|
13
|
+
// SAME store object reference, mutating its client.connect.scopes once — before we
|
|
14
|
+
// kick off the dispatch, while still inside the route's ALS context — propagates the
|
|
15
|
+
// elevated scopes to every later reader, including the subagent spawn.
|
|
16
|
+
//
|
|
17
|
+
// Subagent lifecycle admin methods (sessions.patch/delete) are unaffected: core pins
|
|
18
|
+
// those to ADMIN_SCOPE via a synthetic client, so only the `agent` method depends on
|
|
19
|
+
// this ambient operator.write. See memory: subagent-spawn-missing-operator-write.
|
|
20
|
+
import { getPluginRuntimeGatewayRequestScope } from "openclaw/plugin-sdk/plugin-runtime";
|
|
21
|
+
|
|
22
|
+
/** Operator scopes the friday-next dispatch needs so agents can spawn subagents. */
|
|
23
|
+
const REQUIRED_OPERATOR_SCOPES = ["operator.write", "operator.read"] as const;
|
|
24
|
+
|
|
25
|
+
type ScopeLike =
|
|
26
|
+
| {
|
|
27
|
+
client?: { connect?: { scopes?: unknown } };
|
|
28
|
+
}
|
|
29
|
+
| null
|
|
30
|
+
| undefined;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Adds the required operator scopes to a gateway-request-scope's
|
|
34
|
+
* `client.connect.scopes` array in place. Pure and idempotent.
|
|
35
|
+
* Returns the scopes that were actually added (empty if none / no array present).
|
|
36
|
+
*/
|
|
37
|
+
export function elevateScopeForSubagentSpawn(scope: ScopeLike): string[] {
|
|
38
|
+
const connect = scope?.client?.connect;
|
|
39
|
+
if (!connect || !Array.isArray(connect.scopes)) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
const scopes = connect.scopes as string[];
|
|
43
|
+
const added: string[] = [];
|
|
44
|
+
for (const scopeName of REQUIRED_OPERATOR_SCOPES) {
|
|
45
|
+
if (!scopes.includes(scopeName)) {
|
|
46
|
+
scopes.push(scopeName);
|
|
47
|
+
added.push(scopeName);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return added;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fetches the live plugin gateway-request-scope and elevates it so the dispatched
|
|
55
|
+
* agent can spawn subagents. Never throws — returns the scopes added (or []).
|
|
56
|
+
*/
|
|
57
|
+
export function ensureSubagentSpawnScope(): string[] {
|
|
58
|
+
try {
|
|
59
|
+
return elevateScopeForSubagentSpawn(getPluginRuntimeGatewayRequestScope());
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -39,8 +39,10 @@ export function accumulateRunUsage(
|
|
|
39
39
|
const entry = ensure(runId);
|
|
40
40
|
if (typeof usage.input === "number" && usage.input > 0) entry.input += usage.input;
|
|
41
41
|
if (typeof usage.output === "number" && usage.output > 0) entry.output += usage.output;
|
|
42
|
-
if (typeof usage.cacheRead === "number" && usage.cacheRead > 0)
|
|
43
|
-
|
|
42
|
+
if (typeof usage.cacheRead === "number" && usage.cacheRead > 0)
|
|
43
|
+
entry.cacheRead += usage.cacheRead;
|
|
44
|
+
if (typeof usage.cacheWrite === "number" && usage.cacheWrite > 0)
|
|
45
|
+
entry.cacheWrite += usage.cacheWrite;
|
|
44
46
|
if (typeof usage.total === "number" && usage.total > 0) entry.total += usage.total;
|
|
45
47
|
if (model && model.trim()) entry.model = model.trim();
|
|
46
48
|
if (provider && provider.trim()) entry.provider = provider.trim();
|
|
@@ -39,10 +39,6 @@ export function registerSessionKeyForRun(sessionKey: string, runId: string): voi
|
|
|
39
39
|
sessionKeyToRunId.set(sessionKey, runId);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function resolveRunIdForSessionKey(sessionKey: string): string | undefined {
|
|
43
|
-
return sessionKeyToRunId.get(sessionKey);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
42
|
/**
|
|
47
43
|
* Parse OpenClaw announce compound runId:
|
|
48
44
|
* announce:v<version>:<sessionKey>:<bareRunId>
|
|
@@ -26,7 +26,9 @@ function getAgentEventState(): AgentEventStateLike | undefined {
|
|
|
26
26
|
return { runContextById };
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export function getOpenClawAgentRunContext(
|
|
29
|
+
export function getOpenClawAgentRunContext(
|
|
30
|
+
runId: string,
|
|
31
|
+
): OpenClawAgentRunContextBridge | undefined {
|
|
30
32
|
if (!runId) return undefined;
|
|
31
33
|
return getAgentEventState()?.runContextById.get(runId);
|
|
32
34
|
}
|