@corenel/harness 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/approvals/service.d.ts.map +1 -1
  2. package/dist/approvals/service.js +4 -1
  3. package/dist/approvals/service.js.map +1 -1
  4. package/dist/audit/log.d.ts.map +1 -1
  5. package/dist/audit/log.js +12 -1
  6. package/dist/audit/log.js.map +1 -1
  7. package/dist/audit/recorder.d.ts +7 -1
  8. package/dist/audit/recorder.d.ts.map +1 -1
  9. package/dist/audit/recorder.js +28 -4
  10. package/dist/audit/recorder.js.map +1 -1
  11. package/dist/audit/runAudit.d.ts +2 -1
  12. package/dist/audit/runAudit.d.ts.map +1 -1
  13. package/dist/audit/runAudit.js +2 -0
  14. package/dist/audit/runAudit.js.map +1 -1
  15. package/dist/core/dontsRecent.d.ts +5 -0
  16. package/dist/core/dontsRecent.d.ts.map +1 -0
  17. package/dist/core/dontsRecent.js +23 -0
  18. package/dist/core/dontsRecent.js.map +1 -0
  19. package/dist/core/loop.d.ts +7 -0
  20. package/dist/core/loop.d.ts.map +1 -1
  21. package/dist/core/loop.js +134 -5
  22. package/dist/core/loop.js.map +1 -1
  23. package/dist/core/spawn.d.ts +13 -1
  24. package/dist/core/spawn.d.ts.map +1 -1
  25. package/dist/core/spawn.js +20 -3
  26. package/dist/core/spawn.js.map +1 -1
  27. package/dist/dag/executeNode.d.ts +3 -1
  28. package/dist/dag/executeNode.d.ts.map +1 -1
  29. package/dist/dag/executeNode.js +1 -0
  30. package/dist/dag/executeNode.js.map +1 -1
  31. package/dist/guardrail/permission-service.d.ts.map +1 -1
  32. package/dist/guardrail/permission-service.js +21 -10
  33. package/dist/guardrail/permission-service.js.map +1 -1
  34. package/dist/guardrail/serialize.d.ts.map +1 -1
  35. package/dist/guardrail/serialize.js +5 -0
  36. package/dist/guardrail/serialize.js.map +1 -1
  37. package/dist/guardrail/validate.d.ts.map +1 -1
  38. package/dist/guardrail/validate.js +21 -6
  39. package/dist/guardrail/validate.js.map +1 -1
  40. package/dist/memory/store.d.ts +3 -0
  41. package/dist/memory/store.d.ts.map +1 -1
  42. package/dist/memory/store.js +16 -1
  43. package/dist/memory/store.js.map +1 -1
  44. package/dist/orchestrator/record.d.ts +7 -0
  45. package/dist/orchestrator/record.d.ts.map +1 -1
  46. package/dist/orchestrator/record.js +14 -2
  47. package/dist/orchestrator/record.js.map +1 -1
  48. package/dist/prompts/library/defaults.generated.d.ts +2 -0
  49. package/dist/prompts/library/defaults.generated.d.ts.map +1 -1
  50. package/dist/prompts/library/defaults.generated.js +6 -0
  51. package/dist/prompts/library/defaults.generated.js.map +1 -1
  52. package/dist/prompts/library/index.d.ts +3 -0
  53. package/dist/prompts/library/index.d.ts.map +1 -1
  54. package/dist/prompts/library/index.js +3 -0
  55. package/dist/prompts/library/index.js.map +1 -1
  56. package/dist/sessions/fileSessionStore.d.ts +1 -0
  57. package/dist/sessions/fileSessionStore.d.ts.map +1 -1
  58. package/dist/sessions/fileSessionStore.js +11 -1
  59. package/dist/sessions/fileSessionStore.js.map +1 -1
  60. package/dist/sidecar/accessTestRig.d.ts +32 -0
  61. package/dist/sidecar/accessTestRig.d.ts.map +1 -0
  62. package/dist/sidecar/accessTestRig.js +107 -0
  63. package/dist/sidecar/accessTestRig.js.map +1 -0
  64. package/dist/sidecar/askView.d.ts +20 -0
  65. package/dist/sidecar/askView.d.ts.map +1 -0
  66. package/dist/sidecar/askView.js +38 -0
  67. package/dist/sidecar/askView.js.map +1 -0
  68. package/dist/sidecar/host.d.ts +146 -11
  69. package/dist/sidecar/host.d.ts.map +1 -1
  70. package/dist/sidecar/host.js +406 -13
  71. package/dist/sidecar/host.js.map +1 -1
  72. package/dist/sidecar/hostCapability.d.ts +6 -0
  73. package/dist/sidecar/hostCapability.d.ts.map +1 -0
  74. package/dist/sidecar/hostCapability.js +7 -0
  75. package/dist/sidecar/hostCapability.js.map +1 -0
  76. package/dist/sidecar/identity.d.ts +5 -0
  77. package/dist/sidecar/identity.d.ts.map +1 -1
  78. package/dist/sidecar/memberGate.d.ts +5 -0
  79. package/dist/sidecar/memberGate.d.ts.map +1 -0
  80. package/dist/sidecar/memberGate.js +128 -0
  81. package/dist/sidecar/memberGate.js.map +1 -0
  82. package/dist/sidecar/server.d.ts +425 -11
  83. package/dist/sidecar/server.d.ts.map +1 -1
  84. package/dist/sidecar/server.js +1809 -52
  85. package/dist/sidecar/server.js.map +1 -1
  86. package/dist/skills.d.ts +7 -1
  87. package/dist/skills.d.ts.map +1 -1
  88. package/dist/skills.js +18 -5
  89. package/dist/skills.js.map +1 -1
  90. package/dist/tools/builtins.d.ts +17 -0
  91. package/dist/tools/builtins.d.ts.map +1 -1
  92. package/dist/tools/builtins.js +32 -3
  93. package/dist/tools/builtins.js.map +1 -1
  94. package/dist/tools/callAgent.d.ts +14 -1
  95. package/dist/tools/callAgent.d.ts.map +1 -1
  96. package/dist/tools/callAgent.js +18 -2
  97. package/dist/tools/callAgent.js.map +1 -1
  98. package/dist/tools/createCrewAgent.d.ts +39 -0
  99. package/dist/tools/createCrewAgent.d.ts.map +1 -0
  100. package/dist/tools/createCrewAgent.js +121 -0
  101. package/dist/tools/createCrewAgent.js.map +1 -0
  102. package/package.json +2 -2
@@ -1,5 +1,6 @@
1
1
  import { asEcPublicJwk, jwkThumbprint, verifySignature, randomToken, serverProofPayload, clientProofPayload, parseEnrollmentCode, bytesToB64url, codeProof, codeHash, } from './deviceKeys.js';
2
- import { SIDECAR_PROTOCOL, HOST_METHODS, errMessage, RUN_TASK_FEATURE, RUN_PLAN_FEATURE, CREW_DEPLOY_FEATURE, DEVICES_FEATURE, isClerkOrgId, } from '@corenel/protocol';
2
+ import { capabilityMissingMessage } from './hostCapability.js';
3
+ import { SIDECAR_PROTOCOL, HOST_METHODS, errMessage, RUN_TASK_FEATURE, RUN_PLAN_FEATURE, CREW_DEPLOY_FEATURE, DEVICES_FEATURE, AGENT_ACCESS_FEATURE, AGENT_RUNS_FEATURE, PEER_LINKS_FEATURE, CHILD_EVENTS_FEATURE, PEER_DAEMON_FEATURE, TEAM_MEMBERS_FEATURE, SPEECH_FEATURE, encodePcm16, isClerkOrgId, } from '@corenel/protocol';
3
4
  /** Decode a base64 chunk to bytes (atob is available in the browser and Node 18+). */
4
5
  function b64ToBytes(b64) {
5
6
  const bin = atob(b64);
@@ -32,6 +33,26 @@ export function isSidecarKeyMismatch(e) {
32
33
  * Deliberately a different prefix from 'sidecar denied connection', so no
33
34
  * caller that forgets a device on a denial can mistake one for the other. */
34
35
  export const SIDECAR_NOT_ADMITTED = 'sidecar did not admit this account';
36
+ /** Prefix of the error `connect()` rejects with when the daemon sent
37
+ * `denied` before any identity challenge: its reason follows. */
38
+ export const SIDECAR_DENIED = 'sidecar denied connection';
39
+ /** The daemon itself refused the handshake (`denied`, either kind), so the
40
+ * words after the prefix are that daemon's own answer, as opposed to a
41
+ * transport failure (DNS, TLS, refused, timeout) on this side. */
42
+ export function isSidecarDenial(e) {
43
+ const msg = e instanceof Error ? e.message : String(e);
44
+ return msg.startsWith(`${SIDECAR_DENIED}: `) || msg.startsWith(`${SIDECAR_NOT_ADMITTED}: `);
45
+ }
46
+ /** Prefix of the error a DEVICE dial rejects with when the daemon sent
47
+ * `denied` before proving the key this client pinned. A real daemon proves
48
+ * its key before it judges a device, so this is not its verdict on the
49
+ * device, and nothing may be forgotten because of it. Deliberately without
50
+ * the word "denied", so no caller that forgets a device on a denial matches it. */
51
+ export const SIDECAR_UNPROVEN_DENIAL = 'sidecar refused before proving its key';
52
+ export function isSidecarUnprovenDenial(e) {
53
+ const msg = e instanceof Error ? e.message : String(e);
54
+ return msg.startsWith(SIDECAR_UNPROVEN_DENIAL);
55
+ }
35
56
  export function isSidecarAccountRefusal(e) {
36
57
  const msg = e instanceof Error ? e.message : String(e);
37
58
  return msg.startsWith(SIDECAR_NOT_ADMITTED);
@@ -215,6 +236,66 @@ export class SidecarHost {
215
236
  writable: true,
216
237
  value: new Map()
217
238
  });
239
+ Object.defineProperty(this, "accessSeq", {
240
+ enumerable: true,
241
+ configurable: true,
242
+ writable: true,
243
+ value: 0
244
+ });
245
+ Object.defineProperty(this, "accessPending", {
246
+ enumerable: true,
247
+ configurable: true,
248
+ writable: true,
249
+ value: new Map()
250
+ });
251
+ Object.defineProperty(this, "memberSeq", {
252
+ enumerable: true,
253
+ configurable: true,
254
+ writable: true,
255
+ value: 0
256
+ });
257
+ Object.defineProperty(this, "memberReplies", {
258
+ enumerable: true,
259
+ configurable: true,
260
+ writable: true,
261
+ value: new Map()
262
+ });
263
+ Object.defineProperty(this, "overviewReplies", {
264
+ enumerable: true,
265
+ configurable: true,
266
+ writable: true,
267
+ value: new Map()
268
+ });
269
+ Object.defineProperty(this, "agentCredReplies", {
270
+ enumerable: true,
271
+ configurable: true,
272
+ writable: true,
273
+ value: new Map()
274
+ });
275
+ Object.defineProperty(this, "agentCredStatusReplies", {
276
+ enumerable: true,
277
+ configurable: true,
278
+ writable: true,
279
+ value: new Map()
280
+ });
281
+ Object.defineProperty(this, "teamSurfaceSeq", {
282
+ enumerable: true,
283
+ configurable: true,
284
+ writable: true,
285
+ value: 0
286
+ });
287
+ Object.defineProperty(this, "agentRunsReplies", {
288
+ enumerable: true,
289
+ configurable: true,
290
+ writable: true,
291
+ value: new Map()
292
+ });
293
+ Object.defineProperty(this, "peerReplies", {
294
+ enumerable: true,
295
+ configurable: true,
296
+ writable: true,
297
+ value: new Map()
298
+ });
218
299
  Object.defineProperty(this, "approvalsSeq", {
219
300
  enumerable: true,
220
301
  configurable: true,
@@ -376,6 +457,18 @@ export class SidecarHost {
376
457
  writable: true,
377
458
  value: []
378
459
  });
460
+ Object.defineProperty(this, "sttSeq", {
461
+ enumerable: true,
462
+ configurable: true,
463
+ writable: true,
464
+ value: 0
465
+ });
466
+ Object.defineProperty(this, "sttStreams", {
467
+ enumerable: true,
468
+ configurable: true,
469
+ writable: true,
470
+ value: new Map()
471
+ });
379
472
  Object.defineProperty(this, "vapidKey", {
380
473
  enumerable: true,
381
474
  configurable: true,
@@ -484,6 +577,15 @@ export class SidecarHost {
484
577
  writable: true,
485
578
  value: false
486
579
  });
580
+ /** Bumped whenever the proven state is reset (each `connect`, each
581
+ * `disconnect`), so a server-proof still being verified for an EARLIER
582
+ * connection cannot mark the current one proven. */
583
+ Object.defineProperty(this, "proofEpoch", {
584
+ enumerable: true,
585
+ configurable: true,
586
+ writable: true,
587
+ value: 0
588
+ });
487
589
  /** The credential failed before sending (a malformed code, no key). */
488
590
  Object.defineProperty(this, "credentialError", {
489
591
  enumerable: true,
@@ -522,11 +624,29 @@ export class SidecarHost {
522
624
  writable: true,
523
625
  value: null
524
626
  });
627
+ /* A peer daemon serves the team daemon its MCP servers and nothing else
628
+ * (spec 2026-10-01 Part D): handing it any other store is a wiring
629
+ * mistake, refused here rather than silently withheld later. */
630
+ if (opts.peerDaemon) {
631
+ const extra = Object.keys(opts.providers)
632
+ .filter((k) => k !== 'mcp' && opts.providers[k] !== undefined);
633
+ if (extra.length)
634
+ throw new Error(`a peer daemon serves only mcp, not ${extra.join(', ')}`);
635
+ }
525
636
  this.opts = opts;
526
637
  this.ch = opts.channel;
527
638
  this.device = opts.device;
528
639
  this.ch.onMessage((m) => this.onMessage(m));
529
640
  }
641
+ /** Forget everything this host proved about a daemon: a new or dropped
642
+ * connection starts unproven, and must prove the pinned key again. */
643
+ resetProof() {
644
+ this.proofEpoch++;
645
+ this.expectProof = false;
646
+ this.verifiedSidecarJkt = null;
647
+ this.provenSidecarId = null;
648
+ this.challengedInHandshake = false;
649
+ }
530
650
  /** The hello's credential: the device if this client has one, else the
531
651
  * enrollment code. Exactly one is sent, and no secret. Sets up what the
532
652
  * proof exchange will be bound to. Without either (or without a key) there
@@ -564,6 +684,9 @@ export class SidecarHost {
564
684
  deviceCredential() { return this.device; }
565
685
  /** The sidecar key this connection verified, or null before it has. */
566
686
  sidecarKeyJkt() { return this.verifiedSidecarJkt; }
687
+ /** The id the daemon proved itself as (its server-proof), or null before
688
+ * it has. A peer daemon keys its per-team-daemon allowlist by it. */
689
+ sidecarId() { return this.provenSidecarId; }
567
690
  /** The daemon's `server-proof`: check it is the PINNED key and that it
568
691
  * signed THIS exchange, then prove this client's key. On any
569
692
  * failure nothing more is sent: the connection is dropped and `connect()`
@@ -573,6 +696,7 @@ export class SidecarHost {
573
696
  if (!this.expectProof || !key || this.verifiedSidecarJkt !== null)
574
697
  return;
575
698
  this.expectProof = false; // one exchange per connection
699
+ const epoch = this.proofEpoch;
576
700
  const sidecarKey = asEcPublicJwk(m.publicKey);
577
701
  const sidecarJkt = sidecarKey ? await jwkThumbprint(sidecarKey) : null;
578
702
  const ok = !!sidecarKey && !!sidecarJkt
@@ -581,6 +705,10 @@ export class SidecarHost {
581
705
  && await verifySignature(sidecarKey, serverProofPayload({
582
706
  clientNonce: this.clientNonce, serverNonce: m.serverNonce, sidecarId: m.sidecarId, sidecarJkt, subject: this.proofSubject,
583
707
  }), m.sig);
708
+ // The connection this proof belonged to was dropped or replaced while it
709
+ // was being verified: it proves nothing about the current one.
710
+ if (epoch !== this.proofEpoch)
711
+ return;
584
712
  if (!ok || !sidecarJkt) {
585
713
  this.failKeyMismatch();
586
714
  return;
@@ -627,6 +755,7 @@ export class SidecarHost {
627
755
  pending?.reject(new Error(`sidecar handshake timed out after ${timeoutMs}ms waiting for welcome`));
628
756
  }, timeoutMs);
629
757
  }
758
+ this.resetProof();
630
759
  const credential = this.credentialFields();
631
760
  if (this.credentialError) {
632
761
  const pending = this.welcome;
@@ -637,15 +766,22 @@ export class SidecarHost {
637
766
  this.ch.send({
638
767
  t: 'hello', protocol: SIDECAR_PROTOCOL, ...credential,
639
768
  ...(this.opts.stateDir ? { stateDir: this.opts.stateDir } : {}),
640
- ...(this.opts.clientId ? { clientId: this.opts.clientId } : {}),
641
- ...(this.opts.clientName ? { clientName: this.opts.clientName } : {}),
769
+ // A peer daemon is not a person's app: it has no client identity to
770
+ // record, and only it says what it serves.
771
+ ...(!this.opts.peerDaemon && this.opts.clientId ? { clientId: this.opts.clientId } : {}),
772
+ ...(!this.opts.peerDaemon && this.opts.clientName ? { clientName: this.opts.clientName } : {}),
642
773
  ...(this.opts.accounts?.length ? { accounts: this.opts.accounts } : {}),
774
+ ...(this.opts.peerDaemon && this.opts.provides ? { provides: this.opts.provides } : {}),
643
775
  // What THIS client understands, so a daemon can tell an older app
644
776
  // (whose agents sync but never fire until deployed) from this one.
645
777
  // DEVICES_FEATURE only when the caller has somewhere to KEEP an issued
646
778
  // device credential: a daemon issues one only to a client that says
647
779
  // so, since one thrown away is a ghost device on the daemon for ever.
648
- capabilities: [CREW_DEPLOY_FEATURE, ...(this.opts.onDeviceIssued && this.opts.deviceKey ? [DEVICES_FEATURE] : [])],
780
+ capabilities: [
781
+ CREW_DEPLOY_FEATURE,
782
+ ...(this.opts.onDeviceIssued && this.opts.deviceKey ? [DEVICES_FEATURE] : []),
783
+ ...(this.opts.peerDaemon ? [PEER_DAEMON_FEATURE] : []),
784
+ ],
649
785
  });
650
786
  });
651
787
  }
@@ -671,6 +807,7 @@ export class SidecarHost {
671
807
  }
672
808
  /** Drop the connection: unregister imported tools and fail anything in flight. */
673
809
  disconnect(reason = 'disconnected') {
810
+ this.resetProof();
674
811
  for (const name of this.imported)
675
812
  this.opts.unregisterTool?.(name);
676
813
  this.imported.length = 0;
@@ -730,6 +867,20 @@ export class SidecarHost {
730
867
  for (const p of this.crewDeployPending.values())
731
868
  p.reject(new Error(reason));
732
869
  this.crewDeployPending.clear();
870
+ for (const p of this.accessPending.values())
871
+ p.reject(new Error(reason));
872
+ this.accessPending.clear();
873
+ for (const map of [this.memberReplies, this.overviewReplies, this.agentCredReplies, this.agentCredStatusReplies]) {
874
+ for (const p of map.values())
875
+ p.reject(new Error(reason));
876
+ map.clear();
877
+ }
878
+ for (const p of this.agentRunsReplies.values())
879
+ p.reject(new Error(reason));
880
+ this.agentRunsReplies.clear();
881
+ for (const p of this.peerReplies.values())
882
+ p.reject(new Error(reason));
883
+ this.peerReplies.clear();
733
884
  for (const p of this.approvalsPending.values())
734
885
  p.reject(new Error(reason));
735
886
  this.approvalsPending.clear();
@@ -792,6 +943,9 @@ export class SidecarHost {
792
943
  this.mcpLoginUrlWaiters.clear();
793
944
  this.welcome?.reject(new Error(reason));
794
945
  this.welcome = null;
946
+ for (const s of this.sttStreams.values())
947
+ s.onClosed(reason);
948
+ this.sttStreams.clear();
795
949
  this.ch.close();
796
950
  }
797
951
  onMessage(m) {
@@ -850,7 +1004,9 @@ export class SidecarHost {
850
1004
  case 'denied': {
851
1005
  this.welcome?.reject(new Error(this.challengedInHandshake
852
1006
  ? `${SIDECAR_NOT_ADMITTED}: ${m.reason}`
853
- : `sidecar denied connection: ${m.reason}`));
1007
+ : this.mode === 'device' && this.verifiedSidecarJkt === null
1008
+ ? `${SIDECAR_UNPROVEN_DENIAL}: ${m.reason}`
1009
+ : `${SIDECAR_DENIED}: ${m.reason}`));
854
1010
  this.welcome = null;
855
1011
  return;
856
1012
  }
@@ -975,6 +1131,10 @@ export class SidecarHost {
975
1131
  this.runPending.get(m.id)?.onEvent(m.event);
976
1132
  return;
977
1133
  }
1134
+ case 'child-event': {
1135
+ this.runPending.get(m.id)?.onChild?.(m.event);
1136
+ return;
1137
+ }
978
1138
  case 'run-done': {
979
1139
  const p = this.runPending.get(m.id);
980
1140
  if (!p)
@@ -1047,6 +1207,15 @@ export class SidecarHost {
1047
1207
  this.opts.onOpenUrl?.({ url: m.url, title: m.title, note: m.note, meta: m.meta });
1048
1208
  return;
1049
1209
  }
1210
+ case 'stt': {
1211
+ const s = this.sttStreams.get(m.sid);
1212
+ if (!s)
1213
+ return;
1214
+ if (m.msg.kind === 'final' || m.msg.kind === 'error')
1215
+ this.sttStreams.delete(m.sid);
1216
+ s.onMessage(m.msg);
1217
+ return;
1218
+ }
1050
1219
  case 'proc-started': {
1051
1220
  const p = this.procPending.get(m.id);
1052
1221
  if (!p)
@@ -1333,6 +1502,81 @@ export class SidecarHost {
1333
1502
  p.resolve(m.reply);
1334
1503
  return;
1335
1504
  }
1505
+ case 'member-result': {
1506
+ const p = this.memberReplies.get(m.id);
1507
+ if (!p)
1508
+ return;
1509
+ this.memberReplies.delete(m.id);
1510
+ p.resolve(m.ok
1511
+ ? {
1512
+ ok: true,
1513
+ ...(m.status ? { status: m.status } : {}),
1514
+ ...(m.daemonName !== undefined ? { daemonName: m.daemonName } : {}),
1515
+ ...(m.adminLabel !== undefined ? { adminLabel: m.adminLabel } : {}),
1516
+ ...(m.adminId !== undefined ? { adminId: m.adminId } : {}),
1517
+ ...(m.admin !== undefined ? { admin: m.admin } : {}),
1518
+ ...(m.members ? { members: m.members } : {}),
1519
+ }
1520
+ : { ok: false, code: m.code, error: m.error });
1521
+ return;
1522
+ }
1523
+ case 'member-status-changed':
1524
+ if (m.status === 'active')
1525
+ this.opts.onMemberStatusChanged?.('active');
1526
+ return;
1527
+ case 'access-overview-result': {
1528
+ const p = this.overviewReplies.get(m.id);
1529
+ if (!p)
1530
+ return;
1531
+ this.overviewReplies.delete(m.id);
1532
+ // An older daemon sends no inactiveDeployments: read as none.
1533
+ p.resolve(m.ok
1534
+ ? { ok: true, rows: m.rows, inactiveDeployments: Array.isArray(m.inactiveDeployments) ? m.inactiveDeployments : [] }
1535
+ : { ok: false, code: m.code, error: m.error });
1536
+ return;
1537
+ }
1538
+ case 'agent-cred-result': {
1539
+ const p = this.agentCredReplies.get(m.id);
1540
+ if (!p)
1541
+ return;
1542
+ this.agentCredReplies.delete(m.id);
1543
+ p.resolve(m.ok ? { ok: true } : { ok: false, code: m.code, error: m.error, ...(m.reason ? { reason: m.reason } : {}) });
1544
+ return;
1545
+ }
1546
+ case 'agent-runs-result': {
1547
+ const p = this.agentRunsReplies.get(m.id);
1548
+ if (!p)
1549
+ return;
1550
+ this.agentRunsReplies.delete(m.id);
1551
+ p.resolve(m.ok ? { ok: true, runs: m.runs } : { ok: false, code: m.code, error: m.error });
1552
+ return;
1553
+ }
1554
+ case 'peer-result': {
1555
+ const p = this.peerReplies.get(m.id);
1556
+ if (!p)
1557
+ return;
1558
+ this.peerReplies.delete(m.id);
1559
+ p.resolve(m.ok ? { ok: true, ...(m.peers ? { peers: m.peers } : {}) } : { ok: false, code: m.code, error: m.error });
1560
+ return;
1561
+ }
1562
+ case 'agent-cred-status-result': {
1563
+ const p = this.agentCredStatusReplies.get(m.id);
1564
+ if (!p)
1565
+ return;
1566
+ this.agentCredStatusReplies.delete(m.id);
1567
+ p.resolve(m.ok ? { ok: true, servers: m.servers } : { ok: false, code: m.code, error: m.error });
1568
+ return;
1569
+ }
1570
+ case 'access-result': {
1571
+ const p = this.accessPending.get(m.id);
1572
+ if (!p)
1573
+ return;
1574
+ this.accessPending.delete(m.id);
1575
+ p.resolve(m.ok
1576
+ ? { ok: true, ...(m.access ? { access: m.access } : {}), ...(m.members ? { members: m.members } : {}) }
1577
+ : { ok: false, code: m.code, error: m.error });
1578
+ return;
1579
+ }
1336
1580
  case 'checks-get-result':
1337
1581
  case 'checks-pin-result': {
1338
1582
  // One map for both: each answers with the complete state.
@@ -1449,6 +1693,25 @@ export class SidecarHost {
1449
1693
  * allowed shell. A client MUST check this before offering the surface: a
1450
1694
  * daemon without it answers every proc message with an error. */
1451
1695
  supportsProcs() { return this.features.includes('procs'); }
1696
+ /** True when the daemon transcribes dictation on its own machine. */
1697
+ supportsSpeech() { return this.features.includes(SPEECH_FEATURE); }
1698
+ /** One dictation stream to the daemon's speech model. Replies arrive on
1699
+ * `onMessage` until a final or an error; a dropped connection calls
1700
+ * `onClosed`, which the browser engine turns into a crash so the mic is
1701
+ * released. */
1702
+ openSpeechStream(onMessage, onClosed) {
1703
+ const sid = `s${++this.sttSeq}${Math.random().toString(36).slice(2, 8)}`;
1704
+ this.sttStreams.set(sid, { onMessage, onClosed });
1705
+ this.ch.send({ t: 'stt-start', sid });
1706
+ return {
1707
+ audio: (pcm) => { if (this.sttStreams.has(sid))
1708
+ this.ch.send({ t: 'stt-audio', sid, pcm: encodePcm16(pcm) }); },
1709
+ flush: () => { if (this.sttStreams.has(sid))
1710
+ this.ch.send({ t: 'stt-flush', sid }); },
1711
+ cancel: () => { if (this.sttStreams.delete(sid))
1712
+ this.ch.send({ t: 'stt-cancel', sid }); },
1713
+ };
1714
+ }
1452
1715
  /** Start a long-running command on the sidecar and stream its output.
1453
1716
  *
1454
1717
  * This is the counterpart to the one-shot `sidecar_shell_exec` tool, not a
@@ -1757,7 +2020,7 @@ export class SidecarHost {
1757
2020
  /** WATCH a run this client did not start (a trigger-fired crew run). Rides
1758
2021
  * the same pending map as `run`, so `event`/`run-done`/`run-error` route here
1759
2022
  * unchanged; the server replays what already happened first. */
1760
- watchRun(runId, onEvent) {
2023
+ watchRun(runId, onEvent, onChild) {
1761
2024
  const id = ++this.runSeq;
1762
2025
  let rejectDone;
1763
2026
  const done = new Promise((resolve, reject) => {
@@ -1766,6 +2029,7 @@ export class SidecarHost {
1766
2029
  resolve: (r) => resolve({ text: r.text, stopReason: r.stopReason }),
1767
2030
  reject,
1768
2031
  onEvent,
2032
+ ...(onChild ? { onChild } : {}),
1769
2033
  });
1770
2034
  this.ch.send({ t: 'subscribe', id, runId });
1771
2035
  });
@@ -1790,6 +2054,10 @@ export class SidecarHost {
1790
2054
  }
1791
2055
  /** True when the connected sidecar's runner supports cancelling a run. */
1792
2056
  supportsRunCancel() { return this.features.includes('run-cancel'); }
2057
+ /** True when the connected daemon fans a delegated callee's own node events
2058
+ * out to a watched run's `onChild` (running pills). An older daemon simply
2059
+ * never sends `child-event`, so a watcher's card shows no callee. */
2060
+ supportsChildEvents() { return this.features.includes(CHILD_EVENTS_FEATURE); }
1793
2061
  /** True when the connected sidecar understands `run`'s `task`/`autonomy`
1794
2062
  * fields. An older sidecar without this feature would silently ignore
1795
2063
  * them, so callers must check this before sending either — never send and
@@ -2113,6 +2381,126 @@ export class SidecarHost {
2113
2381
  crewResume(ref, reason) { return this.crewDeployRequest((id) => ({ t: 'crew-resume', id, ref, ...(reason ? { reason } : {}) })); }
2114
2382
  /** Stop a run AND record who stopped it and why (the plain `cancelRun` records nothing). */
2115
2383
  crewStop(ref, runId, reason) { return this.crewDeployRequest((id) => ({ t: 'crew-stop', id, ref, runId, reason })); }
2384
+ /** True when the connected daemon keeps per-agent access (team mode, Part B). */
2385
+ supportsAgentAccess() { return this.features.includes(AGENT_ACCESS_FEATURE); }
2386
+ /** One access-* request. `ok: false` is the daemon's considered answer and
2387
+ * resolves; only a dropped connection rejects. Nothing is sent to a daemon
2388
+ * without the feature. */
2389
+ accessRequest(build) {
2390
+ if (!this.supportsAgentAccess())
2391
+ return Promise.resolve({ ok: false, code: 'NOT_TEAM_AGENT', error: 'this daemon does not keep agent access' });
2392
+ const id = ++this.accessSeq;
2393
+ return new Promise((resolve, reject) => {
2394
+ this.accessPending.set(id, { resolve, reject });
2395
+ this.ch.send(build(id));
2396
+ });
2397
+ }
2398
+ accessGet(ref) { return this.accessRequest((id) => ({ t: 'access-get', id, ref })); }
2399
+ accessGrant(ref, userId, role) {
2400
+ return this.accessRequest((id) => ({ t: 'access-grant', id, ref, userId, role }));
2401
+ }
2402
+ accessRevoke(ref, userId) { return this.accessRequest((id) => ({ t: 'access-revoke', id, ref, userId })); }
2403
+ accessTransfer(ref, userId) { return this.accessRequest((id) => ({ t: 'access-transfer', id, ref, userId })); }
2404
+ accessMembers() { return this.accessRequest((id) => ({ t: 'access-members', id })); }
2405
+ /** True when the connected daemon is in team mode with a member list (Part E). */
2406
+ supportsTeamMembers() { return this.features.includes(TEAM_MEMBERS_FEATURE); }
2407
+ /** One member-* request. `ok: false` is the daemon's answer and resolves;
2408
+ * only a dropped connection rejects. Nothing is sent without the feature. */
2409
+ memberRequest(build) {
2410
+ if (!this.supportsTeamMembers())
2411
+ return Promise.resolve({ ok: false, code: 'UNAVAILABLE', error: 'this daemon keeps no member list' });
2412
+ const id = ++this.memberSeq;
2413
+ return new Promise((resolve, reject) => {
2414
+ this.memberReplies.set(id, { resolve, reject });
2415
+ this.ch.send(build(id));
2416
+ });
2417
+ }
2418
+ memberStatus() { return this.memberRequest((id) => ({ t: 'member-status', id })); }
2419
+ membersList() { return this.memberRequest((id) => ({ t: 'members-list', id })); }
2420
+ memberApprove(userId) { return this.memberRequest((id) => ({ t: 'member-approve', id, userId })); }
2421
+ memberRemove(userId) { return this.memberRequest((id) => ({ t: 'member-remove', id, userId })); }
2422
+ /** Every team agent this user may see, scoped to their role (Part E, E2). */
2423
+ accessOverview() {
2424
+ if (!this.supportsAgentAccess())
2425
+ return Promise.resolve({ ok: false, code: 'NOT_TEAM_AGENT', error: 'this daemon does not keep agent access' });
2426
+ const id = ++this.memberSeq;
2427
+ return new Promise((resolve, reject) => {
2428
+ this.overviewReplies.set(id, { resolve, reject });
2429
+ this.ch.send({ t: 'access-overview', id });
2430
+ });
2431
+ }
2432
+ /** Each MCP server's binding and credential presence for one agent (owner or admin). */
2433
+ agentCredStatus(ref) {
2434
+ if (!this.supportsAgentAccess())
2435
+ return Promise.resolve({ ok: false, code: 'UNAVAILABLE', error: 'this daemon does not manage agent credentials' });
2436
+ const id = ++this.memberSeq;
2437
+ return new Promise((resolve, reject) => {
2438
+ this.agentCredStatusReplies.set(id, { resolve, reject });
2439
+ this.ch.send({ t: 'agent-cred-status', id, ref });
2440
+ });
2441
+ }
2442
+ /** The connected daemon keeps team run history (spec 2026-10-01 E5.6). */
2443
+ supportsAgentRuns() { return this.features.includes(AGENT_RUNS_FEATURE); }
2444
+ /** Every member's runs of a team agent, newest first, no content. A
2445
+ * refusal resolves; only a dropped connection rejects. */
2446
+ agentRuns(ref, opts = {}) {
2447
+ if (!this.supportsAgentRuns())
2448
+ return Promise.resolve({ ok: false, code: 'NOT_TEAM_AGENT', error: 'this daemon keeps no team run history' });
2449
+ const id = ++this.teamSurfaceSeq;
2450
+ return new Promise((resolve, reject) => {
2451
+ this.agentRunsReplies.set(id, { resolve, reject });
2452
+ this.ch.send({
2453
+ t: 'agent-runs', id, ref,
2454
+ ...(opts.limit !== undefined ? { limit: opts.limit } : {}),
2455
+ ...(opts.before !== undefined ? { before: opts.before } : {}),
2456
+ });
2457
+ });
2458
+ }
2459
+ /** The connected daemon links to team daemons from here (spec 2026-10-01 E5.5). */
2460
+ supportsPeerLinks() { return this.features.includes(PEER_LINKS_FEATURE); }
2461
+ peerRequest(build) {
2462
+ if (!this.supportsPeerLinks())
2463
+ return Promise.resolve({ ok: false, code: 'UNAVAILABLE', error: 'this daemon does not link to team daemons' });
2464
+ const id = ++this.teamSurfaceSeq;
2465
+ return new Promise((resolve, reject) => {
2466
+ this.peerReplies.set(id, { resolve, reject });
2467
+ this.ch.send(build(id));
2468
+ });
2469
+ }
2470
+ peerList() { return this.peerRequest((id) => ({ t: 'peer-list', id })); }
2471
+ peerAdd(url, code) { return this.peerRequest((id) => ({ t: 'peer-add', id, url, code })); }
2472
+ peerRemove(sidecarId) { return this.peerRequest((id) => ({ t: 'peer-remove', id, sidecarId })); }
2473
+ peerAllowSet(sidecarId, tools) { return this.peerRequest((id) => ({ t: 'peer-allow-set', id, sidecarId, tools })); }
2474
+ /** One of Part D's credential messages, answered by `agent-cred-result`.
2475
+ * Ids come from the MCP sequence so an agent login's `mcp-login-url` reaches
2476
+ * the waiter registered here and never collides with a server login. */
2477
+ agentCredRequest(build, onAuthUrl) {
2478
+ if (!this.supportsAgentAccess())
2479
+ return Promise.resolve({ ok: false, code: 'UNAVAILABLE', error: 'this daemon does not manage agent credentials' });
2480
+ const id = ++this.mcpSeq;
2481
+ if (onAuthUrl)
2482
+ this.mcpLoginUrlWaiters.set(id, onAuthUrl);
2483
+ return new Promise((resolve, reject) => {
2484
+ this.agentCredReplies.set(id, {
2485
+ resolve: (r) => { this.mcpLoginUrlWaiters.delete(id); resolve(r); },
2486
+ reject: (e) => { this.mcpLoginUrlWaiters.delete(id); reject(e); },
2487
+ });
2488
+ this.ch.send(build(id));
2489
+ });
2490
+ }
2491
+ accessBind(ref, server, binding) {
2492
+ return this.agentCredRequest((id) => ({ t: 'access-bind', id, ref, server, binding }));
2493
+ }
2494
+ /** The header values go out in this one message; nothing here keeps or returns them. */
2495
+ agentCredSet(ref, server, headers) {
2496
+ return this.agentCredRequest((id) => ({ t: 'agent-cred-set', id, ref, server, headers }));
2497
+ }
2498
+ agentMcpLogin(ref, server, onAuthUrl) {
2499
+ return this.agentCredRequest((id) => ({ t: 'agent-mcp-login', id, ref, server }), onAuthUrl);
2500
+ }
2501
+ agentCredClear(ref, server) {
2502
+ return this.agentCredRequest((id) => ({ t: 'agent-cred-clear', id, ref, server }));
2503
+ }
2116
2504
  /** True when the connected daemon keeps "This machine" standing approvals
2117
2505
  * (plan 3). Feature-detected from the welcome. */
2118
2506
  supportsApprovals() { return this.features.includes('approvals'); }
@@ -2133,13 +2521,15 @@ export class SidecarHost {
2133
2521
  const r = await this.approvalsRequest((id) => ({ t: 'approvals-accept', id, suggestionId, choice }));
2134
2522
  return r.ok ? { ok: true } : r;
2135
2523
  }
2136
- /** The daemon's standing rules and its (daemon.json) approval settings. */
2137
- async listDaemonApprovals() {
2138
- return this.withState(await this.approvalsRequest((id) => ({ t: 'approvals-list', id })));
2524
+ /** The daemon's standing rules and its (daemon.json) approval settings.
2525
+ * With `ref` on a team daemon: that agent's own rules. */
2526
+ async listDaemonApprovals(ref) {
2527
+ return this.withState(await this.approvalsRequest((id) => ({ t: 'approvals-list', id, ...(ref ? { ref } : {}) })));
2139
2528
  }
2140
- /** Remove one daemon standing rule; resolves with the complete state after. */
2141
- async revokeDaemonApproval(tool) {
2142
- return this.withState(await this.approvalsRequest((id) => ({ t: 'approvals-revoke', id, tool })));
2529
+ /** Remove one daemon standing rule; resolves with the complete state after.
2530
+ * With `ref` on a team daemon: that agent's rule (its owner only). */
2531
+ async revokeDaemonApproval(tool, ref) {
2532
+ return this.withState(await this.approvalsRequest((id) => ({ t: 'approvals-revoke', id, tool, ...(ref ? { ref } : {}) })));
2143
2533
  }
2144
2534
  withState(r) {
2145
2535
  if (!r.ok)
@@ -2360,6 +2750,9 @@ export class SidecarHost {
2360
2750
  * (`obj[method](...args)`) is uniform. */
2361
2751
  resolveProvider(p) {
2362
2752
  const pv = this.opts.providers;
2753
+ // A peer daemon answers mcp only, whatever else it may hold.
2754
+ if (this.opts.peerDaemon && p !== 'mcp')
2755
+ throw new Error(capabilityMissingMessage(p));
2363
2756
  let found;
2364
2757
  switch (p) {
2365
2758
  case 'confirm':
@@ -2373,7 +2766,7 @@ export class SidecarHost {
2373
2766
  break;
2374
2767
  }
2375
2768
  if (!found)
2376
- throw new Error(`capability ${p} is not available on this host`);
2769
+ throw new Error(capabilityMissingMessage(p));
2377
2770
  return found;
2378
2771
  }
2379
2772
  }