@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.
Files changed (59) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +174 -2
  3. package/dist/auth-store.js +1 -1
  4. package/dist/bridge.d.ts +10 -2
  5. package/dist/bridge.js +61 -14
  6. package/dist/browser/broker.d.ts +63 -0
  7. package/dist/browser/broker.js +990 -0
  8. package/dist/browser/host.d.ts +98 -0
  9. package/dist/browser/host.js +945 -0
  10. package/dist/browser/resume.d.ts +10 -0
  11. package/dist/browser/resume.js +23 -0
  12. package/dist/browser/store.d.ts +25 -0
  13. package/dist/browser/store.js +151 -0
  14. package/dist/browser/verify.d.ts +17 -0
  15. package/dist/browser/verify.js +38 -0
  16. package/dist/browser.d.ts +212 -0
  17. package/dist/browser.js +4 -0
  18. package/dist/config.d.ts +28 -0
  19. package/dist/config.js +130 -0
  20. package/dist/day-usage.d.ts +76 -0
  21. package/dist/day-usage.js +104 -0
  22. package/dist/device.d.ts +32 -2
  23. package/dist/device.js +224 -4
  24. package/dist/engine-patches.d.ts +36 -0
  25. package/dist/engine-patches.js +320 -0
  26. package/dist/engine-status.d.ts +13 -0
  27. package/dist/engine-status.js +31 -0
  28. package/dist/engine.d.ts +13 -1
  29. package/dist/engine.js +99 -52
  30. package/dist/index.d.ts +6 -1
  31. package/dist/index.js +2 -0
  32. package/dist/kit.d.ts +29 -3
  33. package/dist/kit.js +324 -13
  34. package/dist/link.d.ts +5 -2
  35. package/dist/link.js +333 -4
  36. package/dist/routes.d.ts +19 -8
  37. package/dist/routes.js +44 -11
  38. package/dist/routes.json +3775 -356
  39. package/dist/runs.js +8 -2
  40. package/dist/signin.d.ts +2 -1
  41. package/dist/signin.js +103 -7
  42. package/dist/testing/browser.d.ts +50 -0
  43. package/dist/testing/browser.js +166 -0
  44. package/dist/testing/index.d.ts +1 -0
  45. package/dist/testing/index.js +1 -0
  46. package/dist/testing/model-stub.d.ts +1 -0
  47. package/dist/testing/model-stub.js +3 -1
  48. package/dist/types.d.ts +50 -3
  49. package/dist/usage-boots.d.ts +8 -0
  50. package/dist/usage-boots.js +26 -0
  51. package/dist/usage.d.ts +51 -0
  52. package/dist/usage.js +61 -0
  53. package/dist/words.json +42 -0
  54. package/engine/OPENCLAW-LICENSE +24 -0
  55. package/engine/patches.json +59 -0
  56. package/engine/patches.schema.json +41 -0
  57. package/package.json +12 -4
  58. package/plugin/index.js +78 -5
  59. 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
- this.bridge = new Bridge({ path: this.engine.bridgeSock, host: o.host, tools: new Set((o.tools ?? []).map((t) => t.name)),
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) { this.current = s; this.o.onState?.(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() { return this.engine.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
- return Promise.reject(new Error('gateway not ready'));
588
- return this.members.ensure(member);
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, o: {
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: {