@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/kit.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { PROTOCOL_VERSION } from "./constants.js";
|
|
6
|
+
import { atomic } from "./engine-patches.js";
|
|
6
7
|
import { Approvals } from "./approvals.js";
|
|
7
8
|
import { Bridge } from "./bridge.js";
|
|
8
9
|
import { Engine } from "./engine.js";
|
|
@@ -15,7 +16,10 @@ import { outputSchema } from "./output.js";
|
|
|
15
16
|
import { createKeys } from "./keys.js";
|
|
16
17
|
import { routes as routeTable } from "./routes.js";
|
|
17
18
|
import { providers as engineProviders, signIn as startSignIn, signOut as engineSignOut } from "./signin.js";
|
|
18
|
-
import { reconcileConfig, memoryLimited as configMemoryLimited } from "./config.js";
|
|
19
|
+
import { reconcileConfig, browserToolPolicySafe, browserProfileAcknowledged, browserSessionMayRun, memoryLimited as configMemoryLimited, configKeyPath, readConfigKey, writeConfigKey } from "./config.js";
|
|
20
|
+
import { createBrowserHost } from "./browser/host.js";
|
|
21
|
+
import { fileSignInStore } from "./browser/store.js";
|
|
22
|
+
import { launchBroker } from "./browser/broker.js";
|
|
19
23
|
const operatorMethods = new Set([
|
|
20
24
|
'agent',
|
|
21
25
|
'agent.identity.get',
|
|
@@ -427,20 +431,217 @@ export class OpenClawKit {
|
|
|
427
431
|
approvalsCtl;
|
|
428
432
|
listeners = new Set();
|
|
429
433
|
off = [];
|
|
434
|
+
signInDisconnects = new Set();
|
|
430
435
|
starting;
|
|
431
436
|
stopping = false;
|
|
432
437
|
failures = 0;
|
|
438
|
+
browser;
|
|
439
|
+
browserHost;
|
|
440
|
+
brokers = new Map();
|
|
441
|
+
browserSafe = false;
|
|
442
|
+
browserAttaching = new Map();
|
|
443
|
+
browserPolicySessions = new Set();
|
|
444
|
+
browserProfiles = {};
|
|
445
|
+
browserCapabilities = new Set();
|
|
446
|
+
browserConfig() { return { profiles: this.browserProfiles, tools: this.toolNames() }; }
|
|
447
|
+
async launchBrowser(member) {
|
|
448
|
+
let owned;
|
|
449
|
+
const broker = await launchBroker({ executablePath: this.o.browser.executablePath,
|
|
450
|
+
profileDir: join(this.o.stateDir, 'browser', member, 'profile'), member,
|
|
451
|
+
onExit: () => {
|
|
452
|
+
if (!this.stopping && owned && this.browserHost?.brokerBinding(member)?.broker === owned)
|
|
453
|
+
void this.browserHost.browserGone(member, owned).catch(() => { });
|
|
454
|
+
} });
|
|
455
|
+
owned = broker;
|
|
456
|
+
try {
|
|
457
|
+
const endpoint = broker.endpoint().cdpUrl, url = new URL(endpoint);
|
|
458
|
+
this.browserCapabilities.add(endpoint);
|
|
459
|
+
const token = url.searchParams.get('token');
|
|
460
|
+
if (!token)
|
|
461
|
+
throw new Error('browser capability unavailable');
|
|
462
|
+
this.browserCapabilities.add(token);
|
|
463
|
+
const id = url.pathname.split('/').at(-1);
|
|
464
|
+
if (id && /^[a-f0-9]{32}$/i.test(id))
|
|
465
|
+
this.browserCapabilities.add(id);
|
|
466
|
+
await broker.fence(true);
|
|
467
|
+
return broker;
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
await broker.close();
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async prepareBrowsers() {
|
|
475
|
+
if (!this.o.browser || this.browserHost)
|
|
476
|
+
return;
|
|
477
|
+
await this.engine.prepare();
|
|
478
|
+
const c = JSON.parse(readFileSync(join(this.engine.root, 'openclaw.json'), 'utf8'));
|
|
479
|
+
if (browserToolPolicySafe(c, this.toolNames())) {
|
|
480
|
+
const members = this.o.browser.members === 'all' ? Object.keys(c.agents?.entries ?? { main: {} }) : this.o.browser.members;
|
|
481
|
+
for (const member of members)
|
|
482
|
+
if (this.browserMember(member) && !this.brokers.has(member)) {
|
|
483
|
+
const broker = await this.launchBrowser(member);
|
|
484
|
+
this.brokers.set(member, broker);
|
|
485
|
+
this.browserProfiles[`byokit-${member}`] = { ...broker.endpoint(), attachOnly: true };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
this.browserHost = await createBrowserHost({ brokers: this.brokers, store: fileSignInStore(this.o.stateDir),
|
|
489
|
+
options: this.o.browser, authorize: (grant, member) => !!grant && this.browserMember(member),
|
|
490
|
+
park: async (_member, key) => this.abort(key), resume: async () => 'unknown',
|
|
491
|
+
// Production cannot raise while unprotected. Never substitute a guessed registrable site.
|
|
492
|
+
siteOf: () => { throw new Error('browser sign-in handoff unavailable'); },
|
|
493
|
+
ping: (member, kind) => {
|
|
494
|
+
for (const fn of this.listeners)
|
|
495
|
+
fn({ event: 'byokit.browser', payload: { member, kind } });
|
|
496
|
+
if (kind === 'state' && this.browserHost)
|
|
497
|
+
void this.publishBrowserProfile(member).catch(() => { });
|
|
498
|
+
},
|
|
499
|
+
// Host recovery owns the registry transition. Never recursively attach or publish a still-unowned endpoint.
|
|
500
|
+
restart: member => this.launchBrowser(member),
|
|
501
|
+
});
|
|
502
|
+
const controller = this.browserHost;
|
|
503
|
+
this.browser = {
|
|
504
|
+
state: member => this.browserState(member), signIns: controller.signIns.bind(controller),
|
|
505
|
+
takeover: controller.takeover.bind(controller), confirmOrigin: controller.confirmOrigin.bind(controller),
|
|
506
|
+
done: controller.done.bind(controller), notNow: controller.notNow.bind(controller),
|
|
507
|
+
reopen: controller.reopen.bind(controller), retry: controller.retry.bind(controller),
|
|
508
|
+
cancel: controller.cancel.bind(controller),
|
|
509
|
+
thumbnail: (source, by) => this.browserSafe ? controller.thumbnail(source, by) : Promise.resolve({ state: 'off' }),
|
|
510
|
+
live: (source, options, on) => {
|
|
511
|
+
if (!this.browserSafe)
|
|
512
|
+
throw new Error('browser tool policy refused');
|
|
513
|
+
return controller.live(source, options, on);
|
|
514
|
+
}, forget: controller.forget.bind(controller),
|
|
515
|
+
...{ revokeGrant: controller.revokeGrant.bind(controller) },
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
async publishBrowserProfile(member) {
|
|
519
|
+
const binding = this.browserHost?.brokerBinding(member);
|
|
520
|
+
if (!binding)
|
|
521
|
+
return;
|
|
522
|
+
const { broker, generation, endpoint } = binding;
|
|
523
|
+
const name = `byokit-${member}`;
|
|
524
|
+
if (this.browserProfiles[name]?.cdpUrl === endpoint.cdpUrl)
|
|
525
|
+
return;
|
|
526
|
+
this.brokers.set(member, broker);
|
|
527
|
+
await broker.fence(true);
|
|
528
|
+
this.browserProfiles[name] = { ...endpoint, attachOnly: true };
|
|
529
|
+
try {
|
|
530
|
+
await this.patchConfig({});
|
|
531
|
+
const current = this.browserHost?.brokerBinding(member);
|
|
532
|
+
if (current?.broker !== broker || current.generation !== generation)
|
|
533
|
+
throw new Error('browser binding changed');
|
|
534
|
+
await this.checkBrowserTools(); // acknowledges the exact profile and rechecks policy/no-open request before unfencing
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
this.browserSafe = false;
|
|
538
|
+
await broker.fence(true);
|
|
539
|
+
throw new Error('browser endpoint unavailable');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
browserMember(member) {
|
|
543
|
+
return MEMBER_ID.test(member) && !member.startsWith(KEY_PREFIX) && !!this.o.browser
|
|
544
|
+
&& (this.o.browser.members === 'all' || this.o.browser.members.includes(member));
|
|
545
|
+
}
|
|
546
|
+
browserState(member) {
|
|
547
|
+
if (!this.browserSafe)
|
|
548
|
+
return { member, phase: 'blocked', why: 'unsafe-tools' };
|
|
549
|
+
return this.browserHost?.state(member) ?? { member, phase: 'off', why: 'no-browser' };
|
|
550
|
+
}
|
|
551
|
+
async checkBrowserTools() {
|
|
552
|
+
if (!this.o.browser)
|
|
553
|
+
return true;
|
|
554
|
+
this.browserSafe = false;
|
|
555
|
+
try {
|
|
556
|
+
const configPath = join(this.engine.root, 'openclaw.json');
|
|
557
|
+
const disk = readFileSync(configPath, 'utf8');
|
|
558
|
+
const snapshot = await this.request()('config.get');
|
|
559
|
+
const c = snapshot.config;
|
|
560
|
+
const owned = JSON.parse(disk);
|
|
561
|
+
if (!browserToolPolicySafe(c, this.toolNames()))
|
|
562
|
+
return false;
|
|
563
|
+
const roster = await this.request()('agents.list');
|
|
564
|
+
if (!Array.isArray(roster.agents) || !roster.agents.length)
|
|
565
|
+
return false;
|
|
566
|
+
const safe = new Set(['browser', 'request_sign_in', ...this.toolNames()]);
|
|
567
|
+
for (const { id } of roster.agents) {
|
|
568
|
+
if (!MEMBER_ID.test(id))
|
|
569
|
+
return false;
|
|
570
|
+
const sessionKey = `agent:${id}:byokit-browser-policy`;
|
|
571
|
+
if (!this.browserPolicySessions.has(sessionKey)) {
|
|
572
|
+
// The published tools.effective route requires an existing session. No message/task or model submission.
|
|
573
|
+
await this.request()('sessions.create', { agentId: id, key: sessionKey, label: 'Browser tool policy' });
|
|
574
|
+
this.browserPolicySessions.add(sessionKey);
|
|
575
|
+
}
|
|
576
|
+
const policy = await this.request()('tools.effective', { agentId: id, sessionKey });
|
|
577
|
+
if (policy.agentId !== id || !Array.isArray(policy.groups)
|
|
578
|
+
|| policy.groups.some(group => !Array.isArray(group.tools) || group.tools.some(tool => !safe.has(tool.id))))
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
const stable = disk === readFileSync(configPath, 'utf8');
|
|
582
|
+
for (const [member, broker] of this.brokers) {
|
|
583
|
+
const binding = this.browserHost?.brokerBinding(member);
|
|
584
|
+
const profile = c.browser?.profiles?.[`byokit-${member}`];
|
|
585
|
+
if (!binding || binding.broker !== broker || !browserProfileAcknowledged(profile, owned.browser?.profiles?.[`byokit-${member}`], binding.endpoint.cdpUrl, snapshot, stable))
|
|
586
|
+
return false;
|
|
587
|
+
if (!this.browserHost?.signIns(member).some(r => ['waiting', 'held', 'checking'].includes(r.state))) {
|
|
588
|
+
await broker.fence(false);
|
|
589
|
+
const current = this.browserHost?.brokerBinding(member);
|
|
590
|
+
if (current?.broker !== broker || current.generation !== binding.generation)
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
this.browserSafe = true;
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
finally {
|
|
601
|
+
if (!this.browserSafe)
|
|
602
|
+
for (const broker of this.brokers.values())
|
|
603
|
+
await broker.fence(true);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async browserGate(run, tool) {
|
|
607
|
+
if (!this.o.browser)
|
|
608
|
+
return undefined;
|
|
609
|
+
const safe = await this.checkBrowserTools();
|
|
610
|
+
const allowed = new Set(['browser', 'request_sign_in', ...this.toolNames()]);
|
|
611
|
+
if (!safe || !allowed.has(tool))
|
|
612
|
+
return 'browser tool policy refused';
|
|
613
|
+
if ((tool === 'browser' || tool === 'request_sign_in') && !this.browserMember(run.member))
|
|
614
|
+
return 'browser member unavailable';
|
|
615
|
+
if ((tool === 'browser' || tool === 'request_sign_in') && this.browserHost?.signIns(run.member)
|
|
616
|
+
.some(r => ['waiting', 'held', 'checking'].includes(r.state)))
|
|
617
|
+
return 'Waiting for the person to sign in';
|
|
618
|
+
if (tool === 'request_sign_in')
|
|
619
|
+
return 'browser sign-in handoff unavailable';
|
|
620
|
+
return undefined;
|
|
621
|
+
}
|
|
433
622
|
constructor(o) {
|
|
434
623
|
if (o.tools?.length && !o.host)
|
|
435
624
|
throw new Error('host required when tools are registered');
|
|
625
|
+
if (o.browser && o.gateBuiltins === false)
|
|
626
|
+
throw new Error('gate-off');
|
|
627
|
+
if (o.browser && (!o.browser.executablePath.startsWith('/') || (o.browser.members !== 'all'
|
|
628
|
+
&& (!Array.isArray(o.browser.members) || o.browser.members.some(m => !MEMBER_ID.test(m) || m.startsWith(KEY_PREFIX))))))
|
|
629
|
+
throw new Error('invalid browser options');
|
|
436
630
|
// The engine lowercases and alias-maps a tool name before the gate hook sees it (bash -> exec, cron ->
|
|
437
631
|
// automations); a name it would rewrite reaches the gate as a builtin, so refuse it here.
|
|
438
632
|
for (const t of o.tools ?? []) {
|
|
439
|
-
if (!/^[a-z][a-z0-9_]*$/.test(t.name) || t.name === 'bash' || t.name === 'cron'
|
|
633
|
+
if (!/^[a-z][a-z0-9_]*$/.test(t.name) || t.name === 'bash' || t.name === 'cron'
|
|
634
|
+
|| (o.browser && ['browser', 'request_sign_in', 'exec', 'process', 'code_execution', 'terminal', 'read', 'write', 'edit', 'apply_patch', 'gateway'].includes(t.name)))
|
|
440
635
|
throw new Error(`invalid tool name: ${t.name}`);
|
|
441
636
|
}
|
|
442
637
|
this.o = o;
|
|
443
|
-
this.engine = new Engine({ ...o, pluginId: o.plugin?.id ?? 'byokit', tools: o.tools ?? [],
|
|
638
|
+
this.engine = new Engine({ ...o, pluginId: o.plugin?.id ?? 'byokit', tools: [...o.tools ?? [], ...(o.browser ? [{
|
|
639
|
+
name: 'request_sign_in', description: 'Ask the person to sign in to the current browser site.',
|
|
640
|
+
parameters: { type: 'object', additionalProperties: false, properties: {
|
|
641
|
+
note: { type: 'string', maxLength: 140 }, targetId: { type: 'string' }
|
|
642
|
+
} },
|
|
643
|
+
}] : [])],
|
|
644
|
+
...(o.browser ? { browserConfig: () => this.browserConfig() } : {}),
|
|
444
645
|
gateBuiltins: o.gateBuiltins !== false, spawnEngine: o.spawnEngine !== false,
|
|
445
646
|
onState: (s) => this.setState(s), onExit: () => this.closed('engine exited') });
|
|
446
647
|
this.keys = createKeys({ root: this.engine.root,
|
|
@@ -452,14 +653,46 @@ export class OpenClawKit {
|
|
|
452
653
|
request: (method, params, co) => this.request()(method, params, co),
|
|
453
654
|
bridge: { resolveAsk: (id, d) => slot.bridge?.resolveAsk(id, d) ?? false },
|
|
454
655
|
});
|
|
455
|
-
|
|
656
|
+
const host = o.browser ? {
|
|
657
|
+
gate: async (run, tool, input, info) => {
|
|
658
|
+
const refusal = await this.browserGate(run, tool);
|
|
659
|
+
if (refusal)
|
|
660
|
+
return { allow: false, reason: refusal };
|
|
661
|
+
const result = await o.host?.gate(run, tool, input, info) ?? { allow: false, reason: 'tool host unavailable' };
|
|
662
|
+
const changed = await this.browserGate(run, tool);
|
|
663
|
+
return changed ? { allow: false, reason: changed } : result;
|
|
664
|
+
},
|
|
665
|
+
call: async (run, tool, input, signal) => {
|
|
666
|
+
const refusal = await this.browserGate(run, tool);
|
|
667
|
+
if (refusal)
|
|
668
|
+
throw new Error(refusal);
|
|
669
|
+
if (!o.host)
|
|
670
|
+
throw new Error('tool host unavailable');
|
|
671
|
+
return o.host.call(run, tool, input, signal);
|
|
672
|
+
},
|
|
673
|
+
} : o.host;
|
|
674
|
+
this.bridge = new Bridge({ path: this.engine.bridgeSock, host, tools: new Set([...this.toolNames(), ...(o.browser ? ['request_sign_in'] : [])]),
|
|
675
|
+
...(o.browser ? { browserCapabilities: () => [...this.browserCapabilities],
|
|
676
|
+
beforeAgentRun: async (key, runId) => {
|
|
677
|
+
// No action-id submission proof or redispatch claim. Open and unproven resumes are always refused.
|
|
678
|
+
if (!await this.checkBrowserTools() || !this.browserHost)
|
|
679
|
+
return false;
|
|
680
|
+
return browserSessionMayRun(this.browserHost.signIns(), key, this.bridge.isRegisteredRun(key, runId));
|
|
681
|
+
} } : {}),
|
|
456
682
|
permitted: o.permitted ?? (() => true), approvalTimeoutMs: o.approvalTimeoutMs ?? 180_000,
|
|
457
683
|
onAsk: (a) => this.approvalsCtl.add(a), onAskGone: (id) => this.approvalsCtl.remove(id) });
|
|
458
684
|
slot.bridge = this.bridge;
|
|
459
685
|
}
|
|
460
|
-
setState(s) {
|
|
686
|
+
setState(s) {
|
|
687
|
+
this.current = { ...s, ...(this.engine.patchSet !== undefined ? { patchSet: this.engine.patchSet } : {}) };
|
|
688
|
+
this.o.onState?.(this.current);
|
|
689
|
+
}
|
|
461
690
|
get state() { return this.current; }
|
|
462
|
-
prepare() {
|
|
691
|
+
async prepare() {
|
|
692
|
+
await this.engine.prepare();
|
|
693
|
+
if (this.engine.patchSet !== undefined)
|
|
694
|
+
this.setState(this.current);
|
|
695
|
+
}
|
|
463
696
|
start() {
|
|
464
697
|
if (this.current.phase === 'ready')
|
|
465
698
|
return Promise.resolve();
|
|
@@ -475,6 +708,7 @@ export class OpenClawKit {
|
|
|
475
708
|
async connect() {
|
|
476
709
|
let transport;
|
|
477
710
|
try {
|
|
711
|
+
await this.prepareBrowsers();
|
|
478
712
|
const ctx = await this.engine.start();
|
|
479
713
|
if (!ctx || this.stopping)
|
|
480
714
|
return;
|
|
@@ -503,18 +737,26 @@ export class OpenClawKit {
|
|
|
503
737
|
this.off.push(transport.onEvent((e) => this.approvalsCtl.handleEvent(e)));
|
|
504
738
|
this.failures = 0;
|
|
505
739
|
this.setState({ phase: 'ready' });
|
|
740
|
+
if (this.o.browser)
|
|
741
|
+
await this.checkBrowserTools();
|
|
506
742
|
// Replays the engine's native approval lists over the now-live transport (N9).
|
|
507
743
|
await this.approvalsCtl.resync();
|
|
508
744
|
}
|
|
509
745
|
catch (error) {
|
|
510
746
|
const needsUpdate = this.current.phase === 'needs-update';
|
|
747
|
+
const patchFailure = this.current.why === 'engine-patch';
|
|
511
748
|
if (transport && this.transport === transport)
|
|
512
749
|
await this.disconnect();
|
|
513
750
|
await this.engine.stop();
|
|
751
|
+
await this.closeBrowsers();
|
|
514
752
|
if (error instanceof Error && 'code' in error && error.code === 'engine-already-running') {
|
|
515
753
|
this.setState({ phase: 'failed', why: 'engine-already-running' });
|
|
516
754
|
throw error;
|
|
517
755
|
}
|
|
756
|
+
if (patchFailure) {
|
|
757
|
+
this.setState({ phase: 'failed', why: 'engine-patch' });
|
|
758
|
+
throw error;
|
|
759
|
+
}
|
|
518
760
|
if (needsUpdate)
|
|
519
761
|
this.setState({ phase: 'needs-update', why: 'version' });
|
|
520
762
|
if (!needsUpdate && !this.stopping)
|
|
@@ -528,6 +770,7 @@ export class OpenClawKit {
|
|
|
528
770
|
this.transport = undefined;
|
|
529
771
|
this.greeting = undefined;
|
|
530
772
|
this.members = undefined;
|
|
773
|
+
this.browserPolicySessions.clear();
|
|
531
774
|
this.bridge.stop();
|
|
532
775
|
await transport?.stop();
|
|
533
776
|
}
|
|
@@ -535,6 +778,8 @@ export class OpenClawKit {
|
|
|
535
778
|
const engineFailed = _why === 'engine exited' && this.current.phase === 'failed' && this.current.why === 'exited';
|
|
536
779
|
if (this.stopping || (this.current.phase !== 'ready' && !engineFailed))
|
|
537
780
|
return;
|
|
781
|
+
for (const fn of this.signInDisconnects)
|
|
782
|
+
fn();
|
|
538
783
|
void this.disconnect();
|
|
539
784
|
if (this.o.spawnEngine === false) {
|
|
540
785
|
this.setState({ phase: 'failed', why: 'handshake' });
|
|
@@ -549,6 +794,20 @@ export class OpenClawKit {
|
|
|
549
794
|
this.stopping = true;
|
|
550
795
|
await this.disconnect();
|
|
551
796
|
await this.engine.stop();
|
|
797
|
+
await this.closeBrowsers();
|
|
798
|
+
}
|
|
799
|
+
async closeBrowsers() {
|
|
800
|
+
if (this.browserHost)
|
|
801
|
+
await this.browserHost.close();
|
|
802
|
+
else
|
|
803
|
+
for (const broker of this.brokers.values())
|
|
804
|
+
await broker.close();
|
|
805
|
+
this.browserHost = undefined;
|
|
806
|
+
this.browser = undefined;
|
|
807
|
+
this.browserSafe = false;
|
|
808
|
+
this.brokers.clear();
|
|
809
|
+
for (const profile of Object.keys(this.browserProfiles))
|
|
810
|
+
delete this.browserProfiles[profile];
|
|
552
811
|
}
|
|
553
812
|
request() {
|
|
554
813
|
if (!this.transport || this.current.phase !== 'ready')
|
|
@@ -556,6 +815,8 @@ export class OpenClawKit {
|
|
|
556
815
|
return this.transport.request.bind(this.transport);
|
|
557
816
|
}
|
|
558
817
|
call(method, params, o) {
|
|
818
|
+
if (this.o.browser && /^(config\.(apply|patch|set)|agents\.(create|update|delete)|plugins\.(install|uninstall|setEnabled|refresh))$/.test(method))
|
|
819
|
+
return Promise.reject(new Error('browser policy changes require guarded patchConfig'));
|
|
559
820
|
try {
|
|
560
821
|
return this.request()(method, params, o);
|
|
561
822
|
}
|
|
@@ -582,15 +843,34 @@ export class OpenClawKit {
|
|
|
582
843
|
this.listeners.add(listener);
|
|
583
844
|
return () => { this.listeners.delete(listener); };
|
|
584
845
|
}
|
|
585
|
-
ensureMember(member) {
|
|
846
|
+
async ensureMember(member) {
|
|
586
847
|
if (!this.members)
|
|
587
|
-
|
|
588
|
-
|
|
848
|
+
throw new Error('gateway not ready');
|
|
849
|
+
const result = await this.members.ensure(member);
|
|
850
|
+
if (!this.o.browser || !this.browserMember(member) || this.brokers.has(member) || !this.browserHost)
|
|
851
|
+
return result;
|
|
852
|
+
let attaching = this.browserAttaching.get(member);
|
|
853
|
+
if (!attaching) {
|
|
854
|
+
attaching = (async () => {
|
|
855
|
+
if (!await this.checkBrowserTools())
|
|
856
|
+
return;
|
|
857
|
+
const broker = await this.launchBrowser(member);
|
|
858
|
+
await this.browserHost.attachBroker(member, broker);
|
|
859
|
+
this.brokers.set(member, broker);
|
|
860
|
+
await broker.fence(true);
|
|
861
|
+
await this.publishBrowserProfile(member);
|
|
862
|
+
})();
|
|
863
|
+
this.browserAttaching.set(member, attaching);
|
|
864
|
+
void attaching.finally(() => { this.browserAttaching.delete(member); }).catch(() => { });
|
|
865
|
+
}
|
|
866
|
+
await attaching;
|
|
867
|
+
return result;
|
|
589
868
|
}
|
|
590
869
|
// sign-in (5.7): every body here is the module's, with this kit's transport, members and ports.
|
|
591
870
|
signInCtx() {
|
|
592
871
|
return { request: (method, params, o) => this.request()(method, params, o),
|
|
593
|
-
ensure: (member) => this.ensureMember(member), callbackPort: this.o.callbackPort ?? 1455
|
|
872
|
+
ensure: (member) => this.ensureMember(member), callbackPort: this.o.callbackPort ?? 1455,
|
|
873
|
+
onDisconnect: (fn) => { this.signInDisconnects.add(fn); return () => { this.signInDisconnects.delete(fn); }; } };
|
|
594
874
|
}
|
|
595
875
|
routes() {
|
|
596
876
|
return routeTable();
|
|
@@ -630,7 +910,7 @@ export class OpenClawKit {
|
|
|
630
910
|
};
|
|
631
911
|
},
|
|
632
912
|
ensure: (member) => this.ensureMember(member),
|
|
633
|
-
bridge: this.bridge,
|
|
913
|
+
bridge: { register: (run, tools, runId) => this.bridge.register(run, tools && this.o.browser ? [...tools, 'request_sign_in'] : tools, runId) },
|
|
634
914
|
tools: new Set(this.toolNames()),
|
|
635
915
|
});
|
|
636
916
|
}
|
|
@@ -638,7 +918,13 @@ export class OpenClawKit {
|
|
|
638
918
|
toolNames() {
|
|
639
919
|
return (this.o.tools ?? []).map((t) => t.name);
|
|
640
920
|
}
|
|
641
|
-
run(spec, on) {
|
|
921
|
+
async run(spec, on) {
|
|
922
|
+
if (this.o.browser) {
|
|
923
|
+
if (!await this.checkBrowserTools())
|
|
924
|
+
return { ok: false, kind: 'other', message: 'browser tool policy refused' };
|
|
925
|
+
if (!await this.browserHost?.beforeRun(spec.member, this.runKey(spec.sessionKey, spec)))
|
|
926
|
+
return { ok: false, kind: 'other', message: 'Waiting for the person to sign in' };
|
|
927
|
+
}
|
|
642
928
|
if (spec.auth !== undefined && spec.auth !== 'apiKey')
|
|
643
929
|
return Promise.reject(new Error('Choose a supported account option.'));
|
|
644
930
|
// Snapshot before account readiness awaits; invalid schemas never reach the Gateway.
|
|
@@ -699,12 +985,37 @@ export class OpenClawKit {
|
|
|
699
985
|
const safe = reconcileConfig(response.config, { root: this.engine.root, stateDir: this.o.stateDir,
|
|
700
986
|
port: Number(readFileSync(join(this.engine.root, 'port'), 'utf8')), pluginId: this.o.plugin?.id ?? 'byokit',
|
|
701
987
|
pluginDir: join(this.engine.root, 'plugin'), policyPath: fileURLToPath(new URL('../policy/policy.mjs', import.meta.url)), app: patch,
|
|
702
|
-
installPolicy: this.o.installPolicy });
|
|
988
|
+
installPolicy: this.o.installPolicy, ...(this.o.browser ? { browser: this.browserConfig() } : {}) });
|
|
989
|
+
if (this.o.browser && !browserToolPolicySafe(safe, this.toolNames()))
|
|
990
|
+
throw new Error('browser tool policy refused');
|
|
991
|
+
if (this.o.browser) {
|
|
992
|
+
this.browserSafe = false;
|
|
993
|
+
for (const broker of this.brokers.values())
|
|
994
|
+
await broker.fence(true);
|
|
995
|
+
}
|
|
703
996
|
await this.request()('config.patch', { raw: JSON.stringify(safe), baseHash: response.hash, ...(o?.agentId ? { agentId: o.agentId } : {}) });
|
|
704
997
|
}
|
|
705
998
|
memoryLimited(member) {
|
|
706
999
|
const config = JSON.parse(readFileSync(join(this.engine.root, 'openclaw.json'), 'utf8'));
|
|
707
1000
|
return configMemoryLimited(config, member);
|
|
708
1001
|
}
|
|
1002
|
+
// One key, not the whole config: stock config.get redacts token-bearing values and needs a gateway round
|
|
1003
|
+
// trip, so it can never hand back a value the app may write unchanged. These read and write that one
|
|
1004
|
+
// dotted key in the file prepare() owns; the engine applies it at its next boot.
|
|
1005
|
+
getConfigKey(key) {
|
|
1006
|
+
const config = JSON.parse(readFileSync(join(this.engine.root, 'openclaw.json'), 'utf8'));
|
|
1007
|
+
return readConfigKey(config, configKeyPath(key));
|
|
1008
|
+
}
|
|
1009
|
+
setConfigKey(key, value) {
|
|
1010
|
+
const path = join(this.engine.root, 'openclaw.json');
|
|
1011
|
+
const text = readFileSync(path, 'utf8');
|
|
1012
|
+
const config = JSON.parse(text);
|
|
1013
|
+
const previous = writeConfigKey(config, configKeyPath(key), value);
|
|
1014
|
+
// prepare() writes exactly this shape, so an unchanged value leaves the file byte for byte alone.
|
|
1015
|
+
const next = JSON.stringify(config, null, 2) + '\n';
|
|
1016
|
+
if (next !== text)
|
|
1017
|
+
atomic(path, next);
|
|
1018
|
+
return previous;
|
|
1019
|
+
}
|
|
709
1020
|
doctorContext() { return this.engine.doctorContext(); }
|
|
710
1021
|
}
|
package/dist/link.d.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
import type { BrowserHost } from './browser.ts';
|
|
1
2
|
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
3
|
import { type Grant, type Host, type HostOptions } from '@byokit/link';
|
|
3
4
|
import { type ServeIngress, type Via } from '@byokit/reach';
|
|
4
5
|
import type { PushAction, RelayClient } from '@byokit/relay';
|
|
5
6
|
import type { OpenClawKit } from './kit.ts';
|
|
6
7
|
import type { Member } from './types.ts';
|
|
7
|
-
export declare function openclawLink(kit: OpenClawKit
|
|
8
|
+
export declare function openclawLink(kit: OpenClawKit & {
|
|
9
|
+
browser?: BrowserHost;
|
|
10
|
+
}, o: {
|
|
8
11
|
memberOf: (grant: Grant) => Member | undefined;
|
|
9
12
|
passThrough?: (method: string, grant: Grant) => boolean;
|
|
10
13
|
relay?: RelayClient;
|
|
11
|
-
}): Pick<HostOptions, 'handle' | 'stream' | 'allow'> & {
|
|
14
|
+
}): Pick<HostOptions, 'handle' | 'stream' | 'allow' | 'onGrantRemoved'> & {
|
|
12
15
|
onAction(a: PushAction): Promise<unknown>;
|
|
13
16
|
};
|
|
14
17
|
export declare function serve(o: {
|