@byokit/openclaw 0.5.0 → 0.6.1
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 +39 -0
- package/README.md +174 -2
- package/dist/auth-store.js +1 -1
- package/dist/bridge.d.ts +10 -2
- package/dist/bridge.js +61 -14
- package/dist/browser/broker.d.ts +63 -0
- package/dist/browser/broker.js +990 -0
- package/dist/browser/host.d.ts +98 -0
- package/dist/browser/host.js +945 -0
- package/dist/browser/resume.d.ts +10 -0
- package/dist/browser/resume.js +23 -0
- package/dist/browser/store.d.ts +25 -0
- package/dist/browser/store.js +151 -0
- package/dist/browser/verify.d.ts +17 -0
- package/dist/browser/verify.js +38 -0
- package/dist/browser.d.ts +212 -0
- package/dist/browser.js +4 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.js +130 -0
- package/dist/day-usage.d.ts +76 -0
- package/dist/day-usage.js +104 -0
- package/dist/device.d.ts +32 -2
- package/dist/device.js +224 -4
- package/dist/engine-patches.d.ts +36 -0
- package/dist/engine-patches.js +320 -0
- package/dist/engine-status.d.ts +13 -0
- package/dist/engine-status.js +31 -0
- package/dist/engine.d.ts +13 -1
- package/dist/engine.js +99 -52
- package/dist/index.d.ts +6 -1
- package/dist/index.js +2 -0
- package/dist/kit.d.ts +29 -3
- package/dist/kit.js +324 -13
- package/dist/link.d.ts +5 -2
- package/dist/link.js +333 -4
- package/dist/routes.d.ts +19 -8
- package/dist/routes.js +44 -11
- package/dist/routes.json +3775 -356
- package/dist/runs.js +8 -2
- package/dist/signin.d.ts +2 -1
- package/dist/signin.js +103 -7
- package/dist/testing/browser.d.ts +50 -0
- package/dist/testing/browser.js +166 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/model-stub.d.ts +1 -0
- package/dist/testing/model-stub.js +3 -1
- package/dist/types.d.ts +50 -3
- package/dist/usage-boots.d.ts +8 -0
- package/dist/usage-boots.js +26 -0
- package/dist/usage.d.ts +51 -0
- package/dist/usage.js +61 -0
- package/dist/words.json +42 -0
- package/engine/OPENCLAW-LICENSE +24 -0
- package/engine/patches.json +59 -0
- package/engine/patches.schema.json +41 -0
- package/package.json +12 -4
- package/plugin/index.js +78 -5
- package/plugin/usage.js +125 -0
package/dist/link.js
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
// Host-side link adapter (7.1): typed, member-checked ops over @byokit/link, sealed approval push
|
|
2
2
|
// via the relay (7.3). Node only.
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { sealBox } from '@byokit/seal';
|
|
4
5
|
import { readFileSync } from 'node:fs';
|
|
5
6
|
import { createServer } from 'node:http';
|
|
6
7
|
import { WebSocketServer } from 'ws';
|
|
7
8
|
import { PublicLinkError, } from '@byokit/link';
|
|
8
9
|
import { reach } from '@byokit/reach';
|
|
9
10
|
import { ENGINE_VERSION } from "./constants.js";
|
|
10
|
-
import { b64urlDecode, sealNotice } from "./notices.js";
|
|
11
|
+
import { b64urlDecode, b64urlEncode, sealNotice } from "./notices.js";
|
|
11
12
|
import { routeFor } from "./routes.js";
|
|
12
13
|
import { authStatus } from "./auth-status.js";
|
|
13
14
|
import { outputSchema } from "./output.js";
|
|
14
15
|
import { signedInProviders } from "./runs.js";
|
|
15
16
|
import { stateWords, words } from "./words.js";
|
|
16
|
-
const VIEW_OPS = new Set(['oc.state', 'oc.routes', 'oc.signin.view', 'oc.sessions', 'oc.approvals', 'oc.events'
|
|
17
|
+
const VIEW_OPS = new Set(['oc.state', 'oc.routes', 'oc.signin.view', 'oc.sessions', 'oc.approvals', 'oc.events',
|
|
18
|
+
'oc.browser.state', 'oc.browser.signins', 'oc.browser.thumb']);
|
|
17
19
|
// This kit's own version, read once from its package.json (beside src/ and dist/ alike).
|
|
18
20
|
const KIT_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
19
21
|
const THINKING = new Set(['off', 'low', 'medium', 'high']);
|
|
20
22
|
const refused = () => new PublicLinkError(words('link.notAllowed'));
|
|
23
|
+
const BROWSER_REFUSALS = new Set(['stale', 'not-found', 'held-by-other', 'lease-expired', 'not-control',
|
|
24
|
+
'not-lease-holder', 'confirm-site', 'already-open', 'insecure-remote', 'unsupported']);
|
|
21
25
|
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
26
|
+
const browserTag = (event) => event === 'byokit.browser';
|
|
27
|
+
const browserEvent = (event, payload) => event === 'byokit.browser' && isRecord(payload) && typeof payload.member === 'string'
|
|
28
|
+
&& (payload.kind === 'state' || payload.kind === 'signin');
|
|
22
29
|
const memberKey = (member, sessionKey) => sessionKey.startsWith(`agent:${member}:`);
|
|
23
30
|
function memberSessions(result, member) {
|
|
24
31
|
const rows = Array.isArray(result) ? result : isRecord(result) && Array.isArray(result.sessions) ? result.sessions : [];
|
|
@@ -50,22 +57,140 @@ function runOptions(args) {
|
|
|
50
57
|
...(tools === undefined ? {} : { tools: [...tools] }),
|
|
51
58
|
};
|
|
52
59
|
}
|
|
60
|
+
function browserInput(value) {
|
|
61
|
+
if (!isRecord(value))
|
|
62
|
+
return false;
|
|
63
|
+
const finite = (key) => typeof value[key] === 'number' && Number.isFinite(value[key]);
|
|
64
|
+
if (value.kind === 'text')
|
|
65
|
+
return typeof value.text === 'string' && value.text.length <= 16384;
|
|
66
|
+
if (value.kind === 'nav')
|
|
67
|
+
return ['back', 'forward', 'reload'].includes(value.action);
|
|
68
|
+
if (value.kind === 'key')
|
|
69
|
+
return ['down', 'up'].includes(value.type)
|
|
70
|
+
&& typeof value.key === 'string' && value.key.length <= 128
|
|
71
|
+
&& (value.code === undefined || (typeof value.code === 'string' && value.code.length <= 128))
|
|
72
|
+
&& (value.modifiers === undefined || (Number.isInteger(value.modifiers) && value.modifiers >= 0 && value.modifiers <= 15));
|
|
73
|
+
return value.kind === 'pointer' && ['move', 'down', 'up', 'wheel'].includes(value.type)
|
|
74
|
+
&& finite('x') && finite('y') && (value.button === undefined || ['left', 'right', 'middle'].includes(value.button))
|
|
75
|
+
&& (value.dx === undefined || finite('dx')) && (value.dy === undefined || finite('dy'));
|
|
76
|
+
}
|
|
53
77
|
export function openclawLink(kit, o) {
|
|
54
78
|
const passThrough = o.passThrough ?? (() => false);
|
|
79
|
+
// The host lifecycle extension is supplied by the browser host. Without it control fails closed.
|
|
80
|
+
const browser = () => {
|
|
81
|
+
if (!kit.browser)
|
|
82
|
+
throw refused();
|
|
83
|
+
return kit.browser;
|
|
84
|
+
};
|
|
85
|
+
const leases = new Map();
|
|
86
|
+
const memberSource = (value, member) => {
|
|
87
|
+
if (!isRecord(value) || value.member !== member)
|
|
88
|
+
throw refused();
|
|
89
|
+
if (value.kind === 'browser')
|
|
90
|
+
return { kind: 'browser', member };
|
|
91
|
+
if (value.kind === 'desktop' && ['host', 'node', 'environment'].includes(value.source))
|
|
92
|
+
return { kind: 'desktop', member, source: value.source };
|
|
93
|
+
throw refused();
|
|
94
|
+
};
|
|
95
|
+
const control = () => {
|
|
96
|
+
if (typeof kit.browser?.revokeGrant !== 'function')
|
|
97
|
+
throw refused();
|
|
98
|
+
};
|
|
99
|
+
const ownedLease = (value, member, grant) => {
|
|
100
|
+
control();
|
|
101
|
+
if (!isRecord(value) || typeof value.requestId !== 'string')
|
|
102
|
+
throw refused();
|
|
103
|
+
const held = leases.get(value.requestId);
|
|
104
|
+
if (!held || held.grant !== grant.id || held.lease.nonce !== value.nonce || held.lease.epoch !== value.epoch
|
|
105
|
+
|| held.lease.gen !== value.gen || !browser().signIns(member).some(r => r.id === value.requestId))
|
|
106
|
+
throw refused();
|
|
107
|
+
return held.lease;
|
|
108
|
+
};
|
|
55
109
|
// Grants that have called, so sealed pushes and push actions can check the member (7.3).
|
|
56
110
|
const seen = new Map();
|
|
111
|
+
const expiry = new Map();
|
|
57
112
|
// Devices that registered a box key, by grant id.
|
|
58
113
|
const boxes = new Map();
|
|
59
114
|
// One sign-in drive per member+provider, plus the latest view even after it ends.
|
|
60
115
|
const drives = new Map();
|
|
61
116
|
const latest = new Map();
|
|
62
117
|
const memberOf = (grant) => {
|
|
118
|
+
if (revoked.has(grant.id) || (grant.expires !== undefined && grant.expires <= Date.now()))
|
|
119
|
+
throw refused();
|
|
63
120
|
seen.set(grant.id, grant);
|
|
121
|
+
clearTimeout(expiry.get(grant.id));
|
|
122
|
+
if (grant.expires !== undefined) {
|
|
123
|
+
const timer = setTimeout(() => {
|
|
124
|
+
const current = seen.get(grant.id);
|
|
125
|
+
if (current?.expires !== undefined && current.expires > Date.now())
|
|
126
|
+
memberOf(current);
|
|
127
|
+
else
|
|
128
|
+
void revoke(grant.id).catch(() => { });
|
|
129
|
+
}, Math.min(grant.expires - Date.now(), 2 ** 31 - 1));
|
|
130
|
+
timer.unref();
|
|
131
|
+
expiry.set(grant.id, timer);
|
|
132
|
+
}
|
|
64
133
|
const member = o.memberOf(grant);
|
|
65
134
|
if (member === undefined)
|
|
66
135
|
throw refused();
|
|
67
136
|
return member;
|
|
68
137
|
};
|
|
138
|
+
const viewers = new Map();
|
|
139
|
+
const revoked = new Set();
|
|
140
|
+
const revocations = new Map();
|
|
141
|
+
const revoke = (grant) => {
|
|
142
|
+
const pending = revocations.get(grant);
|
|
143
|
+
if (pending)
|
|
144
|
+
return pending;
|
|
145
|
+
revoked.add(grant);
|
|
146
|
+
clearTimeout(expiry.get(grant));
|
|
147
|
+
expiry.delete(grant);
|
|
148
|
+
for (const [id, held] of leases)
|
|
149
|
+
if (held.grant === grant)
|
|
150
|
+
leases.delete(id);
|
|
151
|
+
for (const close of viewers.get(grant) ?? [])
|
|
152
|
+
close();
|
|
153
|
+
viewers.delete(grant);
|
|
154
|
+
boxes.delete(grant);
|
|
155
|
+
seen.delete(grant);
|
|
156
|
+
let cleanup;
|
|
157
|
+
try {
|
|
158
|
+
cleanup = kit.browser?.revokeGrant?.(grant) ?? Promise.resolve();
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
cleanup = Promise.reject(refused());
|
|
162
|
+
}
|
|
163
|
+
const result = cleanup.catch(() => { throw refused(); });
|
|
164
|
+
revocations.set(grant, result);
|
|
165
|
+
return result;
|
|
166
|
+
};
|
|
167
|
+
const pushSignIn = async (member) => {
|
|
168
|
+
if (!o.relay || !kit.browser)
|
|
169
|
+
return;
|
|
170
|
+
for (const request of kit.browser.signIns(member)) {
|
|
171
|
+
if (request.state !== 'waiting')
|
|
172
|
+
continue;
|
|
173
|
+
for (const [id, box] of boxes) {
|
|
174
|
+
const grant = seen.get(id);
|
|
175
|
+
if (!grant || o.memberOf(grant) !== member || (grant.expires !== undefined && grant.expires <= Date.now()))
|
|
176
|
+
continue;
|
|
177
|
+
const plain = new TextEncoder().encode(JSON.stringify({ source: 'signin', id: request.id, gen: request.gen, member, site: request.site }));
|
|
178
|
+
try {
|
|
179
|
+
await o.relay.notify({ id: request.id, title: words('browser.signin.notice'), to: [id],
|
|
180
|
+
data: { v: 1, sealed: b64urlEncode(sealBox(plain, box)) } }, { includeContent: true });
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
plain.fill(0);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
if (o.relay)
|
|
189
|
+
kit.onEvent('*', (payload, event) => {
|
|
190
|
+
const p = isRecord(payload) ? payload : {};
|
|
191
|
+
if (browserEvent(event, p) && p.kind === 'signin')
|
|
192
|
+
void pushSignIn(p.member).catch(() => { });
|
|
193
|
+
});
|
|
69
194
|
const pushApproval = async (approval) => {
|
|
70
195
|
if (!o.relay)
|
|
71
196
|
return;
|
|
@@ -122,6 +247,69 @@ export function openclawLink(kit, o) {
|
|
|
122
247
|
if (grant.role === 'view' && !VIEW_OPS.has(req.op))
|
|
123
248
|
throw refused();
|
|
124
249
|
const args = isRecord(req.args) ? req.args : {};
|
|
250
|
+
if (req.op.startsWith('oc.browser.')) {
|
|
251
|
+
// Keyed answers may be persisted by @byokit/link. Lease nonces and JPEGs are transient only.
|
|
252
|
+
if (req.key !== undefined)
|
|
253
|
+
throw refused();
|
|
254
|
+
const host = browser();
|
|
255
|
+
const action = req.op.slice('oc.browser.'.length);
|
|
256
|
+
if (action === 'state') {
|
|
257
|
+
if (args.member !== member)
|
|
258
|
+
throw refused();
|
|
259
|
+
return host.state(member);
|
|
260
|
+
}
|
|
261
|
+
if (action === 'signins')
|
|
262
|
+
return host.signIns(member);
|
|
263
|
+
if (action === 'thumb') {
|
|
264
|
+
const source = memberSource(args.source, member);
|
|
265
|
+
if (source.kind === 'desktop')
|
|
266
|
+
return { state: 'unsupported' };
|
|
267
|
+
const result = await host.thumbnail(source, { grant: grant.id });
|
|
268
|
+
return result.state === 'ok' ? { state: 'ok', frame: { ...result.frame, jpeg: b64urlEncode(result.frame.jpeg) } } : result;
|
|
269
|
+
}
|
|
270
|
+
control();
|
|
271
|
+
if (action === 'done' || action === 'confirmorigin') {
|
|
272
|
+
const lease = ownedLease(args.lease, member, grant);
|
|
273
|
+
if (action === 'done')
|
|
274
|
+
return host.done(lease);
|
|
275
|
+
if (typeof args.origin !== 'string')
|
|
276
|
+
throw refused();
|
|
277
|
+
host.confirmOrigin(lease, args.origin);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
if (action === 'forget') {
|
|
281
|
+
if (args.member !== member || typeof args.site !== 'string')
|
|
282
|
+
throw refused();
|
|
283
|
+
await host.forget(member, args.site, { grant: grant.id });
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
if (typeof args.id !== 'string' || !Number.isSafeInteger(args.gen) || args.gen < 1)
|
|
287
|
+
throw refused();
|
|
288
|
+
const request = host.signIns(member).find(r => r.id === args.id);
|
|
289
|
+
if (!request)
|
|
290
|
+
throw refused();
|
|
291
|
+
if (request.state === 'held' && leases.get(request.id)?.grant !== grant.id)
|
|
292
|
+
throw refused();
|
|
293
|
+
const by = { grant: grant.id };
|
|
294
|
+
if (action === 'takeover') {
|
|
295
|
+
if (args.confirmSite !== undefined && typeof args.confirmSite !== 'string')
|
|
296
|
+
throw refused();
|
|
297
|
+
const lease = await host.takeover(args.id, args.gen, { ...by,
|
|
298
|
+
...(typeof args.confirmSite === 'string' ? { confirmSite: args.confirmSite } : {}) });
|
|
299
|
+
memberOf(grant); // revocation during the asynchronous takeover must not publish a new lease
|
|
300
|
+
leases.set(args.id, { grant: grant.id, lease });
|
|
301
|
+
return lease;
|
|
302
|
+
}
|
|
303
|
+
if (action === 'notnow')
|
|
304
|
+
return host.notNow(args.id, args.gen, by);
|
|
305
|
+
if (action === 'reopen')
|
|
306
|
+
return host.reopen(args.id, args.gen, by);
|
|
307
|
+
if (action === 'retry')
|
|
308
|
+
return host.retry(args.id, args.gen, by);
|
|
309
|
+
if (action === 'cancel')
|
|
310
|
+
return host.cancel(args.id, args.gen, by);
|
|
311
|
+
throw refused();
|
|
312
|
+
}
|
|
125
313
|
switch (req.op) {
|
|
126
314
|
case 'oc.state': {
|
|
127
315
|
// signedIn: the providers the device member is usably signed in to (an expired or unfinished sign-in is not),
|
|
@@ -131,7 +319,7 @@ export function openclawLink(kit, o) {
|
|
|
131
319
|
...(signedIn ? { signedIn } : {}) };
|
|
132
320
|
}
|
|
133
321
|
case 'oc.routes':
|
|
134
|
-
return kit.routes()
|
|
322
|
+
return kit.routes(); // Discovery is not authorization or default eligibility.
|
|
135
323
|
case 'oc.signin.start': {
|
|
136
324
|
if (typeof args.provider !== 'string' || (args.via !== 'browser' && args.via !== 'code'))
|
|
137
325
|
throw new Error('oc.signin.start needs { provider, via: browser|code }');
|
|
@@ -229,6 +417,13 @@ export function openclawLink(kit, o) {
|
|
|
229
417
|
case 'oc.call': {
|
|
230
418
|
if (typeof args.method !== 'string')
|
|
231
419
|
throw new Error('oc.call needs { method, params }');
|
|
420
|
+
if (kit.browser && (args.method === 'browser.request' || args.method.startsWith('terminal.') || args.method === 'tools.invoke'))
|
|
421
|
+
throw refused();
|
|
422
|
+
if (args.method === 'byokit.usage.engineStarted') {
|
|
423
|
+
if (!isRecord(args.params) || args.params.agentId !== member)
|
|
424
|
+
throw refused();
|
|
425
|
+
return await kit.callDynamic(args.method, args.params);
|
|
426
|
+
}
|
|
232
427
|
if (!passThrough(args.method, grant))
|
|
233
428
|
throw refused();
|
|
234
429
|
return await kit.call(args.method, args.params);
|
|
@@ -239,6 +434,135 @@ export function openclawLink(kit, o) {
|
|
|
239
434
|
};
|
|
240
435
|
const stream = async (s, req, grant) => {
|
|
241
436
|
const member = memberOf(grant);
|
|
437
|
+
if (req.op.startsWith('oc.browser.') && req.op !== 'oc.browser.live') {
|
|
438
|
+
try {
|
|
439
|
+
const value = await handle({ op: req.op, args: req.args }, grant);
|
|
440
|
+
await s.write(`${JSON.stringify({ value })}\n`);
|
|
441
|
+
s.end();
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
const why = error instanceof Error && 'why' in error ? error.why : undefined;
|
|
445
|
+
if (typeof why === 'string' && BROWSER_REFUSALS.has(why)) {
|
|
446
|
+
await s.write(`${JSON.stringify({ refused: why })}\n`).catch(() => { });
|
|
447
|
+
s.end();
|
|
448
|
+
}
|
|
449
|
+
else
|
|
450
|
+
s.end(words('link.notAllowed'));
|
|
451
|
+
}
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (req.op === 'oc.browser.live') {
|
|
455
|
+
const args = isRecord(req.args) ? req.args : {};
|
|
456
|
+
const source = memberSource(args.source, member);
|
|
457
|
+
if (args.mode !== 'observe' && args.mode !== 'control')
|
|
458
|
+
throw refused();
|
|
459
|
+
if (source.kind === 'desktop') {
|
|
460
|
+
if (args.mode !== 'observe' || args.lease !== undefined)
|
|
461
|
+
throw refused();
|
|
462
|
+
await s.write(`${JSON.stringify({ state: { source, mode: 'observe', phase: 'failed', why: 'unsupported' } })}\n`);
|
|
463
|
+
s.end();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const lease = args.mode === 'control' ? (grant.role === 'control' ? ownedLease(args.lease, member, grant) : undefined) : undefined;
|
|
467
|
+
if (args.mode === 'control' && !lease)
|
|
468
|
+
throw refused();
|
|
469
|
+
if (args.mode === 'observe' && args.lease !== undefined)
|
|
470
|
+
throw refused();
|
|
471
|
+
if (args.maxWidth !== undefined && (!Number.isSafeInteger(args.maxWidth) || args.maxWidth < 1 || args.maxWidth > 4096))
|
|
472
|
+
throw refused();
|
|
473
|
+
let ended = false;
|
|
474
|
+
let busy = false;
|
|
475
|
+
let acceptingFrames = false;
|
|
476
|
+
let nextState;
|
|
477
|
+
let nextFrame;
|
|
478
|
+
let viewer;
|
|
479
|
+
// One in-flight write + latest state/frame: slow viewers cannot grow a JPEG queue.
|
|
480
|
+
const flush = async () => {
|
|
481
|
+
if (busy || ended)
|
|
482
|
+
return;
|
|
483
|
+
busy = true;
|
|
484
|
+
try {
|
|
485
|
+
while (!ended && (nextState !== undefined || nextFrame !== undefined)) {
|
|
486
|
+
const value = nextState ?? nextFrame;
|
|
487
|
+
if (nextState !== undefined)
|
|
488
|
+
nextState = undefined;
|
|
489
|
+
else
|
|
490
|
+
nextFrame = undefined;
|
|
491
|
+
await s.write(`${JSON.stringify(value)}\n`);
|
|
492
|
+
if (isRecord(value) && isRecord(value.state) && ['ended', 'failed'].includes(value.state.phase)) {
|
|
493
|
+
close();
|
|
494
|
+
s.end();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
close();
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
busy = false;
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
const close = () => {
|
|
506
|
+
if (ended)
|
|
507
|
+
return;
|
|
508
|
+
ended = true;
|
|
509
|
+
nextState = nextFrame = undefined;
|
|
510
|
+
viewer?.close();
|
|
511
|
+
};
|
|
512
|
+
const revokeViewer = () => { close(); s.end(words('link.notAllowed')); };
|
|
513
|
+
const active = viewers.get(grant.id) ?? new Set();
|
|
514
|
+
active.add(revokeViewer);
|
|
515
|
+
viewers.set(grant.id, active);
|
|
516
|
+
s.onEnd = () => { close(); active.delete(revokeViewer); };
|
|
517
|
+
let text = '';
|
|
518
|
+
const decoder = new TextDecoder();
|
|
519
|
+
s.onData = (chunk) => {
|
|
520
|
+
if (ended)
|
|
521
|
+
return;
|
|
522
|
+
try {
|
|
523
|
+
if (!lease || grant.role !== 'control' || (grant.expires !== undefined && grant.expires <= Date.now()))
|
|
524
|
+
throw refused();
|
|
525
|
+
text += decoder.decode(chunk, { stream: true });
|
|
526
|
+
if (text.length > 65536)
|
|
527
|
+
throw refused();
|
|
528
|
+
let at;
|
|
529
|
+
while ((at = text.indexOf('\n')) >= 0) {
|
|
530
|
+
const input = JSON.parse(text.slice(0, at));
|
|
531
|
+
text = text.slice(at + 1);
|
|
532
|
+
if (!browserInput(input))
|
|
533
|
+
throw refused();
|
|
534
|
+
ownedLease(lease, member, grant);
|
|
535
|
+
viewer?.input(input);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
close();
|
|
540
|
+
s.end(words('link.notAllowed'));
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
try {
|
|
544
|
+
viewer = browser().live(source, { grant: grant.id, ...(lease ? { lease } : {}),
|
|
545
|
+
...(typeof args.maxWidth === 'number' ? { maxWidth: args.maxWidth } : {}) }, {
|
|
546
|
+
state: state => { if (!ended) {
|
|
547
|
+
acceptingFrames = state.phase === 'live';
|
|
548
|
+
nextState = { state };
|
|
549
|
+
nextFrame = undefined;
|
|
550
|
+
void flush();
|
|
551
|
+
} },
|
|
552
|
+
frame: frame => { if (!ended && acceptingFrames) {
|
|
553
|
+
nextFrame = { frame: { ...frame, jpeg: b64urlEncode(frame.jpeg) } };
|
|
554
|
+
void flush();
|
|
555
|
+
} },
|
|
556
|
+
});
|
|
557
|
+
if (ended)
|
|
558
|
+
viewer.close();
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
close();
|
|
562
|
+
s.end(words('link.notAllowed'));
|
|
563
|
+
}
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
242
566
|
if (req.op === 'oc.run') {
|
|
243
567
|
if (grant.role === 'view')
|
|
244
568
|
throw refused();
|
|
@@ -271,6 +595,11 @@ export function openclawLink(kit, o) {
|
|
|
271
595
|
if (req.op === 'oc.events') {
|
|
272
596
|
const offEvent = kit.onEvent('*', (payload, event) => {
|
|
273
597
|
const record = payload;
|
|
598
|
+
if (browserTag(event)) {
|
|
599
|
+
if (browserEvent(event, record) && record.member === member)
|
|
600
|
+
void s.write(`${JSON.stringify({ event: 'byokit.browser', payload: { member, kind: record.kind } })}\n`).catch(() => { });
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
274
603
|
const agent = record?.agentId;
|
|
275
604
|
const key = record?.sessionKey;
|
|
276
605
|
if (agent !== member && !(typeof key === 'string' && memberKey(member, key)))
|
|
@@ -287,7 +616,7 @@ export function openclawLink(kit, o) {
|
|
|
287
616
|
}
|
|
288
617
|
throw refused();
|
|
289
618
|
};
|
|
290
|
-
return { handle, stream, allow: () => true, onAction };
|
|
619
|
+
return { handle, stream, allow: () => true, onAction, onGrantRemoved: grant => revoke(grant.id) };
|
|
291
620
|
}
|
|
292
621
|
// ponytail: this ~40-line serve() wiring is deliberately duplicated per runtime kit (D3);
|
|
293
622
|
// extract it into a shared package when a third runtime kit appears.
|
package/dist/routes.d.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import type { Route } from './types.ts';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
export type RouteFacts = {
|
|
3
|
+
platform?: 'node' | 'browser' | 'rn';
|
|
4
|
+
host?: boolean;
|
|
5
|
+
binaries?: readonly string[];
|
|
6
|
+
plugins?: readonly string[];
|
|
7
|
+
clients?: readonly string[];
|
|
8
|
+
};
|
|
9
|
+
export type RouteView = Route & {
|
|
10
|
+
readiness: NonNullable<Route['readiness']>;
|
|
11
|
+
};
|
|
12
|
+
/** Every choice, including external dependencies and providers without a wizard choice.
|
|
13
|
+
* `offerPolicy` is eligibility; legacy boolean `offer` additionally requires readiness.
|
|
14
|
+
* No facts means the kit's Node host with bundled plugins, not installed external plugins or CLIs.
|
|
15
|
+
* External installs belong to the explicit host operation; discovery never performs one.
|
|
16
|
+
*/
|
|
17
|
+
export declare function routes(facts?: RouteFacts): RouteView[];
|
|
18
|
+
/** Retain explicit legacy selectors alongside corrected discovery metadata. A native CLI selector still
|
|
19
|
+
* reaches engine detection (which checks its own login); it does not make an unavailable route default.
|
|
20
|
+
* API billing is never picked as a fallback. */
|
|
10
21
|
export declare function routeFor(provider: string, via: 'browser' | 'code'): Route | undefined;
|
package/dist/routes.js
CHANGED
|
@@ -1,16 +1,49 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Complete pinned discovery. No credential reads, plugin installation or live qualification.
|
|
2
2
|
import routesJson from './routes.json' with { type: 'json' };
|
|
3
3
|
const table = routesJson;
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return table
|
|
4
|
+
/** Every choice, including external dependencies and providers without a wizard choice.
|
|
5
|
+
* `offerPolicy` is eligibility; legacy boolean `offer` additionally requires readiness.
|
|
6
|
+
* No facts means the kit's Node host with bundled plugins, not installed external plugins or CLIs.
|
|
7
|
+
* External installs belong to the explicit host operation; discovery never performs one.
|
|
8
|
+
*/
|
|
9
|
+
export function routes(facts = {}) {
|
|
10
|
+
const platform = facts.platform ?? 'node';
|
|
11
|
+
return table.map(route => {
|
|
12
|
+
let readiness = 'ready';
|
|
13
|
+
let why;
|
|
14
|
+
if (route.upstream?.flow === 'absent') {
|
|
15
|
+
readiness = 'no_upstream_flow';
|
|
16
|
+
why = route.reason;
|
|
17
|
+
}
|
|
18
|
+
else if (route.platforms?.[platform] === 'no') {
|
|
19
|
+
readiness = 'unsupported_platform';
|
|
20
|
+
why = `Not supported on ${platform}.`;
|
|
21
|
+
}
|
|
22
|
+
else if (route.platforms?.[platform] === 'host' && !facts.host) {
|
|
23
|
+
readiness = 'needs_host';
|
|
24
|
+
why = 'Needs the linked engine host.';
|
|
25
|
+
}
|
|
26
|
+
else if (route.needs?.binary && !facts.binaries?.includes(route.needs.binary)) {
|
|
27
|
+
readiness = 'needs_binary';
|
|
28
|
+
why = `Needs ${route.needs.binary} in the isolated HOME.`;
|
|
29
|
+
}
|
|
30
|
+
else if (route.needs?.plugin && !facts.plugins?.includes(route.needs.plugin)) {
|
|
31
|
+
readiness = 'needs_plugin';
|
|
32
|
+
why = `Needs plugin ${route.needs.plugin}; discovery does not install it.`;
|
|
33
|
+
}
|
|
34
|
+
else if (route.needs?.client && !facts.clients?.includes(route.needs.client)) {
|
|
35
|
+
readiness = 'needs_client';
|
|
36
|
+
why = `Needs host client registration ${route.needs.client}.`;
|
|
37
|
+
}
|
|
38
|
+
return { ...route, readiness, ...(why ? { why } : {}),
|
|
39
|
+
offer: route.offerPolicy === 'default' && route.billing === 'subscription' && readiness === 'ready' };
|
|
40
|
+
});
|
|
12
41
|
}
|
|
13
|
-
/**
|
|
42
|
+
/** Retain explicit legacy selectors alongside corrected discovery metadata. A native CLI selector still
|
|
43
|
+
* reaches engine detection (which checks its own login); it does not make an unavailable route default.
|
|
44
|
+
* API billing is never picked as a fallback. */
|
|
14
45
|
export function routeFor(provider, via) {
|
|
15
|
-
return table.find(
|
|
46
|
+
return table.find(route => route.billing === 'subscription' && route.offerPolicy === 'default'
|
|
47
|
+
&& ((route.offer && route.provider === provider && route.via === via)
|
|
48
|
+
|| (route.legacy?.provider === provider && route.legacy.via === via)));
|
|
16
49
|
}
|