@byokit/openclaw 0.5.1 → 0.6.2
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 +56 -3
- package/README.md +205 -15
- package/SECURITY.md +6 -6
- package/dist/auth-store.d.ts +6 -0
- package/dist/auth-store.js +58 -15
- 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 +1003 -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 +78 -0
- package/dist/day-usage.js +104 -0
- package/dist/device.d.ts +38 -5
- 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 +9 -1
- package/dist/index.js +2 -0
- package/dist/kit.d.ts +28 -3
- package/dist/kit.js +319 -12
- package/dist/link.d.ts +16 -5
- package/dist/link.js +333 -6
- 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 +1 -1
- package/dist/signin.js +19 -4
- package/dist/testing/browser.d.ts +50 -0
- package/dist/testing/browser.js +166 -0
- package/dist/testing/contract.js +1 -2
- package/dist/testing/fake-gateway.d.ts +14 -7
- package/dist/testing/fake-gateway.js +8 -6
- package/dist/testing/index.d.ts +3 -2
- package/dist/testing/index.js +1 -0
- package/dist/testing/model-stub.d.ts +15 -1
- package/dist/testing/model-stub.js +11 -11
- 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 +53 -0
- package/dist/usage.js +60 -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',
|
|
@@ -431,17 +435,213 @@ export class OpenClawKit {
|
|
|
431
435
|
starting;
|
|
432
436
|
stopping = false;
|
|
433
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
|
+
}
|
|
434
622
|
constructor(o) {
|
|
435
623
|
if (o.tools?.length && !o.host)
|
|
436
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');
|
|
437
630
|
// The engine lowercases and alias-maps a tool name before the gate hook sees it (bash -> exec, cron ->
|
|
438
631
|
// automations); a name it would rewrite reaches the gate as a builtin, so refuse it here.
|
|
439
632
|
for (const t of o.tools ?? []) {
|
|
440
|
-
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)))
|
|
441
635
|
throw new Error(`invalid tool name: ${t.name}`);
|
|
442
636
|
}
|
|
443
637
|
this.o = o;
|
|
444
|
-
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() } : {}),
|
|
445
645
|
gateBuiltins: o.gateBuiltins !== false, spawnEngine: o.spawnEngine !== false,
|
|
446
646
|
onState: (s) => this.setState(s), onExit: () => this.closed('engine exited') });
|
|
447
647
|
this.keys = createKeys({ root: this.engine.root,
|
|
@@ -453,14 +653,46 @@ export class OpenClawKit {
|
|
|
453
653
|
request: (method, params, co) => this.request()(method, params, co),
|
|
454
654
|
bridge: { resolveAsk: (id, d) => slot.bridge?.resolveAsk(id, d) ?? false },
|
|
455
655
|
});
|
|
456
|
-
|
|
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
|
+
} } : {}),
|
|
457
682
|
permitted: o.permitted ?? (() => true), approvalTimeoutMs: o.approvalTimeoutMs ?? 180_000,
|
|
458
683
|
onAsk: (a) => this.approvalsCtl.add(a), onAskGone: (id) => this.approvalsCtl.remove(id) });
|
|
459
684
|
slot.bridge = this.bridge;
|
|
460
685
|
}
|
|
461
|
-
setState(s) {
|
|
686
|
+
setState(s) {
|
|
687
|
+
this.current = { ...s, ...(this.engine.patchSet !== undefined ? { patchSet: this.engine.patchSet } : {}) };
|
|
688
|
+
this.o.onState?.(this.current);
|
|
689
|
+
}
|
|
462
690
|
get state() { return this.current; }
|
|
463
|
-
prepare() {
|
|
691
|
+
async prepare() {
|
|
692
|
+
await this.engine.prepare();
|
|
693
|
+
if (this.engine.patchSet !== undefined)
|
|
694
|
+
this.setState(this.current);
|
|
695
|
+
}
|
|
464
696
|
start() {
|
|
465
697
|
if (this.current.phase === 'ready')
|
|
466
698
|
return Promise.resolve();
|
|
@@ -476,6 +708,7 @@ export class OpenClawKit {
|
|
|
476
708
|
async connect() {
|
|
477
709
|
let transport;
|
|
478
710
|
try {
|
|
711
|
+
await this.prepareBrowsers();
|
|
479
712
|
const ctx = await this.engine.start();
|
|
480
713
|
if (!ctx || this.stopping)
|
|
481
714
|
return;
|
|
@@ -504,18 +737,26 @@ export class OpenClawKit {
|
|
|
504
737
|
this.off.push(transport.onEvent((e) => this.approvalsCtl.handleEvent(e)));
|
|
505
738
|
this.failures = 0;
|
|
506
739
|
this.setState({ phase: 'ready' });
|
|
740
|
+
if (this.o.browser)
|
|
741
|
+
await this.checkBrowserTools();
|
|
507
742
|
// Replays the engine's native approval lists over the now-live transport (N9).
|
|
508
743
|
await this.approvalsCtl.resync();
|
|
509
744
|
}
|
|
510
745
|
catch (error) {
|
|
511
746
|
const needsUpdate = this.current.phase === 'needs-update';
|
|
747
|
+
const patchFailure = this.current.why === 'engine-patch';
|
|
512
748
|
if (transport && this.transport === transport)
|
|
513
749
|
await this.disconnect();
|
|
514
750
|
await this.engine.stop();
|
|
751
|
+
await this.closeBrowsers();
|
|
515
752
|
if (error instanceof Error && 'code' in error && error.code === 'engine-already-running') {
|
|
516
753
|
this.setState({ phase: 'failed', why: 'engine-already-running' });
|
|
517
754
|
throw error;
|
|
518
755
|
}
|
|
756
|
+
if (patchFailure) {
|
|
757
|
+
this.setState({ phase: 'failed', why: 'engine-patch' });
|
|
758
|
+
throw error;
|
|
759
|
+
}
|
|
519
760
|
if (needsUpdate)
|
|
520
761
|
this.setState({ phase: 'needs-update', why: 'version' });
|
|
521
762
|
if (!needsUpdate && !this.stopping)
|
|
@@ -529,6 +770,7 @@ export class OpenClawKit {
|
|
|
529
770
|
this.transport = undefined;
|
|
530
771
|
this.greeting = undefined;
|
|
531
772
|
this.members = undefined;
|
|
773
|
+
this.browserPolicySessions.clear();
|
|
532
774
|
this.bridge.stop();
|
|
533
775
|
await transport?.stop();
|
|
534
776
|
}
|
|
@@ -552,6 +794,20 @@ export class OpenClawKit {
|
|
|
552
794
|
this.stopping = true;
|
|
553
795
|
await this.disconnect();
|
|
554
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];
|
|
555
811
|
}
|
|
556
812
|
request() {
|
|
557
813
|
if (!this.transport || this.current.phase !== 'ready')
|
|
@@ -559,6 +815,8 @@ export class OpenClawKit {
|
|
|
559
815
|
return this.transport.request.bind(this.transport);
|
|
560
816
|
}
|
|
561
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'));
|
|
562
820
|
try {
|
|
563
821
|
return this.request()(method, params, o);
|
|
564
822
|
}
|
|
@@ -585,10 +843,28 @@ export class OpenClawKit {
|
|
|
585
843
|
this.listeners.add(listener);
|
|
586
844
|
return () => { this.listeners.delete(listener); };
|
|
587
845
|
}
|
|
588
|
-
ensureMember(member) {
|
|
846
|
+
async ensureMember(member) {
|
|
589
847
|
if (!this.members)
|
|
590
|
-
|
|
591
|
-
|
|
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;
|
|
592
868
|
}
|
|
593
869
|
// sign-in (5.7): every body here is the module's, with this kit's transport, members and ports.
|
|
594
870
|
signInCtx() {
|
|
@@ -634,7 +910,7 @@ export class OpenClawKit {
|
|
|
634
910
|
};
|
|
635
911
|
},
|
|
636
912
|
ensure: (member) => this.ensureMember(member),
|
|
637
|
-
bridge: this.bridge,
|
|
913
|
+
bridge: { register: (run, tools, runId) => this.bridge.register(run, tools && this.o.browser ? [...tools, 'request_sign_in'] : tools, runId) },
|
|
638
914
|
tools: new Set(this.toolNames()),
|
|
639
915
|
});
|
|
640
916
|
}
|
|
@@ -642,7 +918,13 @@ export class OpenClawKit {
|
|
|
642
918
|
toolNames() {
|
|
643
919
|
return (this.o.tools ?? []).map((t) => t.name);
|
|
644
920
|
}
|
|
645
|
-
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
|
+
}
|
|
646
928
|
if (spec.auth !== undefined && spec.auth !== 'apiKey')
|
|
647
929
|
return Promise.reject(new Error('Choose a supported account option.'));
|
|
648
930
|
// Snapshot before account readiness awaits; invalid schemas never reach the Gateway.
|
|
@@ -703,12 +985,37 @@ export class OpenClawKit {
|
|
|
703
985
|
const safe = reconcileConfig(response.config, { root: this.engine.root, stateDir: this.o.stateDir,
|
|
704
986
|
port: Number(readFileSync(join(this.engine.root, 'port'), 'utf8')), pluginId: this.o.plugin?.id ?? 'byokit',
|
|
705
987
|
pluginDir: join(this.engine.root, 'plugin'), policyPath: fileURLToPath(new URL('../policy/policy.mjs', import.meta.url)), app: patch,
|
|
706
|
-
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
|
+
}
|
|
707
996
|
await this.request()('config.patch', { raw: JSON.stringify(safe), baseHash: response.hash, ...(o?.agentId ? { agentId: o.agentId } : {}) });
|
|
708
997
|
}
|
|
709
998
|
memoryLimited(member) {
|
|
710
999
|
const config = JSON.parse(readFileSync(join(this.engine.root, 'openclaw.json'), 'utf8'));
|
|
711
1000
|
return configMemoryLimited(config, member);
|
|
712
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
|
+
}
|
|
713
1020
|
doctorContext() { return this.engine.doctorContext(); }
|
|
714
1021
|
}
|
package/dist/link.d.ts
CHANGED
|
@@ -1,24 +1,35 @@
|
|
|
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
|
-
|
|
8
|
+
/** `openclawLink`'s options: which member a grant acts for, the D8 pass-through predicate, sealed relay push. */
|
|
9
|
+
export type OpenClawLinkOptions = {
|
|
8
10
|
memberOf: (grant: Grant) => Member | undefined;
|
|
9
11
|
passThrough?: (method: string, grant: Grant) => boolean;
|
|
10
12
|
relay?: RelayClient;
|
|
11
|
-
}
|
|
13
|
+
};
|
|
14
|
+
/** What `openclawLink` hands the link host: its request/stream handlers plus the kit's push entry point. */
|
|
15
|
+
export type OpenClawLinkHost = Pick<HostOptions, 'handle' | 'stream' | 'allow' | 'onGrantRemoved'> & {
|
|
12
16
|
onAction(a: PushAction): Promise<unknown>;
|
|
13
17
|
};
|
|
14
|
-
export declare function
|
|
18
|
+
export declare function openclawLink(kit: OpenClawKit & {
|
|
19
|
+
browser?: BrowserHost;
|
|
20
|
+
}, o: OpenClawLinkOptions): OpenClawLinkHost;
|
|
21
|
+
/** `serve`'s options: the link host to accept on, and the ingress to publish or replace. */
|
|
22
|
+
export type OpenClawServeOptions = {
|
|
15
23
|
host: Host;
|
|
16
24
|
port: number;
|
|
17
25
|
via?: Via;
|
|
18
26
|
previous?: ServeIngress;
|
|
19
27
|
http?: (req: IncomingMessage, res: ServerResponse) => void;
|
|
20
|
-
}
|
|
28
|
+
};
|
|
29
|
+
/** The running ingress: its urls, the entry that reaches the engine, and an orderly close. */
|
|
30
|
+
export type OpenClawServeHandle = {
|
|
21
31
|
urls: string[];
|
|
22
32
|
ingress?: ServeIngress;
|
|
23
33
|
close(): Promise<void>;
|
|
24
|
-
}
|
|
34
|
+
};
|
|
35
|
+
export declare function serve(o: OpenClawServeOptions): Promise<OpenClawServeHandle>;
|