@byokit/openclaw 0.16.6 → 0.18.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/CHANGELOG.md +9 -0
- package/README.md +23 -0
- package/dist/bridge.d.ts +5 -0
- package/dist/bridge.js +74 -16
- package/dist/delegations.d.ts +38 -0
- package/dist/delegations.js +204 -0
- package/dist/device.d.ts +7 -2
- package/dist/device.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/kit.d.ts +4 -0
- package/dist/kit.js +25 -5
- package/dist/link.js +31 -3
- package/dist/runs.d.ts +23 -1
- package/dist/runs.js +97 -3
- package/dist/types.d.ts +35 -2
- package/package.json +1 -1
- package/plugin/index.js +30 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.18.0 (2026-10-18)
|
|
6
|
+
|
|
7
|
+
- Reattach a paired device to a live-kit run by its existing idempotency key: sequenced replay then live events, a bounded journal with typed truncation, and grant-member isolation. Device disconnect does not interrupt the run; engine-restart reconstruction is not included.
|
|
8
|
+
|
|
9
|
+
## 0.17.0 (2026-10-18)
|
|
10
|
+
|
|
11
|
+
- Add opt-in `RunSpec.delegates` for native `sessions_send`/`sessions_spawn` app-tool admission and typed delegated events, retained exact receipts and subtree cancellation. Targets retain their own accounts; helper/person account sharing is not included.
|
|
12
|
+
- SECURITY: Delegated authority requires a trusted native parent/tool-call receipt and exact child run/session identity; target tools intersect the parent subset, builtins remain gated, unknown children fail closed, and cancellation revokes parked and active calls before awaiting engine cleanup.
|
|
13
|
+
|
|
5
14
|
## 0.16.6 (2026-10-18)
|
|
6
15
|
|
|
7
16
|
- SECURITY: Run `openai-codex/<model>` through the installed Codex CLI on the ChatGPT subscription using the pinned engine's supported CLI backend; keep engine state isolated and prevent the engine from importing or refreshing the installed Codex login.
|
package/README.md
CHANGED
|
@@ -422,6 +422,29 @@ of 394 methods' params and 193 of their results, plus 21 of 55 event payloads; t
|
|
|
422
422
|
the kit, they are not published by the engine, and typing them here would be a guess that does not match the
|
|
423
423
|
gateway. `callDynamic` stays the same shape for a method the pin never declared.
|
|
424
424
|
|
|
425
|
+
## Native delegated runs (opt-in)
|
|
426
|
+
|
|
427
|
+
Pass `delegates: { scout: ['report'] }` on `kit.run` to permit native delegation to that exact engine agent with
|
|
428
|
+
that app-tool subset. Ensure the target member first and configure the engine's own A2A/visibility/subagent policy;
|
|
429
|
+
this option does not enable engine tools or bypass `host.gate`. The parent subset narrows descendants too.
|
|
430
|
+
Tracked `sessions_send` requires an explicit full `sessionKey` and `timeoutSeconds: 0`; tracked `sessions_spawn`
|
|
431
|
+
requires an explicit allowed `agentId` and native runtime (not ACP). Unknown children fail closed.
|
|
432
|
+
|
|
433
|
+
`kit.delegatedRuns()` lists admitted children. `kit.onEvent('byokit.delegated', fn)` reports admission, native streams
|
|
434
|
+
and terminal observations with exact `runId`, `agentId`, `member`, `sessionKey`, `parent` and `root` identity.
|
|
435
|
+
The same identity reaches `ToolHost`; each delegated action has its own gate/approval. Tool arguments/results
|
|
436
|
+
are omitted from this event surface. An accepted native dispatch is not finished work or proof of delivery.
|
|
437
|
+
|
|
438
|
+
Admitted exact run IDs survive parent settlement and are reconciled on reconnect/start. `abort(rootSessionKey)`
|
|
439
|
+
durably revokes its recorded subtree, closes parked gates/active calls, then asks the engine to abort; a child key
|
|
440
|
+
revokes its subtree. An engine resume with a different run ID, or a dispatch with no retained receipt, is not
|
|
441
|
+
silently granted authority. No prefix grants or `allowOnce` bypass are used.
|
|
442
|
+
|
|
443
|
+
**Per-helper account binding is not included:** targets retain their own existing engine credentials, persona,
|
|
444
|
+
workspace and history. This option never copies credentials or gives helpers another person's account. Legacy
|
|
445
|
+
in-flight jobs may retain their exact old session keys and approval owners; all new helper work uses the new
|
|
446
|
+
mapping. Never copy agent SQLite or replay old sent actions. See the [binding contract](../../docs/runtime-kits.md#native-delegated-tool-admission-opt-in).
|
|
447
|
+
|
|
425
448
|
## Owned browser (unqualified handoff)
|
|
426
449
|
|
|
427
450
|
`browser: { executablePath, members }` opts into isolated member Chromium profiles. It requires a closed
|
package/dist/bridge.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Delegations } from './delegations.ts';
|
|
1
2
|
import type { Approval, ChannelMessage, Decision, Member, RunRef, ToolHost, ToolSpec } from './types.ts';
|
|
2
3
|
/**
|
|
3
4
|
* Asks must expire before the plugin's 195 s gate timeout, or the run gets a generic block while the approval
|
|
@@ -42,6 +43,8 @@ export declare class Bridge {
|
|
|
42
43
|
private server?;
|
|
43
44
|
private closing;
|
|
44
45
|
private readonly sockets;
|
|
46
|
+
private readonly owners;
|
|
47
|
+
private readonly delegations?;
|
|
45
48
|
private readonly runs;
|
|
46
49
|
private readonly live;
|
|
47
50
|
private readonly permits;
|
|
@@ -51,6 +54,7 @@ export declare class Bridge {
|
|
|
51
54
|
private stopped;
|
|
52
55
|
constructor(o: {
|
|
53
56
|
path: string;
|
|
57
|
+
delegations?: Delegations;
|
|
54
58
|
host?: ToolHost;
|
|
55
59
|
tools: ReadonlySet<string>;
|
|
56
60
|
permitted: (tool: string) => boolean;
|
|
@@ -64,6 +68,7 @@ export declare class Bridge {
|
|
|
64
68
|
});
|
|
65
69
|
start(): Promise<void>;
|
|
66
70
|
stop(): void;
|
|
71
|
+
revoke(key: string, runId: string): void;
|
|
67
72
|
isRegisteredRun(key: string, runId?: string): boolean;
|
|
68
73
|
/** Register one run; release revokes only this exact registration, even when sessions overlap. */
|
|
69
74
|
register(run: RunRef, tools?: readonly string[], runId?: string): () => void;
|
package/dist/bridge.js
CHANGED
|
@@ -74,6 +74,8 @@ export class Bridge {
|
|
|
74
74
|
server;
|
|
75
75
|
closing = Promise.resolve();
|
|
76
76
|
sockets = new Set();
|
|
77
|
+
owners = new Map();
|
|
78
|
+
delegations;
|
|
77
79
|
runs = new Map();
|
|
78
80
|
// Every live registration per session key, each with its run's subset of the app's tools (RunSpec.tools; none: all
|
|
79
81
|
// of them). Runs sharing a key share the narrowest: a tool must be in every live subset.
|
|
@@ -85,6 +87,7 @@ export class Bridge {
|
|
|
85
87
|
stopped = false;
|
|
86
88
|
constructor(o) {
|
|
87
89
|
this.path = o.path;
|
|
90
|
+
this.delegations = o.delegations;
|
|
88
91
|
this.host = o.host;
|
|
89
92
|
this.tools = o.tools;
|
|
90
93
|
this.permitted = o.permitted;
|
|
@@ -107,7 +110,7 @@ export class Bridge {
|
|
|
107
110
|
rmSync(this.path, { force: true });
|
|
108
111
|
const server = createServer(socket => {
|
|
109
112
|
this.sockets.add(socket);
|
|
110
|
-
socket.once('close', () => this.sockets.delete(socket));
|
|
113
|
+
socket.once('close', () => { this.sockets.delete(socket); this.owners.delete(socket); });
|
|
111
114
|
this.serve(socket);
|
|
112
115
|
});
|
|
113
116
|
this.server = server;
|
|
@@ -142,6 +145,27 @@ export class Bridge {
|
|
|
142
145
|
this.closing = new Promise(resolve => server.close(() => resolve()))
|
|
143
146
|
.then(() => rm(this.path, { force: true }).catch(() => { }));
|
|
144
147
|
}
|
|
148
|
+
revoke(key, runId) {
|
|
149
|
+
for (const entry of this.live.get(key) ?? [])
|
|
150
|
+
if (entry.runId === runId)
|
|
151
|
+
entry.runId = undefined;
|
|
152
|
+
for (const [id, p] of this.parked)
|
|
153
|
+
if (p.run.sessionKey === key && p.run.runId === runId) {
|
|
154
|
+
clearTimeout(p.timer);
|
|
155
|
+
this.parked.delete(id);
|
|
156
|
+
this.onAskGone(id);
|
|
157
|
+
p.socket.destroy();
|
|
158
|
+
}
|
|
159
|
+
for (const [socket, owner] of this.owners)
|
|
160
|
+
if (owner.key === key && owner.runId === runId)
|
|
161
|
+
socket.destroy();
|
|
162
|
+
for (const [id, p] of this.permits)
|
|
163
|
+
if (p.key === key && p.runId === runId)
|
|
164
|
+
this.permits.delete(id);
|
|
165
|
+
for (let i = this.tickets.length - 1; i >= 0; i--)
|
|
166
|
+
if (this.tickets[i].key === key && this.tickets[i].runId === runId)
|
|
167
|
+
this.tickets.splice(i, 1);
|
|
168
|
+
}
|
|
145
169
|
isRegisteredRun(key, runId) {
|
|
146
170
|
return !this.stopped && typeof runId === 'string' && !!runId
|
|
147
171
|
&& (this.live.get(key) ?? []).some(entry => entry.runId === runId);
|
|
@@ -206,17 +230,17 @@ export class Bridge {
|
|
|
206
230
|
return;
|
|
207
231
|
}
|
|
208
232
|
if (this.permitted(tool)) {
|
|
209
|
-
this.reply(socket, { allow: true, permit: this.mint(key, tool, input) });
|
|
233
|
+
this.reply(socket, { allow: true, permit: this.mint(key, tool, input, this.owners.get(socket)?.runId) });
|
|
210
234
|
return;
|
|
211
235
|
}
|
|
212
|
-
this.tickets.push({ key, tool, input: JSON.stringify(input) });
|
|
236
|
+
this.tickets.push({ key, tool, input: JSON.stringify(input), runId: this.owners.get(socket)?.runId });
|
|
213
237
|
while (this.tickets.length > 1024)
|
|
214
238
|
this.tickets.shift();
|
|
215
239
|
this.reply(socket, { allow: true });
|
|
216
240
|
}
|
|
217
|
-
mint(key, tool, input) {
|
|
241
|
+
mint(key, tool, input, runId) {
|
|
218
242
|
const permit = randomBytes(16).toString('hex');
|
|
219
|
-
this.permits.set(permit, { permit, key, tool, input: JSON.stringify(input) });
|
|
243
|
+
this.permits.set(permit, { permit, key, tool, input: JSON.stringify(input), runId });
|
|
220
244
|
// Single-use entries are consumed on the next call; evict the oldest if the plugin never calls back.
|
|
221
245
|
while (this.permits.size > 1024)
|
|
222
246
|
this.permits.delete(this.permits.keys().next().value);
|
|
@@ -287,22 +311,51 @@ export class Bridge {
|
|
|
287
311
|
catch { /* the app's handler, not the engine's */ }
|
|
288
312
|
return;
|
|
289
313
|
}
|
|
314
|
+
if (message.kind.startsWith('delegation-') && this.delegations) {
|
|
315
|
+
try {
|
|
316
|
+
if (message.kind === 'delegation-prepare') {
|
|
317
|
+
if (typeof message.key !== 'string' || typeof message.runId !== 'string'
|
|
318
|
+
|| (!this.isRegisteredRun(message.key, message.runId) && !this.delegations.has(message.key, message.runId)))
|
|
319
|
+
return this.deny(socket, 'unknown delegation parent');
|
|
320
|
+
this.reply(socket, { tracked: this.delegations.prepare(message) });
|
|
321
|
+
}
|
|
322
|
+
else if (message.kind === 'delegation-receipt') {
|
|
323
|
+
this.delegations.receipt(message);
|
|
324
|
+
this.reply(socket, { ok: true });
|
|
325
|
+
}
|
|
326
|
+
else if (message.kind === 'delegation-event' && isRecord(message.event)) {
|
|
327
|
+
this.delegations.event(message.event);
|
|
328
|
+
this.reply(socket, { ok: true });
|
|
329
|
+
}
|
|
330
|
+
else
|
|
331
|
+
this.deny(socket, 'invalid delegation message');
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
this.deny(socket, error instanceof Error ? error.message : 'delegation unavailable');
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
290
338
|
if (message.kind === 'gate')
|
|
291
|
-
return void this.gate(socket, message);
|
|
339
|
+
return void this.gate(socket, message).catch(() => this.deny(socket, 'run unavailable'));
|
|
292
340
|
if (message.kind === 'call')
|
|
293
|
-
return void this.call(socket, message);
|
|
341
|
+
return void this.call(socket, message).catch(() => this.reply(socket, { ok: false, reason: 'run unavailable' }));
|
|
294
342
|
return this.deny(socket, 'not a gate request');
|
|
295
343
|
}
|
|
296
|
-
gate(socket, message) {
|
|
344
|
+
async gate(socket, message) {
|
|
297
345
|
const key = message.key;
|
|
298
346
|
const tool = message.tool;
|
|
299
347
|
const input = message.input;
|
|
300
348
|
if (typeof key !== 'string' || typeof tool !== 'string' || !isRecord(input)) {
|
|
301
349
|
return this.deny(socket, 'not a gate request');
|
|
302
350
|
}
|
|
303
|
-
const
|
|
351
|
+
const runId = typeof message.runId === 'string' ? message.runId : undefined;
|
|
352
|
+
this.owners.set(socket, { key, runId });
|
|
353
|
+
const delegated = await this.delegations?.resolve(key, runId);
|
|
354
|
+
if (this.stopped || socket.destroyed)
|
|
355
|
+
return;
|
|
356
|
+
const run = delegated?.ref ?? (!runId || this.isRegisteredRun(key, runId) ? this.runs.get(key) : undefined);
|
|
304
357
|
if (run)
|
|
305
|
-
return void this.gateRun(socket, run, tool, input);
|
|
358
|
+
return void this.gateRun(socket, run, tool, input, delegated?.tools);
|
|
306
359
|
const armed = this.armed;
|
|
307
360
|
if (armed && Date.now() < armed.until && key.startsWith(armed.keyPrefix) && tool === armed.tool &&
|
|
308
361
|
(!armed.input || armed.input(input))) {
|
|
@@ -314,11 +367,11 @@ export class Bridge {
|
|
|
314
367
|
}
|
|
315
368
|
return this.deny(socket, 'unknown run');
|
|
316
369
|
}
|
|
317
|
-
async gateRun(socket, run, tool, input) {
|
|
370
|
+
async gateRun(socket, run, tool, input, subset) {
|
|
318
371
|
if (!this.host)
|
|
319
372
|
return this.deny(socket, "can't check this action right now");
|
|
320
373
|
// An app tool outside the run's own subset never reaches the host; builtins stay with the gate.
|
|
321
|
-
if (this.tools.has(tool) && (this.live.get(run.sessionKey) ?? []).some((e) => e.tools && !e.tools.has(tool)))
|
|
374
|
+
if (this.tools.has(tool) && ((subset && !subset.includes(tool)) || (this.live.get(run.sessionKey) ?? []).some((e) => e.tools && !e.tools.has(tool))))
|
|
322
375
|
return this.deny(socket, 'this tool is not available in this run');
|
|
323
376
|
let result;
|
|
324
377
|
try {
|
|
@@ -327,7 +380,7 @@ export class Bridge {
|
|
|
327
380
|
catch {
|
|
328
381
|
return this.deny(socket, "can't check this action right now");
|
|
329
382
|
}
|
|
330
|
-
if (this.stopped)
|
|
383
|
+
if (this.stopped || socket.destroyed)
|
|
331
384
|
return;
|
|
332
385
|
if ('ask' in result) {
|
|
333
386
|
const now = Date.now();
|
|
@@ -383,11 +436,13 @@ export class Bridge {
|
|
|
383
436
|
// Only the app's own tools run through host.call; a builtin never calls back.
|
|
384
437
|
if (!this.tools.has(tool))
|
|
385
438
|
return this.reply(socket, { ok: false, reason: 'this call was not allowed' });
|
|
439
|
+
const runId = typeof message.runId === 'string' ? message.runId : undefined;
|
|
440
|
+
this.owners.set(socket, { key, runId });
|
|
386
441
|
const wanted = JSON.stringify(input);
|
|
387
442
|
const permit = message.permit;
|
|
388
443
|
if (typeof permit === 'string') {
|
|
389
444
|
const p = this.permits.get(permit);
|
|
390
|
-
if (!p || p.key !== key || p.tool !== tool || p.input !== wanted) {
|
|
445
|
+
if (!p || p.key !== key || p.runId !== runId || p.tool !== tool || p.input !== wanted) {
|
|
391
446
|
return this.reply(socket, { ok: false, reason: 'this call was not allowed' });
|
|
392
447
|
}
|
|
393
448
|
this.permits.delete(permit);
|
|
@@ -397,14 +452,17 @@ export class Bridge {
|
|
|
397
452
|
// key + tool + input, so no run registration is needed (N10: allowOnce admits unregistered keys).
|
|
398
453
|
if (this.permitted(tool))
|
|
399
454
|
return this.reply(socket, { ok: false, reason: 'this call was not allowed' });
|
|
400
|
-
const ticket = this.tickets.findIndex((t) => t.key === key && t.tool === tool && t.input === wanted);
|
|
455
|
+
const ticket = this.tickets.findIndex((t) => t.key === key && t.runId === runId && t.tool === tool && t.input === wanted);
|
|
401
456
|
if (ticket < 0)
|
|
402
457
|
return this.reply(socket, { ok: false, reason: 'this call was not allowed' });
|
|
403
458
|
this.tickets.splice(ticket, 1);
|
|
404
459
|
}
|
|
405
460
|
if (!this.host)
|
|
406
461
|
return this.reply(socket, { ok: false, reason: "can't run this action right now" });
|
|
407
|
-
const
|
|
462
|
+
const delegated = await this.delegations?.resolve(key, runId);
|
|
463
|
+
if (this.stopped || socket.destroyed)
|
|
464
|
+
return;
|
|
465
|
+
const run = delegated?.ref ?? this.runs.get(key) ?? { sessionKey: key, member: this.memberOf(key) ?? '' };
|
|
408
466
|
// The plugin destroys its socket when the run aborts (run abort, hook timeout); without this the host's
|
|
409
467
|
// sandboxed command keeps running after the tool is gone.
|
|
410
468
|
const controller = new AbortController();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { DelegatedEvent, DelegatedRun, GatewayTransport, RunRef } from './types.ts';
|
|
2
|
+
type Grant = {
|
|
3
|
+
ref: DelegatedRun;
|
|
4
|
+
tools: string[];
|
|
5
|
+
delegates: Record<string, string[]>;
|
|
6
|
+
sourceKey: string;
|
|
7
|
+
};
|
|
8
|
+
export declare class Delegations {
|
|
9
|
+
private loaded;
|
|
10
|
+
private roots;
|
|
11
|
+
private children;
|
|
12
|
+
private pending;
|
|
13
|
+
private changed;
|
|
14
|
+
private early;
|
|
15
|
+
constructor(privateOptions: {
|
|
16
|
+
file: string;
|
|
17
|
+
tools: ReadonlySet<string>;
|
|
18
|
+
memberOf(agent: string): string | undefined;
|
|
19
|
+
request: GatewayTransport['request'];
|
|
20
|
+
emit(e: DelegatedEvent): void;
|
|
21
|
+
revoke(key: string, runId: string): void;
|
|
22
|
+
});
|
|
23
|
+
private readonly o;
|
|
24
|
+
private load;
|
|
25
|
+
private save;
|
|
26
|
+
validate(delegates: Record<string, string[]>): void;
|
|
27
|
+
register(ref: RunRef, runId: string, tools: readonly string[] | undefined, delegates: Record<string, string[]>, sourceKey?: string): () => void;
|
|
28
|
+
list(): DelegatedRun[];
|
|
29
|
+
has(key: string, runId: string): boolean;
|
|
30
|
+
prepare(m: Record<string, any>): boolean;
|
|
31
|
+
receipt(m: Record<string, any>): void;
|
|
32
|
+
resolve(key: string, runId?: string): Promise<Grant | undefined>;
|
|
33
|
+
event(event: Record<string, any>): void;
|
|
34
|
+
private end;
|
|
35
|
+
reconcile(): Promise<void>;
|
|
36
|
+
abort(key: string): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Native receipts, never session-prefix authority. Persist identity/tool policy only, not prompts or credentials.
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { atomic } from './engine-patches.js';
|
|
4
|
+
const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
5
|
+
const agentOf = (key) => /^agent:([^:]+):.+/.exec(key)?.[1];
|
|
6
|
+
const actionKey = (m) => JSON.stringify([m.key, m.runId, m.callId]);
|
|
7
|
+
export class Delegations {
|
|
8
|
+
loaded = false;
|
|
9
|
+
roots = new Map();
|
|
10
|
+
children = new Map();
|
|
11
|
+
pending = new Map();
|
|
12
|
+
changed = new Set();
|
|
13
|
+
early = new Map();
|
|
14
|
+
constructor(privateOptions) { this.o = privateOptions; }
|
|
15
|
+
o;
|
|
16
|
+
load() {
|
|
17
|
+
if (this.loaded)
|
|
18
|
+
return;
|
|
19
|
+
if (existsSync(this.o.file)) {
|
|
20
|
+
const saved = JSON.parse(readFileSync(this.o.file, 'utf8'));
|
|
21
|
+
if (saved.version !== 1 || !Array.isArray(saved.children) || !Array.isArray(saved.pending))
|
|
22
|
+
throw new Error('invalid delegated run journal');
|
|
23
|
+
this.children = new Map(saved.children);
|
|
24
|
+
this.pending = new Map(saved.pending);
|
|
25
|
+
}
|
|
26
|
+
this.loaded = true;
|
|
27
|
+
}
|
|
28
|
+
save() {
|
|
29
|
+
for (const [key, p] of this.pending)
|
|
30
|
+
if (p.expires < Date.now())
|
|
31
|
+
this.pending.delete(key);
|
|
32
|
+
const bytes = JSON.stringify({ version: 1, children: [...this.children], pending: [...this.pending] });
|
|
33
|
+
atomic(this.o.file, bytes);
|
|
34
|
+
for (const notify of this.changed)
|
|
35
|
+
notify();
|
|
36
|
+
}
|
|
37
|
+
validate(delegates) {
|
|
38
|
+
if (!object(delegates) || Object.keys(delegates).length > 64)
|
|
39
|
+
throw new Error('invalid delegates');
|
|
40
|
+
for (const [agent, tools] of Object.entries(delegates)) {
|
|
41
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(agent) || !this.o.memberOf(agent) || !Array.isArray(tools)
|
|
42
|
+
|| tools.some(t => !this.o.tools.has(t)))
|
|
43
|
+
throw new Error(`invalid delegate: ${agent}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
register(ref, runId, tools, delegates, sourceKey = ref.sessionKey) {
|
|
47
|
+
this.load();
|
|
48
|
+
this.validate(delegates);
|
|
49
|
+
const root = { ref: { ...ref, runId, root: { sessionKey: ref.sessionKey, runId } },
|
|
50
|
+
tools: [...tools ?? this.o.tools], delegates: structuredClone(delegates), sourceKey };
|
|
51
|
+
this.roots.set(runId, root);
|
|
52
|
+
return () => { if (this.roots.get(runId) === root)
|
|
53
|
+
this.roots.delete(runId); };
|
|
54
|
+
}
|
|
55
|
+
list() { this.load(); return [...this.children.values()].map(g => structuredClone(g.ref)); }
|
|
56
|
+
has(key, runId) {
|
|
57
|
+
this.load();
|
|
58
|
+
return (this.roots.get(runId) ?? this.children.get(runId))?.ref.sessionKey === key;
|
|
59
|
+
}
|
|
60
|
+
prepare(m) {
|
|
61
|
+
this.load();
|
|
62
|
+
const parent = this.roots.get(m.runId) ?? this.children.get(m.runId);
|
|
63
|
+
if (!parent || parent.ref.sessionKey !== m.key)
|
|
64
|
+
return false;
|
|
65
|
+
if (typeof m.callId !== 'string' || !m.callId || !object(m.input))
|
|
66
|
+
throw new Error('delegation identity unavailable');
|
|
67
|
+
const send = m.tool === 'sessions_send';
|
|
68
|
+
if (!send && m.tool !== 'sessions_spawn')
|
|
69
|
+
throw new Error('unsupported delegation tool');
|
|
70
|
+
const agent = send ? typeof m.input.sessionKey === 'string' && agentOf(m.input.sessionKey) : m.input.agentId;
|
|
71
|
+
if (typeof agent !== 'string' || !Object.hasOwn(parent.delegates, agent)
|
|
72
|
+
|| (send && (m.input.timeoutSeconds !== 0 || m.input.label !== undefined))
|
|
73
|
+
|| (!send && m.input.runtime !== undefined && m.input.runtime !== 'subagent'))
|
|
74
|
+
throw new Error('delegation target or mode refused');
|
|
75
|
+
for (const [id, p] of this.pending)
|
|
76
|
+
if (p.expires < Date.now())
|
|
77
|
+
this.pending.delete(id);
|
|
78
|
+
if (this.pending.size + this.children.size >= 1024)
|
|
79
|
+
throw new Error('delegated run journal is full');
|
|
80
|
+
this.pending.set(actionKey(m), { parent: structuredClone(parent), agent,
|
|
81
|
+
...(send ? { key: m.input.sessionKey } : {}), expires: Date.now() + 60_000 });
|
|
82
|
+
this.save();
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
receipt(m) {
|
|
86
|
+
this.load();
|
|
87
|
+
const id = actionKey(m), p = this.pending.get(id);
|
|
88
|
+
if (!p)
|
|
89
|
+
return;
|
|
90
|
+
this.pending.delete(id);
|
|
91
|
+
const d = m.result?.details;
|
|
92
|
+
const key = d?.childSessionKey ?? d?.sessionKey;
|
|
93
|
+
if (p.expires < Date.now() || !object(d) || !['accepted', 'ok', 'queued'].includes(d.status)
|
|
94
|
+
|| typeof d.runId !== 'string' || !d.runId || typeof key !== 'string' || agentOf(key) !== p.agent
|
|
95
|
+
|| (p.key !== undefined && p.key !== key)) {
|
|
96
|
+
this.save();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const member = this.o.memberOf(p.agent);
|
|
100
|
+
if (!member) {
|
|
101
|
+
this.save();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const ref = { sessionKey: key, member, runId: d.runId, agentId: p.agent,
|
|
105
|
+
parent: { sessionKey: p.parent.ref.sessionKey, runId: p.parent.ref.runId }, root: p.parent.ref.root };
|
|
106
|
+
const grant = { ref, delegates: p.parent.delegates, sourceKey: p.parent.sourceKey,
|
|
107
|
+
tools: p.parent.tools.filter(t => p.parent.delegates[p.agent].includes(t)) };
|
|
108
|
+
// A steered/already-admitted run keeps its original parent and cannot acquire a broader grant.
|
|
109
|
+
if (this.children.has(ref.runId)) {
|
|
110
|
+
this.save();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.children.set(ref.runId, grant);
|
|
114
|
+
this.save();
|
|
115
|
+
this.o.emit({ type: 'admitted', run: structuredClone(ref) });
|
|
116
|
+
for (const event of this.early.get(ref.runId) ?? [])
|
|
117
|
+
this.event(event);
|
|
118
|
+
this.early.delete(ref.runId);
|
|
119
|
+
}
|
|
120
|
+
async resolve(key, runId) {
|
|
121
|
+
this.load();
|
|
122
|
+
if (!runId)
|
|
123
|
+
return undefined;
|
|
124
|
+
const get = () => { const g = this.children.get(runId); return g?.ref.sessionKey === key ? g : undefined; };
|
|
125
|
+
if (!get() && [...this.pending.values()].some(p => p.expires > Date.now())) {
|
|
126
|
+
await new Promise(resolve => {
|
|
127
|
+
const done = () => { clearTimeout(timer); this.changed.delete(check); resolve(); };
|
|
128
|
+
const check = () => { if (get() || !this.pending.size)
|
|
129
|
+
done(); };
|
|
130
|
+
const timer = setTimeout(done, 5_000);
|
|
131
|
+
this.changed.add(check);
|
|
132
|
+
check();
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return get();
|
|
136
|
+
}
|
|
137
|
+
event(event) {
|
|
138
|
+
this.load();
|
|
139
|
+
const grant = this.children.get(event.runId);
|
|
140
|
+
if (!grant) {
|
|
141
|
+
if (this.pending.size && typeof event.runId === 'string' && this.early.size < 64) {
|
|
142
|
+
const events = this.early.get(event.runId) ?? [];
|
|
143
|
+
if (events.length < 32)
|
|
144
|
+
events.push(event);
|
|
145
|
+
this.early.set(event.runId, events);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (event.sessionKey !== grant.ref.sessionKey)
|
|
150
|
+
return;
|
|
151
|
+
this.o.emit({ type: 'event', run: structuredClone(grant.ref), event: { stream: event.stream, data: event.data } });
|
|
152
|
+
if (event.stream === 'lifecycle' && ['end', 'error'].includes(event.data?.phase))
|
|
153
|
+
this.end(grant);
|
|
154
|
+
}
|
|
155
|
+
end(grant) {
|
|
156
|
+
this.children.delete(grant.ref.runId);
|
|
157
|
+
this.save();
|
|
158
|
+
this.o.revoke(grant.ref.sessionKey, grant.ref.runId);
|
|
159
|
+
this.o.emit({ type: 'ended', run: structuredClone(grant.ref), reason: 'terminal' });
|
|
160
|
+
}
|
|
161
|
+
async reconcile() {
|
|
162
|
+
for (const ref of this.list()) {
|
|
163
|
+
const end = await this.o.request('agent.wait', { runId: ref.runId, timeoutMs: 1 });
|
|
164
|
+
const grant = this.children.get(ref.runId);
|
|
165
|
+
if (grant && ['ok', 'error'].includes(end.status ?? ''))
|
|
166
|
+
this.end(grant);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async abort(key) {
|
|
170
|
+
this.load();
|
|
171
|
+
const ids = new Set();
|
|
172
|
+
for (const g of [...this.roots.values(), ...this.children.values()])
|
|
173
|
+
if (g.ref.sessionKey === key || g.ref.parent?.sessionKey === key || g.ref.root.sessionKey === key || g.sourceKey === key)
|
|
174
|
+
ids.add(g.ref.runId);
|
|
175
|
+
for (;;) {
|
|
176
|
+
const size = ids.size;
|
|
177
|
+
for (const g of this.children.values())
|
|
178
|
+
if (ids.has(g.ref.parent.runId))
|
|
179
|
+
ids.add(g.ref.runId);
|
|
180
|
+
if (ids.size === size)
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
let removed = false;
|
|
184
|
+
for (const [id, p] of this.pending)
|
|
185
|
+
if (ids.has(p.parent.ref.runId) || p.parent.ref.sessionKey === key || p.parent.ref.root.sessionKey === key || p.parent.sourceKey === key) {
|
|
186
|
+
this.pending.delete(id);
|
|
187
|
+
removed = true;
|
|
188
|
+
}
|
|
189
|
+
if (!ids.size && !removed)
|
|
190
|
+
return;
|
|
191
|
+
const cancelled = [...this.children.values()].filter(g => ids.has(g.ref.runId));
|
|
192
|
+
const revoked = [...this.roots.values(), ...cancelled].filter(g => ids.has(g.ref.runId));
|
|
193
|
+
for (const id of ids)
|
|
194
|
+
this.roots.delete(id);
|
|
195
|
+
for (const g of cancelled)
|
|
196
|
+
this.children.delete(g.ref.runId);
|
|
197
|
+
this.save(); // revoke durably before any awaited transport call
|
|
198
|
+
for (const g of revoked)
|
|
199
|
+
this.o.revoke(g.ref.sessionKey, g.ref.runId);
|
|
200
|
+
for (const g of cancelled)
|
|
201
|
+
this.o.emit({ type: 'ended', run: structuredClone(g.ref), reason: 'cancelled' });
|
|
202
|
+
await Promise.all(cancelled.map(g => this.o.request('chat.abort', { sessionKey: g.ref.sessionKey, runId: g.ref.runId })));
|
|
203
|
+
}
|
|
204
|
+
}
|
package/dist/device.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import type { BrowserDevice, SignInRefusedWhy } from './browser.ts';
|
|
|
2
2
|
import type { DeviceLink } from '@byokit/pair';
|
|
3
3
|
import type { RouteView } from './routes.ts';
|
|
4
4
|
import { type AccountView } from './words.ts';
|
|
5
|
-
import type { Approval, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, KitState, RunEnd, OutputSchema, SchemaOutput, RunEvent, SignInView, AccountRef } from './types.ts';
|
|
5
|
+
import type { Approval, Decision, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, KitState, RunEnd, OutputSchema, SchemaOutput, RunEvent, RunFrame, SignInView, AccountRef } from './types.ts';
|
|
6
6
|
export type { GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult } from './types.ts';
|
|
7
|
-
export type { Approval, Decision, KitState, PlanWindow, Route, RunEnd, RunEvent, RunUsage, SignInView, OutputSchema, SchemaOutput } from './types.ts';
|
|
7
|
+
export type { Approval, Decision, KitState, PlanWindow, Route, RunEnd, RunEvent, RunFrame, RunUsage, SignInView, OutputSchema, SchemaOutput } from './types.ts';
|
|
8
8
|
/** One `oc.events` frame: a member's Gateway event, typed by name, or an approval add/resolve (7.2). */
|
|
9
9
|
export type OpenClawLinkEvent = {
|
|
10
10
|
[E in GatewayEventName]: {
|
|
@@ -30,11 +30,13 @@ export type SessionRow = {
|
|
|
30
30
|
/** The frame `oc.run`'s stream ends with: the run's `RunEnd`, beside its events. */
|
|
31
31
|
export type DeviceEndFrame<T = unknown> = {
|
|
32
32
|
type: 'end';
|
|
33
|
+
seq: number;
|
|
33
34
|
end: RunEnd<T>;
|
|
34
35
|
};
|
|
35
36
|
/** `oc.run`'s options beside the message: the member's own session, account, and the kit's run options. */
|
|
36
37
|
export type DeviceRunOptions<S extends OutputSchema | undefined = OutputSchema | undefined> = {
|
|
37
38
|
schema?: S;
|
|
39
|
+
idempotencyKey?: string;
|
|
38
40
|
sessionKey?: string;
|
|
39
41
|
account?: AccountRef | 'default' | 'auto';
|
|
40
42
|
needs?: string[];
|
|
@@ -96,6 +98,9 @@ export type OpenClawDevice = {
|
|
|
96
98
|
signOut(p: string): Promise<void>;
|
|
97
99
|
sessions(): Promise<SessionRow[]>;
|
|
98
100
|
run<const S extends OutputSchema | undefined = undefined>(message: string, o?: DeviceRunOptions<S>): AsyncIterable<DeviceRunEvent | DeviceEndFrame<SchemaOutput<S>>>;
|
|
101
|
+
attach(runId: string, o: {
|
|
102
|
+
fromSeq: number;
|
|
103
|
+
}): AsyncIterable<RunFrame>;
|
|
99
104
|
steer(k: string, t: string, o?: {
|
|
100
105
|
auth?: 'apiKey';
|
|
101
106
|
}): Promise<void>;
|
package/dist/device.js
CHANGED
|
@@ -361,8 +361,9 @@ export function openclawDevice(link) {
|
|
|
361
361
|
await link.request('oc.signout', { provider: p });
|
|
362
362
|
},
|
|
363
363
|
sessions: () => link.request('oc.sessions'),
|
|
364
|
+
attach: (runId, o) => liveStream(() => link.stream('oc.attach', { runId, fromSeq: o.fromSeq })),
|
|
364
365
|
run: (message, o) => {
|
|
365
|
-
const inner = liveStream(() => link.stream('oc.run', { message, ...(o?.sessionKey ? { sessionKey: checkKey(o.sessionKey) } : {}),
|
|
366
|
+
const inner = liveStream(() => link.stream('oc.run', { message, ...(o?.idempotencyKey !== undefined ? { idempotencyKey: o.idempotencyKey } : {}), ...(o?.sessionKey ? { sessionKey: checkKey(o.sessionKey) } : {}),
|
|
366
367
|
...(o?.account !== undefined ? { account: o.account } : {}),
|
|
367
368
|
...(o?.needs !== undefined ? { needs: o.needs } : {}),
|
|
368
369
|
...(o?.model !== undefined ? { model: o.model } : {}), ...(o?.system !== undefined ? { system: o.system } : {}),
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { ENGINE_VERSION, OPERATOR_SCOPES, PROTOCOL_VERSION } from './constants.ts';
|
|
2
|
-
export type { Account, AccountId, AccountPick, AccountRef, Approval, CallOptions, ChannelChat, ChannelMessage, Considered, Decision, Defaults, ModelInfo, MoveResult, PickWhy, Room, RoomSpan, RunSelection, SignInState, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, KitEventName, KitEventPayload, BrowserPing, LearningCapture, LearningMode, Member, PlanWindow, Route, RunEnd, RunEvent, RunRef, RunSpec, OutputSchema, SchemaOutput, RunUsage, ThinkingAsked, ThinkingReport, ThinkingUsed, SignInOptions, SignInView, TelegramChannel, ToolHost, ToolSpec, } from './types.ts';
|
|
2
|
+
export type { Account, AccountId, AccountPick, AccountRef, Approval, CallOptions, ChannelChat, ChannelMessage, Considered, Decision, Defaults, ModelInfo, MoveResult, PickWhy, Room, RoomSpan, RunSelection, SignInState, GatewayEventName, GatewayEventPayload, GatewayMethod, GatewayParams, GatewayResult, GatewayTransport, GateResult, Hello, KitState, KitEventName, KitEventPayload, BrowserPing, LearningCapture, LearningMode, Member, PlanWindow, Route, RunEnd, RunEvent, RunFrame, RunRef, DelegatedRun, DelegatedEvent, RunSpec, OutputSchema, SchemaOutput, RunUsage, ThinkingAsked, ThinkingReport, ThinkingUsed, SignInOptions, SignInView, TelegramChannel, ToolHost, ToolSpec, } from './types.ts';
|
|
3
3
|
export type { GatewayMethods } from './generated/methods.ts';
|
|
4
4
|
export type { JsonValue } from './output.ts';
|
|
5
5
|
export type { RouteFacts, RouteView } from './routes.ts';
|
package/dist/kit.d.ts
CHANGED
|
@@ -64,9 +64,12 @@ export declare class OpenClawKit {
|
|
|
64
64
|
private members?;
|
|
65
65
|
private readonly locks;
|
|
66
66
|
private readonly routeMap;
|
|
67
|
+
private readonly runReplay;
|
|
68
|
+
attach(member: Member, runId: string, fromSeq: number, signal: AbortSignal): AsyncIterable<import("./types.ts").RunFrame>;
|
|
67
69
|
private installs;
|
|
68
70
|
private accountsCtl?;
|
|
69
71
|
private readonly bridge;
|
|
72
|
+
private readonly delegations;
|
|
70
73
|
private readonly keys;
|
|
71
74
|
private readonly admission;
|
|
72
75
|
private readonly approvalsCtl;
|
|
@@ -207,6 +210,7 @@ export declare class OpenClawKit {
|
|
|
207
210
|
steer(sessionKey: string, text: string, o?: {
|
|
208
211
|
auth?: 'apiKey';
|
|
209
212
|
}): Promise<void>;
|
|
213
|
+
delegatedRuns(): import('./types.ts').DelegatedRun[];
|
|
210
214
|
abort(sessionKey: string, o?: {
|
|
211
215
|
auth?: 'apiKey';
|
|
212
216
|
}): Promise<void>;
|
package/dist/kit.js
CHANGED
|
@@ -9,6 +9,7 @@ import { PROTOCOL_VERSION, LIVE_CATALOGS } from './constants.js';
|
|
|
9
9
|
import { atomic } from './engine-patches.js';
|
|
10
10
|
import { Approvals } from './approvals.js';
|
|
11
11
|
import { Bridge } from './bridge.js';
|
|
12
|
+
import { Delegations } from './delegations.js';
|
|
12
13
|
import { Engine, canonicalStateDir, refusedOnly } from './engine.js';
|
|
13
14
|
import { AuthStoreSealSizeError } from './auth-store.js';
|
|
14
15
|
import { gatewayTransport } from './transport.js';
|
|
@@ -17,7 +18,7 @@ import { createAccounts, memberOfAccount } from './accounts.js';
|
|
|
17
18
|
import { createLocks } from './locks.js';
|
|
18
19
|
import { keyAgentId, KEY_PREFIX, MEMBER_ID } from './members.js';
|
|
19
20
|
import { confirmRetainedLogin as confirmLogin, migrateRetainedLogin as migrateLogin } from './migrate.js';
|
|
20
|
-
import { createRuns, signedInProviders } from './runs.js';
|
|
21
|
+
import { createRuns, RunReplay, signedInProviders } from './runs.js';
|
|
21
22
|
import { authStatus, createAuthStatus } from './auth-status.js';
|
|
22
23
|
import { outputSchema } from './output.js';
|
|
23
24
|
import { createKeys } from './keys.js';
|
|
@@ -441,11 +442,16 @@ export class OpenClawKit {
|
|
|
441
442
|
members;
|
|
442
443
|
locks = createLocks();
|
|
443
444
|
routeMap = new Map();
|
|
445
|
+
runReplay = new RunReplay();
|
|
446
|
+
attach(member, runId, fromSeq, signal) {
|
|
447
|
+
return this.runReplay.attach(member, runId, fromSeq, signal);
|
|
448
|
+
}
|
|
444
449
|
installs = Promise.resolve();
|
|
445
450
|
accountsCtl;
|
|
446
451
|
// One bridge and one approval surface per kit: listeners and run registrations survive reconnects (B3).
|
|
447
452
|
// Only the socket itself is per-connection. Approvals always reaches the live transport through this.request().
|
|
448
453
|
bridge;
|
|
454
|
+
delegations;
|
|
449
455
|
keys;
|
|
450
456
|
admission;
|
|
451
457
|
approvalsCtl;
|
|
@@ -716,7 +722,12 @@ export class OpenClawKit {
|
|
|
716
722
|
return o.host.call(run, tool, input, signal);
|
|
717
723
|
},
|
|
718
724
|
} : o.host;
|
|
719
|
-
this.
|
|
725
|
+
this.delegations = new Delegations({ file: join(this.engine.root, 'delegated-runs.json'), tools: new Set(this.toolNames()),
|
|
726
|
+
memberOf: agent => this.memberOf(agent), request: (method, params, options) => this.request()(method, params, options),
|
|
727
|
+
emit: payload => { for (const fn of this.listeners)
|
|
728
|
+
fn({ event: 'byokit.delegated', payload }); },
|
|
729
|
+
revoke: (key, runId) => this.bridge.revoke(key, runId) });
|
|
730
|
+
this.bridge = new Bridge({ path: this.engine.bridgeSock, delegations: this.delegations, host, tools: new Set([...this.toolNames(), ...(o.browser ? ['request_sign_in'] : [])]),
|
|
720
731
|
...(o.browser ? { browserCapabilities: () => [...this.browserCapabilities],
|
|
721
732
|
beforeAgentRun: async (key, runId) => {
|
|
722
733
|
// No action-id submission proof or redispatch claim. Open and unproven resumes are always refused.
|
|
@@ -825,6 +836,7 @@ export class OpenClawKit {
|
|
|
825
836
|
// The fail-closed tool bridge feeds the kit's single Approval surface (5.9, O5).
|
|
826
837
|
await Promise.race([this.bridge.start(), exit]);
|
|
827
838
|
this.off.push(transport.onEvent((e) => this.approvalsCtl.handleEvent(e)));
|
|
839
|
+
await this.delegations.reconcile();
|
|
828
840
|
this.failures = 0;
|
|
829
841
|
this.reachedReady = true;
|
|
830
842
|
this.setState({ phase: 'ready' });
|
|
@@ -940,6 +952,8 @@ export class OpenClawKit {
|
|
|
940
952
|
});
|
|
941
953
|
}
|
|
942
954
|
closed(why) {
|
|
955
|
+
if (why === 'engine exited')
|
|
956
|
+
this.runReplay.clear();
|
|
943
957
|
// A first start's loop owns an engine exit until it reaches ready; other closes are the transport client's to retry.
|
|
944
958
|
if (this.startRun && !this.reachedReady) {
|
|
945
959
|
if (why === 'engine exited')
|
|
@@ -968,6 +982,7 @@ export class OpenClawKit {
|
|
|
968
982
|
return delay(retryAt - Date.now(), undefined, { signal });
|
|
969
983
|
}
|
|
970
984
|
async stop() {
|
|
985
|
+
this.runReplay.clear();
|
|
971
986
|
this.stopping = true;
|
|
972
987
|
this.reachedReady = false;
|
|
973
988
|
this.startRun?.abort();
|
|
@@ -1303,10 +1318,11 @@ export class OpenClawKit {
|
|
|
1303
1318
|
confirmRetainedLogin(member, source) {
|
|
1304
1319
|
return confirmLogin({ ...this.signInCtx(), seal: this.o.authSeal, log: this.o.log }, member, source);
|
|
1305
1320
|
}
|
|
1306
|
-
// runs (5.8): the facade
|
|
1321
|
+
// runs (5.8): the facade retains the live journal; every dispatch delegates to the O8 module
|
|
1307
1322
|
// with this kit's live transport, member table and bridge (spec item O9).
|
|
1308
1323
|
runs() {
|
|
1309
1324
|
return createRuns({
|
|
1325
|
+
replay: this.runReplay,
|
|
1310
1326
|
request: (method, params, o) => this.request()(method, params, o),
|
|
1311
1327
|
authStatus: this.admission.read,
|
|
1312
1328
|
invalidateAuth: this.admission.invalidate,
|
|
@@ -1318,7 +1334,7 @@ export class OpenClawKit {
|
|
|
1318
1334
|
},
|
|
1319
1335
|
ensure: (member) => this.ensureMember(member),
|
|
1320
1336
|
bridge: { register: (run, tools, runId) => this.bridge.register(run, tools && this.o.browser ? [...tools, 'request_sign_in'] : tools, runId) },
|
|
1321
|
-
tools: new Set(this.toolNames()),
|
|
1337
|
+
tools: new Set(this.toolNames()), delegations: this.delegations,
|
|
1322
1338
|
...(this.accountsCtl ? { accounts: this.accountsCtl } : {}),
|
|
1323
1339
|
locks: this.locks,
|
|
1324
1340
|
routes: this.routeMap,
|
|
@@ -1329,6 +1345,8 @@ export class OpenClawKit {
|
|
|
1329
1345
|
return (this.o.tools ?? []).map((t) => t.name);
|
|
1330
1346
|
}
|
|
1331
1347
|
async run(spec, on) {
|
|
1348
|
+
if (spec.delegates && this.o.gateBuiltins === false)
|
|
1349
|
+
throw new Error('delegation requires builtin gating');
|
|
1332
1350
|
if (this.o.browser) {
|
|
1333
1351
|
if (!await this.checkBrowserTools())
|
|
1334
1352
|
return { ok: false, kind: 'other', message: 'browser tool policy refused' };
|
|
@@ -1372,8 +1390,10 @@ export class OpenClawKit {
|
|
|
1372
1390
|
async steer(sessionKey, text, o) {
|
|
1373
1391
|
return this.runs().steer(this.runKey(sessionKey, o), text);
|
|
1374
1392
|
}
|
|
1393
|
+
delegatedRuns() { return this.delegations.list(); }
|
|
1375
1394
|
async abort(sessionKey, o) {
|
|
1376
|
-
|
|
1395
|
+
const key = this.runKey(sessionKey, o);
|
|
1396
|
+
await Promise.all([this.delegations.abort(key), this.runs().abort(key)]);
|
|
1377
1397
|
}
|
|
1378
1398
|
// approvals (5.9)
|
|
1379
1399
|
approvals(member) {
|
package/dist/link.js
CHANGED
|
@@ -30,8 +30,9 @@ function memberSessions(result, owns) {
|
|
|
30
30
|
}
|
|
31
31
|
/** `oc.run`'s run options, type-checked; the kit's own checks (account, tool names) still apply. */
|
|
32
32
|
function runOptions(args) {
|
|
33
|
-
const { message, model, account, needs, auth, system, images, thinking, tools, schema } = args;
|
|
33
|
+
const { message, idempotencyKey, model, account, needs, auth, system, images, thinking, tools, schema } = args;
|
|
34
34
|
const ok = typeof message === 'string'
|
|
35
|
+
&& (idempotencyKey === undefined || typeof idempotencyKey === 'string' && idempotencyKey.length > 0)
|
|
35
36
|
&& (model === undefined || typeof model === 'string')
|
|
36
37
|
&& (account === undefined || typeof account === 'string')
|
|
37
38
|
&& (needs === undefined || (Array.isArray(needs) && needs.every((n) => typeof n === 'string')))
|
|
@@ -46,6 +47,7 @@ function runOptions(args) {
|
|
|
46
47
|
outputSchema(schema);
|
|
47
48
|
return {
|
|
48
49
|
message: message,
|
|
50
|
+
...(idempotencyKey === undefined ? {} : { idempotencyKey: idempotencyKey }),
|
|
49
51
|
...(schema === undefined ? {} : { schema: schema }),
|
|
50
52
|
...(model === undefined ? {} : { model: model }),
|
|
51
53
|
...(account === undefined ? {} : { account: account }),
|
|
@@ -561,6 +563,31 @@ export function openclawLink(kit, o) {
|
|
|
561
563
|
}
|
|
562
564
|
return;
|
|
563
565
|
}
|
|
566
|
+
if (req.op === 'oc.attach') {
|
|
567
|
+
if (grant.role === 'view')
|
|
568
|
+
throw refused();
|
|
569
|
+
const args = isRecord(req.args) ? req.args : {};
|
|
570
|
+
if (typeof args.runId !== 'string' || !args.runId.length || !Number.isSafeInteger(args.fromSeq) || args.fromSeq < 0)
|
|
571
|
+
throw refused();
|
|
572
|
+
const controller = new AbortController();
|
|
573
|
+
let frames;
|
|
574
|
+
try {
|
|
575
|
+
frames = kit.attach(member, args.runId, args.fromSeq, controller.signal);
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
throw refused();
|
|
579
|
+
}
|
|
580
|
+
s.onEnd = () => controller.abort();
|
|
581
|
+
try {
|
|
582
|
+
for await (const frame of frames)
|
|
583
|
+
await s.write(`${JSON.stringify(frame)}\n`);
|
|
584
|
+
s.end();
|
|
585
|
+
}
|
|
586
|
+
finally {
|
|
587
|
+
controller.abort();
|
|
588
|
+
}
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
564
591
|
if (req.op === 'oc.run') {
|
|
565
592
|
if (grant.role === 'view')
|
|
566
593
|
throw refused();
|
|
@@ -579,8 +606,9 @@ export function openclawLink(kit, o) {
|
|
|
579
606
|
tail = tail.then(() => s.write(`${JSON.stringify(frame)}\n`)).catch(() => { });
|
|
580
607
|
};
|
|
581
608
|
try {
|
|
582
|
-
|
|
583
|
-
|
|
609
|
+
let seq = 0;
|
|
610
|
+
const end = await kit.run({ member, sessionKey, ...options }, (e) => { seq = e.seq; send(e); });
|
|
611
|
+
send({ type: 'end', seq: seq + 1, end });
|
|
584
612
|
await tail;
|
|
585
613
|
s.end();
|
|
586
614
|
}
|
package/dist/runs.d.ts
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
import type { Bridge } from './bridge.ts';
|
|
2
|
+
import type { Delegations } from './delegations.ts';
|
|
2
3
|
import type { createAccounts } from './accounts.ts';
|
|
3
4
|
import type { createLocks } from './locks.ts';
|
|
4
5
|
import { authStatus } from './auth-status.ts';
|
|
5
6
|
import { outputSchema, type OutputSchema, type SchemaOutput } from './output.ts';
|
|
6
|
-
import type { GatewayTransport, Member, RunEnd, RunEvent, RunSpec } from './types.ts';
|
|
7
|
+
import type { GatewayTransport, Member, RunEnd, RunEvent, RunFrame, RunSpec } from './types.ts';
|
|
7
8
|
export declare const USABLE: Set<string>;
|
|
8
9
|
/**
|
|
9
10
|
* The providers a `models.authStatus` answer shows usable (the rule a picked account is checked by), lowercased and
|
|
10
11
|
* unique; undefined while the engine has no prepared status (unknown, not none).
|
|
11
12
|
*/
|
|
12
13
|
export declare function signedInProviders(status: unknown): string[] | undefined;
|
|
14
|
+
type Journal = {
|
|
15
|
+
member: Member;
|
|
16
|
+
frames: Exclude<RunFrame, {
|
|
17
|
+
type: 'truncated';
|
|
18
|
+
}>[];
|
|
19
|
+
done: boolean;
|
|
20
|
+
wake: Set<() => void>;
|
|
21
|
+
};
|
|
22
|
+
/** Live-kit only: bounded history, with readers pulling by cursor rather than growing per-reader queues. */
|
|
23
|
+
export declare class RunReplay {
|
|
24
|
+
private readonly runs;
|
|
25
|
+
begin(member: Member, id: string): Journal | undefined;
|
|
26
|
+
push(row: Journal | undefined, frame: Exclude<RunFrame, {
|
|
27
|
+
type: 'truncated';
|
|
28
|
+
}>): void;
|
|
29
|
+
clear(): void;
|
|
30
|
+
attach(member: Member, id: string, fromSeq: number, signal: AbortSignal): AsyncIterable<RunFrame>;
|
|
31
|
+
}
|
|
13
32
|
export declare function createRuns(ctx: {
|
|
33
|
+
replay?: RunReplay;
|
|
14
34
|
request: GatewayTransport['request'];
|
|
15
35
|
authStatus?: (agentId: string, refresh: boolean, native: boolean) => ReturnType<typeof authStatus>;
|
|
16
36
|
invalidateAuth?: () => void;
|
|
@@ -19,6 +39,7 @@ export declare function createRuns(ctx: {
|
|
|
19
39
|
agentId: string;
|
|
20
40
|
}>;
|
|
21
41
|
bridge: Pick<Bridge, 'register'>;
|
|
42
|
+
delegations?: Delegations;
|
|
22
43
|
tools: ReadonlySet<string>;
|
|
23
44
|
accounts?: Pick<ReturnType<typeof createAccounts>, 'bind' | 'keyRefusal' | 'pick' | 'rest'>;
|
|
24
45
|
locks?: ReturnType<typeof createLocks>;
|
|
@@ -30,3 +51,4 @@ export declare function createRuns(ctx: {
|
|
|
30
51
|
steer(k: string, t: string): Promise<void>;
|
|
31
52
|
abort(k: string): Promise<void>;
|
|
32
53
|
};
|
|
54
|
+
export {};
|
package/dist/runs.js
CHANGED
|
@@ -71,6 +71,74 @@ function account(model) {
|
|
|
71
71
|
throw new Error(`refused: "${model}" is not a provider/model reference`);
|
|
72
72
|
return { provider, model: model.slice(model.indexOf('/') + 1) }; // the engine lowercases ids
|
|
73
73
|
}
|
|
74
|
+
/** Live-kit only: bounded history, with readers pulling by cursor rather than growing per-reader queues. */
|
|
75
|
+
export class RunReplay {
|
|
76
|
+
runs = new Map();
|
|
77
|
+
begin(member, id) {
|
|
78
|
+
if (this.runs.has(id))
|
|
79
|
+
return undefined; // engine-cache retry must not replace the original stream
|
|
80
|
+
const row = { member, frames: [], done: false, wake: new Set() };
|
|
81
|
+
this.runs.set(id, row);
|
|
82
|
+
return row;
|
|
83
|
+
}
|
|
84
|
+
push(row, frame) {
|
|
85
|
+
if (!row)
|
|
86
|
+
return;
|
|
87
|
+
row.frames.push(frame);
|
|
88
|
+
if (row.frames.length > 32)
|
|
89
|
+
row.frames.shift();
|
|
90
|
+
row.done = frame.type === 'end';
|
|
91
|
+
for (const wake of row.wake)
|
|
92
|
+
wake();
|
|
93
|
+
if (row.done) {
|
|
94
|
+
const completed = [...this.runs].filter(([, r]) => r.done);
|
|
95
|
+
for (const [id] of completed.slice(0, -128))
|
|
96
|
+
this.runs.delete(id);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
clear() {
|
|
100
|
+
for (const row of this.runs.values())
|
|
101
|
+
for (const wake of row.wake)
|
|
102
|
+
wake();
|
|
103
|
+
this.runs.clear();
|
|
104
|
+
}
|
|
105
|
+
attach(member, id, fromSeq, signal) {
|
|
106
|
+
const row = this.runs.get(id);
|
|
107
|
+
if (row && row.member !== member)
|
|
108
|
+
throw new Error('foreign run');
|
|
109
|
+
const runs = this.runs;
|
|
110
|
+
return (async function* () {
|
|
111
|
+
let cursor = fromSeq;
|
|
112
|
+
while (!signal.aborted) {
|
|
113
|
+
if (!row || runs.get(id) !== row) {
|
|
114
|
+
yield { type: 'truncated', reason: 'not-found' };
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const firstSeq = row.frames[0]?.seq ?? 1;
|
|
118
|
+
const lastSeq = row.frames.at(-1)?.seq ?? 0;
|
|
119
|
+
if (cursor < firstSeq - 1 || cursor > lastSeq) {
|
|
120
|
+
yield { type: 'truncated', reason: 'truncated', firstSeq };
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const next = row.frames.find(f => f.seq > cursor);
|
|
124
|
+
if (next) {
|
|
125
|
+
cursor = next.seq;
|
|
126
|
+
yield next;
|
|
127
|
+
if (next.type === 'end')
|
|
128
|
+
return;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (row.done)
|
|
132
|
+
return;
|
|
133
|
+
await new Promise(resolve => {
|
|
134
|
+
const wake = () => { row.wake.delete(wake); signal.removeEventListener('abort', wake); resolve(); };
|
|
135
|
+
row.wake.add(wake);
|
|
136
|
+
signal.addEventListener('abort', wake, { once: true });
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
})();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
74
142
|
export function createRuns(ctx) {
|
|
75
143
|
// The plan window the engine last read for the run's provider: `models.authStatus` answers from its cache without
|
|
76
144
|
// blocking. Nothing reported, or no answer in time, is no window.
|
|
@@ -90,11 +158,16 @@ export function createRuns(ctx) {
|
|
|
90
158
|
ctx.invalidateAuth?.();
|
|
91
159
|
return { ok: false, ...classified, message };
|
|
92
160
|
};
|
|
93
|
-
const
|
|
161
|
+
const execute = async (spec, on, keyAgent, preparedOutput) => {
|
|
94
162
|
// Member boundary first: a member never speaks in another member's session, refused before any request. A key
|
|
95
163
|
// of one of the member's account agents is the member's too; binding below checks the index names it.
|
|
96
164
|
if (!spec.sessionKey.startsWith(`agent:${spec.member}:`) && !(ctx.accounts && !keyAgent && spec.sessionKey.startsWith(`agent:${spec.member}--`)))
|
|
97
165
|
throw new Error(`refused: "${spec.sessionKey}" is not a session of member "${spec.member}"`);
|
|
166
|
+
if (spec.delegates) {
|
|
167
|
+
if (spec.register === false || !ctx.delegations)
|
|
168
|
+
throw new Error('delegation requires a registered run');
|
|
169
|
+
ctx.delegations.validate(spec.delegates);
|
|
170
|
+
}
|
|
98
171
|
// Same validation as the pin's NonEmptyString; preserve the caller's bytes (including whitespace).
|
|
99
172
|
if (spec.idempotencyKey !== undefined && (typeof spec.idempotencyKey !== 'string' || !spec.idempotencyKey.length))
|
|
100
173
|
throw new Error('refused: idempotencyKey must be a non-empty string');
|
|
@@ -213,7 +286,8 @@ export function createRuns(ctx) {
|
|
|
213
286
|
};
|
|
214
287
|
const start = async (spec, on, t) => {
|
|
215
288
|
const { agentId, sessionKey, idempotencyKey, system, picked, output, chosen } = t;
|
|
216
|
-
|
|
289
|
+
let release;
|
|
290
|
+
let releaseDelegation;
|
|
217
291
|
let last;
|
|
218
292
|
let runId = ''; // gateway events for other runs carry a real runId and never match the empty one
|
|
219
293
|
let ended = false;
|
|
@@ -251,6 +325,8 @@ export function createRuns(ctx) {
|
|
|
251
325
|
}
|
|
252
326
|
});
|
|
253
327
|
try {
|
|
328
|
+
releaseDelegation = spec.delegates ? ctx.delegations.register({ sessionKey, member: spec.member }, idempotencyKey, spec.tools, spec.delegates, spec.sessionKey) : undefined;
|
|
329
|
+
release = spec.register !== false ? ctx.bridge.register({ sessionKey, member: spec.member }, spec.tools, idempotencyKey) : undefined;
|
|
254
330
|
if (picked && !chosen) {
|
|
255
331
|
// The picked account must be the one called: check it is signed in for this member before the run, since
|
|
256
332
|
// the engine would otherwise fail the run only after admitting it. An explicit provider/model is strict on
|
|
@@ -340,10 +416,28 @@ export function createRuns(ctx) {
|
|
|
340
416
|
ended = true;
|
|
341
417
|
unsubscribe();
|
|
342
418
|
release?.();
|
|
419
|
+
releaseDelegation?.();
|
|
343
420
|
}
|
|
344
421
|
};
|
|
422
|
+
const replay = ctx.replay ?? new RunReplay();
|
|
345
423
|
return {
|
|
346
|
-
run,
|
|
424
|
+
run: async (spec, on, keyAgent, preparedOutput) => {
|
|
425
|
+
if (spec.idempotencyKey !== undefined && (typeof spec.idempotencyKey !== 'string' || !spec.idempotencyKey.length))
|
|
426
|
+
throw new Error('refused: idempotencyKey must be a non-empty string');
|
|
427
|
+
const id = spec.idempotencyKey ?? randomUUID();
|
|
428
|
+
const row = replay.begin(spec.member, id);
|
|
429
|
+
let seq = 0;
|
|
430
|
+
const emit = (e) => { const event = { ...e, seq: ++seq }; replay.push(row, event); on?.(event); };
|
|
431
|
+
try {
|
|
432
|
+
const end = await execute({ ...spec, idempotencyKey: id }, emit, keyAgent, preparedOutput);
|
|
433
|
+
replay.push(row, { type: 'end', seq: ++seq, end });
|
|
434
|
+
return end;
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
replay.push(row, { type: 'end', seq: ++seq, end: failure(error instanceof Error ? error.message : String(error)) });
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
},
|
|
347
441
|
steer: (k, t) => ctx.request('sessions.steer', { sessionKey: ctx.routes.get(k)?.session ?? k, message: t }),
|
|
348
442
|
abort: async (k) => { await ctx.request('chat.abort', { sessionKey: ctx.routes.get(k)?.session ?? k }); },
|
|
349
443
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -20,7 +20,27 @@ export interface RunRef {
|
|
|
20
20
|
sessionKey: string;
|
|
21
21
|
member: Member;
|
|
22
22
|
meta?: unknown;
|
|
23
|
+
runId?: string;
|
|
24
|
+
agentId?: string;
|
|
25
|
+
parent?: {
|
|
26
|
+
sessionKey: string;
|
|
27
|
+
runId: string;
|
|
28
|
+
};
|
|
29
|
+
root?: {
|
|
30
|
+
sessionKey: string;
|
|
31
|
+
runId: string;
|
|
32
|
+
};
|
|
23
33
|
}
|
|
34
|
+
export type DelegatedRun = RunRef & Required<Pick<RunRef, 'runId' | 'agentId' | 'parent' | 'root'>>;
|
|
35
|
+
export type DelegatedEvent = {
|
|
36
|
+
type: 'admitted' | 'event' | 'ended';
|
|
37
|
+
run: DelegatedRun;
|
|
38
|
+
reason?: 'terminal' | 'cancelled';
|
|
39
|
+
event?: {
|
|
40
|
+
stream: string;
|
|
41
|
+
data?: Record<string, unknown>;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
24
44
|
export type GateResult = {
|
|
25
45
|
allow: true;
|
|
26
46
|
} | {
|
|
@@ -55,9 +75,13 @@ export interface RunSpec<S extends OutputSchema | undefined = OutputSchema | und
|
|
|
55
75
|
/** Explicit paid key choice. Uses a separate member-owned agent and separate session history. */
|
|
56
76
|
auth?: 'apiKey';
|
|
57
77
|
tools?: string[];
|
|
78
|
+
/** Exact target engine agent IDs and their app-tool subsets; does not share credentials. */
|
|
79
|
+
delegates?: Record<string, string[]>;
|
|
58
80
|
register?: boolean;
|
|
59
81
|
}
|
|
60
82
|
export type RunEvent = {
|
|
83
|
+
seq: number;
|
|
84
|
+
} & ({
|
|
61
85
|
type: 'started';
|
|
62
86
|
} | {
|
|
63
87
|
type: 'thinking';
|
|
@@ -81,6 +105,15 @@ export type RunEvent = {
|
|
|
81
105
|
input?: Record<string, unknown>;
|
|
82
106
|
output?: unknown;
|
|
83
107
|
error?: boolean;
|
|
108
|
+
});
|
|
109
|
+
export type RunFrame = RunEvent | {
|
|
110
|
+
type: 'end';
|
|
111
|
+
seq: number;
|
|
112
|
+
end: RunEnd;
|
|
113
|
+
} | {
|
|
114
|
+
type: 'truncated';
|
|
115
|
+
reason: 'truncated' | 'not-found';
|
|
116
|
+
firstSeq?: number;
|
|
84
117
|
};
|
|
85
118
|
/** Token usage of one run as the engine totals it (every model call of the run, compaction included). */
|
|
86
119
|
export type RunUsage = {
|
|
@@ -282,8 +315,8 @@ export type BrowserPing = {
|
|
|
282
315
|
member: Member;
|
|
283
316
|
kind: 'state' | 'signin';
|
|
284
317
|
};
|
|
285
|
-
export type KitEventName = GatewayEventName | 'byokit.browser';
|
|
286
|
-
export type KitEventPayload<E extends KitEventName> = E extends 'byokit.browser' ? BrowserPing : GatewayEventPayload<Extract<E, GatewayEventName>>;
|
|
318
|
+
export type KitEventName = GatewayEventName | 'byokit.browser' | 'byokit.delegated';
|
|
319
|
+
export type KitEventPayload<E extends KitEventName> = E extends 'byokit.delegated' ? DelegatedEvent : E extends 'byokit.browser' ? BrowserPing : GatewayEventPayload<Extract<E, GatewayEventName>>;
|
|
287
320
|
export type { GatewayMethod, GatewayParams, GatewayResult } from './generated/methods.ts';
|
|
288
321
|
export type { GatewayEventName, GatewayEventPayload } from './generated/events.ts';
|
|
289
322
|
export type AccountId = string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "The OpenClaw runtime kit: the pinned engine's full operator surface with plain-words helpers for members, sign-in, runs and approvals.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/plugin/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const Kind = Symbol.for('TypeBox.Kind');
|
|
|
15
15
|
const table = JSON.parse(readFileSync(new URL('./tools.json', import.meta.url), 'utf8'));
|
|
16
16
|
const RUN_PARAM = table.runParam;
|
|
17
17
|
const PERMIT_PARAM = table.permitParam;
|
|
18
|
+
const RUN_ID_PARAM = `${RUN_PARAM}_id`;
|
|
18
19
|
const TOOLS = new Map(table.tools.map((t) => [t.name, t]));
|
|
19
20
|
// Absent (a table from before the flag) reads as true: fail closed.
|
|
20
21
|
const GATE_BUILTINS = table.gateBuiltins !== false;
|
|
@@ -131,8 +132,8 @@ function bridgeRequest(message, { timeoutMs, signal } = {}) {
|
|
|
131
132
|
});
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
async function gate(key, tool, input, signal) {
|
|
135
|
-
const reply = await bridgeRequest({ kind: 'gate', key, tool, input }, { timeoutMs: 195_000, signal });
|
|
135
|
+
async function gate(key, tool, input, signal, runId) {
|
|
136
|
+
const reply = await bridgeRequest({ kind: 'gate', key, tool, input, runId }, { timeoutMs: 195_000, signal });
|
|
136
137
|
return reply && typeof reply === 'object' ? reply : { allow: false, reason: "can't check this action right now" };
|
|
137
138
|
}
|
|
138
139
|
|
|
@@ -151,6 +152,21 @@ export default {
|
|
|
151
152
|
},
|
|
152
153
|
register(api) {
|
|
153
154
|
registerCodex(api);
|
|
155
|
+
api.on('after_tool_call', async (event, ctx) => {
|
|
156
|
+
if (!['sessions_send', 'sessions_spawn'].includes(event.toolName)) return;
|
|
157
|
+
const d = event.result?.details;
|
|
158
|
+
await bridgeRequest({ kind: 'delegation-receipt', key: ctx?.sessionKey, runId: ctx?.runId ?? event.runId,
|
|
159
|
+
callId: ctx?.toolCallId ?? event.toolCallId,
|
|
160
|
+
result: { details: d && { status: d.status, runId: d.runId, sessionKey: d.sessionKey, childSessionKey: d.childSessionKey } } });
|
|
161
|
+
});
|
|
162
|
+
api.registerAgentEventSubscription?.({ id: 'byokit-delegated-events', handle(event) {
|
|
163
|
+
if (!['assistant', 'tool', 'thinking', 'lifecycle'].includes(event.stream)) return;
|
|
164
|
+
const data = Object.fromEntries(['text', 'name', 'phase', 'toolCallId', 'isError', 'progressTokens', 'aborted', 'error', 'provider', 'model']
|
|
165
|
+
.filter(key => ['string', 'number', 'boolean'].includes(typeof event.data?.[key]))
|
|
166
|
+
.map(key => [key, event.data[key]]));
|
|
167
|
+
void bridgeRequest({ kind: 'delegation-event', event: { runId: event.runId, sessionKey: event.sessionKey,
|
|
168
|
+
stream: event.stream, data } }).catch(() => {});
|
|
169
|
+
} });
|
|
154
170
|
if (api.registerGatewayMethod) { registerKeys(api); registerUsage(api); registerAccounts(api); registerCatalog(api); }
|
|
155
171
|
if (BROWSER) {
|
|
156
172
|
if (typeof api.registerAgentToolResultMiddleware !== 'function') throw new Error('browser result protection unavailable');
|
|
@@ -189,13 +205,15 @@ export default {
|
|
|
189
205
|
const input = { ...(params ?? {}) };
|
|
190
206
|
const run = input[RUN_PARAM];
|
|
191
207
|
const permit = input[PERMIT_PARAM];
|
|
208
|
+
const runId = input[RUN_ID_PARAM];
|
|
192
209
|
delete input[RUN_PARAM];
|
|
193
210
|
delete input[PERMIT_PARAM];
|
|
211
|
+
delete input[RUN_ID_PARAM];
|
|
194
212
|
// The run key is always required; the permit only rides along when the gate minted one (N1).
|
|
195
213
|
if (typeof run !== 'string') {
|
|
196
214
|
throw new Error('this tool needs approval before it runs');
|
|
197
215
|
}
|
|
198
|
-
const call = { kind: 'call', key: run, tool: spec.name, input };
|
|
216
|
+
const call = { kind: 'call', key: run, runId, tool: spec.name, input };
|
|
199
217
|
if (typeof permit === 'string') call.permit = permit;
|
|
200
218
|
let reply;
|
|
201
219
|
try {
|
|
@@ -247,7 +265,7 @@ export default {
|
|
|
247
265
|
}
|
|
248
266
|
let decision;
|
|
249
267
|
try {
|
|
250
|
-
decision = await gate(key, event.toolName, params, ctx?.abortSignal);
|
|
268
|
+
decision = await gate(key, event.toolName, params, ctx?.abortSignal, ctx?.runId ?? event.runId);
|
|
251
269
|
} catch (error) {
|
|
252
270
|
return { block: true, blockReason: error instanceof Error ? error.message : "can't check this action right now" };
|
|
253
271
|
}
|
|
@@ -257,6 +275,13 @@ export default {
|
|
|
257
275
|
blockReason: typeof decision.reason === 'string' ? decision.reason : "can't check this action right now",
|
|
258
276
|
};
|
|
259
277
|
}
|
|
278
|
+
if (builtin && ['sessions_send', 'sessions_spawn'].includes(event.toolName)) {
|
|
279
|
+
try {
|
|
280
|
+
const prepared = await bridgeRequest({ kind: 'delegation-prepare', key, runId: ctx?.runId ?? event.runId,
|
|
281
|
+
callId: ctx?.toolCallId ?? event.toolCallId, tool: event.toolName, input: params });
|
|
282
|
+
if (prepared.allow === false) return { block: true, blockReason: prepared.reason };
|
|
283
|
+
} catch { return { block: true, blockReason: 'delegation admission unavailable' }; }
|
|
284
|
+
}
|
|
260
285
|
if (builtin) return BROWSER && event.toolName === 'browser' ? { params } : undefined;
|
|
261
286
|
// The bridge mints a permit for permitted tools and admits the rest ticket-side; either way the run key
|
|
262
287
|
// rides along and execute sends back only what the gate gave it (N1).
|
|
@@ -264,6 +289,7 @@ export default {
|
|
|
264
289
|
params: {
|
|
265
290
|
...params,
|
|
266
291
|
[RUN_PARAM]: key,
|
|
292
|
+
...(typeof (ctx?.runId ?? event.runId) === 'string' ? { [RUN_ID_PARAM]: ctx?.runId ?? event.runId } : {}),
|
|
267
293
|
...(typeof decision.permit === 'string' ? { [PERMIT_PARAM]: decision.permit } : {}),
|
|
268
294
|
},
|
|
269
295
|
};
|