@corenel/harness 0.13.0 → 0.15.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 (145) 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 +12 -1
  8. package/dist/audit/recorder.d.ts.map +1 -1
  9. package/dist/audit/recorder.js +36 -5
  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 +4 -0
  14. package/dist/audit/runAudit.js.map +1 -1
  15. package/dist/conversation.d.ts +33 -0
  16. package/dist/conversation.d.ts.map +1 -1
  17. package/dist/conversation.js +44 -1
  18. package/dist/conversation.js.map +1 -1
  19. package/dist/core/compactHistory.d.ts +27 -18
  20. package/dist/core/compactHistory.d.ts.map +1 -1
  21. package/dist/core/compactHistory.js +106 -40
  22. package/dist/core/compactHistory.js.map +1 -1
  23. package/dist/core/dontsRecent.d.ts +5 -0
  24. package/dist/core/dontsRecent.d.ts.map +1 -0
  25. package/dist/core/dontsRecent.js +23 -0
  26. package/dist/core/dontsRecent.js.map +1 -0
  27. package/dist/core/foldBlocks.d.ts +43 -0
  28. package/dist/core/foldBlocks.d.ts.map +1 -0
  29. package/dist/core/foldBlocks.js +114 -0
  30. package/dist/core/foldBlocks.js.map +1 -0
  31. package/dist/core/foldTypes.d.ts +25 -0
  32. package/dist/core/foldTypes.d.ts.map +1 -0
  33. package/dist/core/foldTypes.js +2 -0
  34. package/dist/core/foldTypes.js.map +1 -0
  35. package/dist/core/loop.d.ts +26 -5
  36. package/dist/core/loop.d.ts.map +1 -1
  37. package/dist/core/loop.js +274 -74
  38. package/dist/core/loop.js.map +1 -1
  39. package/dist/core/recovery.d.ts +1 -0
  40. package/dist/core/recovery.d.ts.map +1 -1
  41. package/dist/core/recovery.js +12 -0
  42. package/dist/core/recovery.js.map +1 -1
  43. package/dist/core/spawn.d.ts +13 -1
  44. package/dist/core/spawn.d.ts.map +1 -1
  45. package/dist/core/spawn.js +10 -1
  46. package/dist/core/spawn.js.map +1 -1
  47. package/dist/core/summarize.d.ts +9 -3
  48. package/dist/core/summarize.d.ts.map +1 -1
  49. package/dist/core/summarize.js +97 -42
  50. package/dist/core/summarize.js.map +1 -1
  51. package/dist/dag/executeNode.d.ts +3 -1
  52. package/dist/dag/executeNode.d.ts.map +1 -1
  53. package/dist/dag/executeNode.js +1 -0
  54. package/dist/dag/executeNode.js.map +1 -1
  55. package/dist/guardrail/permission-service.d.ts.map +1 -1
  56. package/dist/guardrail/permission-service.js +31 -11
  57. package/dist/guardrail/permission-service.js.map +1 -1
  58. package/dist/guardrail/serialize.d.ts.map +1 -1
  59. package/dist/guardrail/serialize.js +5 -0
  60. package/dist/guardrail/serialize.js.map +1 -1
  61. package/dist/guardrail/validate.d.ts.map +1 -1
  62. package/dist/guardrail/validate.js +21 -6
  63. package/dist/guardrail/validate.js.map +1 -1
  64. package/dist/memory/store.d.ts +3 -0
  65. package/dist/memory/store.d.ts.map +1 -1
  66. package/dist/memory/store.js +16 -1
  67. package/dist/memory/store.js.map +1 -1
  68. package/dist/orchestrator/record.d.ts +7 -0
  69. package/dist/orchestrator/record.d.ts.map +1 -1
  70. package/dist/orchestrator/record.js +14 -2
  71. package/dist/orchestrator/record.js.map +1 -1
  72. package/dist/prompts/library/defaults.generated.d.ts +2 -0
  73. package/dist/prompts/library/defaults.generated.d.ts.map +1 -1
  74. package/dist/prompts/library/defaults.generated.js +6 -0
  75. package/dist/prompts/library/defaults.generated.js.map +1 -1
  76. package/dist/prompts/library/index.d.ts +3 -0
  77. package/dist/prompts/library/index.d.ts.map +1 -1
  78. package/dist/prompts/library/index.js +3 -0
  79. package/dist/prompts/library/index.js.map +1 -1
  80. package/dist/sessions/directivesStore.d.ts +9 -0
  81. package/dist/sessions/directivesStore.d.ts.map +1 -0
  82. package/dist/sessions/directivesStore.js +54 -0
  83. package/dist/sessions/directivesStore.js.map +1 -0
  84. package/dist/sessions/fileSessionStore.d.ts +3 -2
  85. package/dist/sessions/fileSessionStore.d.ts.map +1 -1
  86. package/dist/sessions/fileSessionStore.js +30 -9
  87. package/dist/sessions/fileSessionStore.js.map +1 -1
  88. package/dist/sessions/foldStorage.d.ts +15 -0
  89. package/dist/sessions/foldStorage.d.ts.map +1 -0
  90. package/dist/sessions/foldStorage.js +121 -0
  91. package/dist/sessions/foldStorage.js.map +1 -0
  92. package/dist/sessions/jsonl.d.ts +6 -0
  93. package/dist/sessions/jsonl.d.ts.map +1 -1
  94. package/dist/sessions/jsonl.js +15 -1
  95. package/dist/sessions/jsonl.js.map +1 -1
  96. package/dist/sidecar/accessTestRig.d.ts +32 -0
  97. package/dist/sidecar/accessTestRig.d.ts.map +1 -0
  98. package/dist/sidecar/accessTestRig.js +107 -0
  99. package/dist/sidecar/accessTestRig.js.map +1 -0
  100. package/dist/sidecar/askView.d.ts +20 -0
  101. package/dist/sidecar/askView.d.ts.map +1 -0
  102. package/dist/sidecar/askView.js +38 -0
  103. package/dist/sidecar/askView.js.map +1 -0
  104. package/dist/sidecar/host.d.ts +139 -11
  105. package/dist/sidecar/host.d.ts.map +1 -1
  106. package/dist/sidecar/host.js +394 -13
  107. package/dist/sidecar/host.js.map +1 -1
  108. package/dist/sidecar/hostCapability.d.ts +6 -0
  109. package/dist/sidecar/hostCapability.d.ts.map +1 -0
  110. package/dist/sidecar/hostCapability.js +7 -0
  111. package/dist/sidecar/hostCapability.js.map +1 -0
  112. package/dist/sidecar/identity.d.ts +5 -0
  113. package/dist/sidecar/identity.d.ts.map +1 -1
  114. package/dist/sidecar/memberGate.d.ts +5 -0
  115. package/dist/sidecar/memberGate.d.ts.map +1 -0
  116. package/dist/sidecar/memberGate.js +128 -0
  117. package/dist/sidecar/memberGate.js.map +1 -0
  118. package/dist/sidecar/server.d.ts +425 -11
  119. package/dist/sidecar/server.d.ts.map +1 -1
  120. package/dist/sidecar/server.js +1820 -53
  121. package/dist/sidecar/server.js.map +1 -1
  122. package/dist/skills.d.ts +7 -1
  123. package/dist/skills.d.ts.map +1 -1
  124. package/dist/skills.js +18 -5
  125. package/dist/skills.js.map +1 -1
  126. package/dist/tools/builtins.d.ts +17 -0
  127. package/dist/tools/builtins.d.ts.map +1 -1
  128. package/dist/tools/builtins.js +22 -2
  129. package/dist/tools/builtins.js.map +1 -1
  130. package/dist/tools/callAgent.d.ts +14 -1
  131. package/dist/tools/callAgent.d.ts.map +1 -1
  132. package/dist/tools/callAgent.js +18 -2
  133. package/dist/tools/callAgent.js.map +1 -1
  134. package/dist/tools/createCrewAgent.d.ts +39 -0
  135. package/dist/tools/createCrewAgent.d.ts.map +1 -0
  136. package/dist/tools/createCrewAgent.js +121 -0
  137. package/dist/tools/createCrewAgent.js.map +1 -0
  138. package/dist/tools/foldTools.d.ts +5 -0
  139. package/dist/tools/foldTools.d.ts.map +1 -0
  140. package/dist/tools/foldTools.js +123 -0
  141. package/dist/tools/foldTools.js.map +1 -0
  142. package/dist/tools/skillTool.d.ts.map +1 -1
  143. package/dist/tools/skillTool.js +7 -2
  144. package/dist/tools/skillTool.js.map +1 -1
  145. 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,16 @@ 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
+ }
35
46
  export function isSidecarAccountRefusal(e) {
36
47
  const msg = e instanceof Error ? e.message : String(e);
37
48
  return msg.startsWith(SIDECAR_NOT_ADMITTED);
@@ -215,6 +226,66 @@ export class SidecarHost {
215
226
  writable: true,
216
227
  value: new Map()
217
228
  });
229
+ Object.defineProperty(this, "accessSeq", {
230
+ enumerable: true,
231
+ configurable: true,
232
+ writable: true,
233
+ value: 0
234
+ });
235
+ Object.defineProperty(this, "accessPending", {
236
+ enumerable: true,
237
+ configurable: true,
238
+ writable: true,
239
+ value: new Map()
240
+ });
241
+ Object.defineProperty(this, "memberSeq", {
242
+ enumerable: true,
243
+ configurable: true,
244
+ writable: true,
245
+ value: 0
246
+ });
247
+ Object.defineProperty(this, "memberReplies", {
248
+ enumerable: true,
249
+ configurable: true,
250
+ writable: true,
251
+ value: new Map()
252
+ });
253
+ Object.defineProperty(this, "overviewReplies", {
254
+ enumerable: true,
255
+ configurable: true,
256
+ writable: true,
257
+ value: new Map()
258
+ });
259
+ Object.defineProperty(this, "agentCredReplies", {
260
+ enumerable: true,
261
+ configurable: true,
262
+ writable: true,
263
+ value: new Map()
264
+ });
265
+ Object.defineProperty(this, "agentCredStatusReplies", {
266
+ enumerable: true,
267
+ configurable: true,
268
+ writable: true,
269
+ value: new Map()
270
+ });
271
+ Object.defineProperty(this, "teamSurfaceSeq", {
272
+ enumerable: true,
273
+ configurable: true,
274
+ writable: true,
275
+ value: 0
276
+ });
277
+ Object.defineProperty(this, "agentRunsReplies", {
278
+ enumerable: true,
279
+ configurable: true,
280
+ writable: true,
281
+ value: new Map()
282
+ });
283
+ Object.defineProperty(this, "peerReplies", {
284
+ enumerable: true,
285
+ configurable: true,
286
+ writable: true,
287
+ value: new Map()
288
+ });
218
289
  Object.defineProperty(this, "approvalsSeq", {
219
290
  enumerable: true,
220
291
  configurable: true,
@@ -376,6 +447,18 @@ export class SidecarHost {
376
447
  writable: true,
377
448
  value: []
378
449
  });
450
+ Object.defineProperty(this, "sttSeq", {
451
+ enumerable: true,
452
+ configurable: true,
453
+ writable: true,
454
+ value: 0
455
+ });
456
+ Object.defineProperty(this, "sttStreams", {
457
+ enumerable: true,
458
+ configurable: true,
459
+ writable: true,
460
+ value: new Map()
461
+ });
379
462
  Object.defineProperty(this, "vapidKey", {
380
463
  enumerable: true,
381
464
  configurable: true,
@@ -484,6 +567,15 @@ export class SidecarHost {
484
567
  writable: true,
485
568
  value: false
486
569
  });
570
+ /** Bumped whenever the proven state is reset (each `connect`, each
571
+ * `disconnect`), so a server-proof still being verified for an EARLIER
572
+ * connection cannot mark the current one proven. */
573
+ Object.defineProperty(this, "proofEpoch", {
574
+ enumerable: true,
575
+ configurable: true,
576
+ writable: true,
577
+ value: 0
578
+ });
487
579
  /** The credential failed before sending (a malformed code, no key). */
488
580
  Object.defineProperty(this, "credentialError", {
489
581
  enumerable: true,
@@ -522,11 +614,29 @@ export class SidecarHost {
522
614
  writable: true,
523
615
  value: null
524
616
  });
617
+ /* A peer daemon serves the team daemon its MCP servers and nothing else
618
+ * (spec 2026-10-01 Part D): handing it any other store is a wiring
619
+ * mistake, refused here rather than silently withheld later. */
620
+ if (opts.peerDaemon) {
621
+ const extra = Object.keys(opts.providers)
622
+ .filter((k) => k !== 'mcp' && opts.providers[k] !== undefined);
623
+ if (extra.length)
624
+ throw new Error(`a peer daemon serves only mcp, not ${extra.join(', ')}`);
625
+ }
525
626
  this.opts = opts;
526
627
  this.ch = opts.channel;
527
628
  this.device = opts.device;
528
629
  this.ch.onMessage((m) => this.onMessage(m));
529
630
  }
631
+ /** Forget everything this host proved about a daemon: a new or dropped
632
+ * connection starts unproven, and must prove the pinned key again. */
633
+ resetProof() {
634
+ this.proofEpoch++;
635
+ this.expectProof = false;
636
+ this.verifiedSidecarJkt = null;
637
+ this.provenSidecarId = null;
638
+ this.challengedInHandshake = false;
639
+ }
530
640
  /** The hello's credential: the device if this client has one, else the
531
641
  * enrollment code. Exactly one is sent, and no secret. Sets up what the
532
642
  * proof exchange will be bound to. Without either (or without a key) there
@@ -564,6 +674,9 @@ export class SidecarHost {
564
674
  deviceCredential() { return this.device; }
565
675
  /** The sidecar key this connection verified, or null before it has. */
566
676
  sidecarKeyJkt() { return this.verifiedSidecarJkt; }
677
+ /** The id the daemon proved itself as (its server-proof), or null before
678
+ * it has. A peer daemon keys its per-team-daemon allowlist by it. */
679
+ sidecarId() { return this.provenSidecarId; }
567
680
  /** The daemon's `server-proof`: check it is the PINNED key and that it
568
681
  * signed THIS exchange, then prove this client's key. On any
569
682
  * failure nothing more is sent: the connection is dropped and `connect()`
@@ -573,6 +686,7 @@ export class SidecarHost {
573
686
  if (!this.expectProof || !key || this.verifiedSidecarJkt !== null)
574
687
  return;
575
688
  this.expectProof = false; // one exchange per connection
689
+ const epoch = this.proofEpoch;
576
690
  const sidecarKey = asEcPublicJwk(m.publicKey);
577
691
  const sidecarJkt = sidecarKey ? await jwkThumbprint(sidecarKey) : null;
578
692
  const ok = !!sidecarKey && !!sidecarJkt
@@ -581,6 +695,10 @@ export class SidecarHost {
581
695
  && await verifySignature(sidecarKey, serverProofPayload({
582
696
  clientNonce: this.clientNonce, serverNonce: m.serverNonce, sidecarId: m.sidecarId, sidecarJkt, subject: this.proofSubject,
583
697
  }), m.sig);
698
+ // The connection this proof belonged to was dropped or replaced while it
699
+ // was being verified: it proves nothing about the current one.
700
+ if (epoch !== this.proofEpoch)
701
+ return;
584
702
  if (!ok || !sidecarJkt) {
585
703
  this.failKeyMismatch();
586
704
  return;
@@ -627,6 +745,7 @@ export class SidecarHost {
627
745
  pending?.reject(new Error(`sidecar handshake timed out after ${timeoutMs}ms waiting for welcome`));
628
746
  }, timeoutMs);
629
747
  }
748
+ this.resetProof();
630
749
  const credential = this.credentialFields();
631
750
  if (this.credentialError) {
632
751
  const pending = this.welcome;
@@ -637,15 +756,22 @@ export class SidecarHost {
637
756
  this.ch.send({
638
757
  t: 'hello', protocol: SIDECAR_PROTOCOL, ...credential,
639
758
  ...(this.opts.stateDir ? { stateDir: this.opts.stateDir } : {}),
640
- ...(this.opts.clientId ? { clientId: this.opts.clientId } : {}),
641
- ...(this.opts.clientName ? { clientName: this.opts.clientName } : {}),
759
+ // A peer daemon is not a person's app: it has no client identity to
760
+ // record, and only it says what it serves.
761
+ ...(!this.opts.peerDaemon && this.opts.clientId ? { clientId: this.opts.clientId } : {}),
762
+ ...(!this.opts.peerDaemon && this.opts.clientName ? { clientName: this.opts.clientName } : {}),
642
763
  ...(this.opts.accounts?.length ? { accounts: this.opts.accounts } : {}),
764
+ ...(this.opts.peerDaemon && this.opts.provides ? { provides: this.opts.provides } : {}),
643
765
  // What THIS client understands, so a daemon can tell an older app
644
766
  // (whose agents sync but never fire until deployed) from this one.
645
767
  // DEVICES_FEATURE only when the caller has somewhere to KEEP an issued
646
768
  // device credential: a daemon issues one only to a client that says
647
769
  // 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] : [])],
770
+ capabilities: [
771
+ CREW_DEPLOY_FEATURE,
772
+ ...(this.opts.onDeviceIssued && this.opts.deviceKey ? [DEVICES_FEATURE] : []),
773
+ ...(this.opts.peerDaemon ? [PEER_DAEMON_FEATURE] : []),
774
+ ],
649
775
  });
650
776
  });
651
777
  }
@@ -671,6 +797,7 @@ export class SidecarHost {
671
797
  }
672
798
  /** Drop the connection: unregister imported tools and fail anything in flight. */
673
799
  disconnect(reason = 'disconnected') {
800
+ this.resetProof();
674
801
  for (const name of this.imported)
675
802
  this.opts.unregisterTool?.(name);
676
803
  this.imported.length = 0;
@@ -730,6 +857,20 @@ export class SidecarHost {
730
857
  for (const p of this.crewDeployPending.values())
731
858
  p.reject(new Error(reason));
732
859
  this.crewDeployPending.clear();
860
+ for (const p of this.accessPending.values())
861
+ p.reject(new Error(reason));
862
+ this.accessPending.clear();
863
+ for (const map of [this.memberReplies, this.overviewReplies, this.agentCredReplies, this.agentCredStatusReplies]) {
864
+ for (const p of map.values())
865
+ p.reject(new Error(reason));
866
+ map.clear();
867
+ }
868
+ for (const p of this.agentRunsReplies.values())
869
+ p.reject(new Error(reason));
870
+ this.agentRunsReplies.clear();
871
+ for (const p of this.peerReplies.values())
872
+ p.reject(new Error(reason));
873
+ this.peerReplies.clear();
733
874
  for (const p of this.approvalsPending.values())
734
875
  p.reject(new Error(reason));
735
876
  this.approvalsPending.clear();
@@ -792,6 +933,9 @@ export class SidecarHost {
792
933
  this.mcpLoginUrlWaiters.clear();
793
934
  this.welcome?.reject(new Error(reason));
794
935
  this.welcome = null;
936
+ for (const s of this.sttStreams.values())
937
+ s.onClosed(reason);
938
+ this.sttStreams.clear();
795
939
  this.ch.close();
796
940
  }
797
941
  onMessage(m) {
@@ -850,7 +994,7 @@ export class SidecarHost {
850
994
  case 'denied': {
851
995
  this.welcome?.reject(new Error(this.challengedInHandshake
852
996
  ? `${SIDECAR_NOT_ADMITTED}: ${m.reason}`
853
- : `sidecar denied connection: ${m.reason}`));
997
+ : `${SIDECAR_DENIED}: ${m.reason}`));
854
998
  this.welcome = null;
855
999
  return;
856
1000
  }
@@ -975,6 +1119,10 @@ export class SidecarHost {
975
1119
  this.runPending.get(m.id)?.onEvent(m.event);
976
1120
  return;
977
1121
  }
1122
+ case 'child-event': {
1123
+ this.runPending.get(m.id)?.onChild?.(m.event);
1124
+ return;
1125
+ }
978
1126
  case 'run-done': {
979
1127
  const p = this.runPending.get(m.id);
980
1128
  if (!p)
@@ -1047,6 +1195,15 @@ export class SidecarHost {
1047
1195
  this.opts.onOpenUrl?.({ url: m.url, title: m.title, note: m.note, meta: m.meta });
1048
1196
  return;
1049
1197
  }
1198
+ case 'stt': {
1199
+ const s = this.sttStreams.get(m.sid);
1200
+ if (!s)
1201
+ return;
1202
+ if (m.msg.kind === 'final' || m.msg.kind === 'error')
1203
+ this.sttStreams.delete(m.sid);
1204
+ s.onMessage(m.msg);
1205
+ return;
1206
+ }
1050
1207
  case 'proc-started': {
1051
1208
  const p = this.procPending.get(m.id);
1052
1209
  if (!p)
@@ -1333,6 +1490,81 @@ export class SidecarHost {
1333
1490
  p.resolve(m.reply);
1334
1491
  return;
1335
1492
  }
1493
+ case 'member-result': {
1494
+ const p = this.memberReplies.get(m.id);
1495
+ if (!p)
1496
+ return;
1497
+ this.memberReplies.delete(m.id);
1498
+ p.resolve(m.ok
1499
+ ? {
1500
+ ok: true,
1501
+ ...(m.status ? { status: m.status } : {}),
1502
+ ...(m.daemonName !== undefined ? { daemonName: m.daemonName } : {}),
1503
+ ...(m.adminLabel !== undefined ? { adminLabel: m.adminLabel } : {}),
1504
+ ...(m.adminId !== undefined ? { adminId: m.adminId } : {}),
1505
+ ...(m.admin !== undefined ? { admin: m.admin } : {}),
1506
+ ...(m.members ? { members: m.members } : {}),
1507
+ }
1508
+ : { ok: false, code: m.code, error: m.error });
1509
+ return;
1510
+ }
1511
+ case 'member-status-changed':
1512
+ if (m.status === 'active')
1513
+ this.opts.onMemberStatusChanged?.('active');
1514
+ return;
1515
+ case 'access-overview-result': {
1516
+ const p = this.overviewReplies.get(m.id);
1517
+ if (!p)
1518
+ return;
1519
+ this.overviewReplies.delete(m.id);
1520
+ // An older daemon sends no inactiveDeployments: read as none.
1521
+ p.resolve(m.ok
1522
+ ? { ok: true, rows: m.rows, inactiveDeployments: Array.isArray(m.inactiveDeployments) ? m.inactiveDeployments : [] }
1523
+ : { ok: false, code: m.code, error: m.error });
1524
+ return;
1525
+ }
1526
+ case 'agent-cred-result': {
1527
+ const p = this.agentCredReplies.get(m.id);
1528
+ if (!p)
1529
+ return;
1530
+ this.agentCredReplies.delete(m.id);
1531
+ p.resolve(m.ok ? { ok: true } : { ok: false, code: m.code, error: m.error, ...(m.reason ? { reason: m.reason } : {}) });
1532
+ return;
1533
+ }
1534
+ case 'agent-runs-result': {
1535
+ const p = this.agentRunsReplies.get(m.id);
1536
+ if (!p)
1537
+ return;
1538
+ this.agentRunsReplies.delete(m.id);
1539
+ p.resolve(m.ok ? { ok: true, runs: m.runs } : { ok: false, code: m.code, error: m.error });
1540
+ return;
1541
+ }
1542
+ case 'peer-result': {
1543
+ const p = this.peerReplies.get(m.id);
1544
+ if (!p)
1545
+ return;
1546
+ this.peerReplies.delete(m.id);
1547
+ p.resolve(m.ok ? { ok: true, ...(m.peers ? { peers: m.peers } : {}) } : { ok: false, code: m.code, error: m.error });
1548
+ return;
1549
+ }
1550
+ case 'agent-cred-status-result': {
1551
+ const p = this.agentCredStatusReplies.get(m.id);
1552
+ if (!p)
1553
+ return;
1554
+ this.agentCredStatusReplies.delete(m.id);
1555
+ p.resolve(m.ok ? { ok: true, servers: m.servers } : { ok: false, code: m.code, error: m.error });
1556
+ return;
1557
+ }
1558
+ case 'access-result': {
1559
+ const p = this.accessPending.get(m.id);
1560
+ if (!p)
1561
+ return;
1562
+ this.accessPending.delete(m.id);
1563
+ p.resolve(m.ok
1564
+ ? { ok: true, ...(m.access ? { access: m.access } : {}), ...(m.members ? { members: m.members } : {}) }
1565
+ : { ok: false, code: m.code, error: m.error });
1566
+ return;
1567
+ }
1336
1568
  case 'checks-get-result':
1337
1569
  case 'checks-pin-result': {
1338
1570
  // One map for both: each answers with the complete state.
@@ -1449,6 +1681,25 @@ export class SidecarHost {
1449
1681
  * allowed shell. A client MUST check this before offering the surface: a
1450
1682
  * daemon without it answers every proc message with an error. */
1451
1683
  supportsProcs() { return this.features.includes('procs'); }
1684
+ /** True when the daemon transcribes dictation on its own machine. */
1685
+ supportsSpeech() { return this.features.includes(SPEECH_FEATURE); }
1686
+ /** One dictation stream to the daemon's speech model. Replies arrive on
1687
+ * `onMessage` until a final or an error; a dropped connection calls
1688
+ * `onClosed`, which the browser engine turns into a crash so the mic is
1689
+ * released. */
1690
+ openSpeechStream(onMessage, onClosed) {
1691
+ const sid = `s${++this.sttSeq}${Math.random().toString(36).slice(2, 8)}`;
1692
+ this.sttStreams.set(sid, { onMessage, onClosed });
1693
+ this.ch.send({ t: 'stt-start', sid });
1694
+ return {
1695
+ audio: (pcm) => { if (this.sttStreams.has(sid))
1696
+ this.ch.send({ t: 'stt-audio', sid, pcm: encodePcm16(pcm) }); },
1697
+ flush: () => { if (this.sttStreams.has(sid))
1698
+ this.ch.send({ t: 'stt-flush', sid }); },
1699
+ cancel: () => { if (this.sttStreams.delete(sid))
1700
+ this.ch.send({ t: 'stt-cancel', sid }); },
1701
+ };
1702
+ }
1452
1703
  /** Start a long-running command on the sidecar and stream its output.
1453
1704
  *
1454
1705
  * This is the counterpart to the one-shot `sidecar_shell_exec` tool, not a
@@ -1757,7 +2008,7 @@ export class SidecarHost {
1757
2008
  /** WATCH a run this client did not start (a trigger-fired crew run). Rides
1758
2009
  * the same pending map as `run`, so `event`/`run-done`/`run-error` route here
1759
2010
  * unchanged; the server replays what already happened first. */
1760
- watchRun(runId, onEvent) {
2011
+ watchRun(runId, onEvent, onChild) {
1761
2012
  const id = ++this.runSeq;
1762
2013
  let rejectDone;
1763
2014
  const done = new Promise((resolve, reject) => {
@@ -1766,6 +2017,7 @@ export class SidecarHost {
1766
2017
  resolve: (r) => resolve({ text: r.text, stopReason: r.stopReason }),
1767
2018
  reject,
1768
2019
  onEvent,
2020
+ ...(onChild ? { onChild } : {}),
1769
2021
  });
1770
2022
  this.ch.send({ t: 'subscribe', id, runId });
1771
2023
  });
@@ -1790,6 +2042,10 @@ export class SidecarHost {
1790
2042
  }
1791
2043
  /** True when the connected sidecar's runner supports cancelling a run. */
1792
2044
  supportsRunCancel() { return this.features.includes('run-cancel'); }
2045
+ /** True when the connected daemon fans a delegated callee's own node events
2046
+ * out to a watched run's `onChild` (running pills). An older daemon simply
2047
+ * never sends `child-event`, so a watcher's card shows no callee. */
2048
+ supportsChildEvents() { return this.features.includes(CHILD_EVENTS_FEATURE); }
1793
2049
  /** True when the connected sidecar understands `run`'s `task`/`autonomy`
1794
2050
  * fields. An older sidecar without this feature would silently ignore
1795
2051
  * them, so callers must check this before sending either — never send and
@@ -2113,6 +2369,126 @@ export class SidecarHost {
2113
2369
  crewResume(ref, reason) { return this.crewDeployRequest((id) => ({ t: 'crew-resume', id, ref, ...(reason ? { reason } : {}) })); }
2114
2370
  /** Stop a run AND record who stopped it and why (the plain `cancelRun` records nothing). */
2115
2371
  crewStop(ref, runId, reason) { return this.crewDeployRequest((id) => ({ t: 'crew-stop', id, ref, runId, reason })); }
2372
+ /** True when the connected daemon keeps per-agent access (team mode, Part B). */
2373
+ supportsAgentAccess() { return this.features.includes(AGENT_ACCESS_FEATURE); }
2374
+ /** One access-* request. `ok: false` is the daemon's considered answer and
2375
+ * resolves; only a dropped connection rejects. Nothing is sent to a daemon
2376
+ * without the feature. */
2377
+ accessRequest(build) {
2378
+ if (!this.supportsAgentAccess())
2379
+ return Promise.resolve({ ok: false, code: 'NOT_TEAM_AGENT', error: 'this daemon does not keep agent access' });
2380
+ const id = ++this.accessSeq;
2381
+ return new Promise((resolve, reject) => {
2382
+ this.accessPending.set(id, { resolve, reject });
2383
+ this.ch.send(build(id));
2384
+ });
2385
+ }
2386
+ accessGet(ref) { return this.accessRequest((id) => ({ t: 'access-get', id, ref })); }
2387
+ accessGrant(ref, userId, role) {
2388
+ return this.accessRequest((id) => ({ t: 'access-grant', id, ref, userId, role }));
2389
+ }
2390
+ accessRevoke(ref, userId) { return this.accessRequest((id) => ({ t: 'access-revoke', id, ref, userId })); }
2391
+ accessTransfer(ref, userId) { return this.accessRequest((id) => ({ t: 'access-transfer', id, ref, userId })); }
2392
+ accessMembers() { return this.accessRequest((id) => ({ t: 'access-members', id })); }
2393
+ /** True when the connected daemon is in team mode with a member list (Part E). */
2394
+ supportsTeamMembers() { return this.features.includes(TEAM_MEMBERS_FEATURE); }
2395
+ /** One member-* request. `ok: false` is the daemon's answer and resolves;
2396
+ * only a dropped connection rejects. Nothing is sent without the feature. */
2397
+ memberRequest(build) {
2398
+ if (!this.supportsTeamMembers())
2399
+ return Promise.resolve({ ok: false, code: 'UNAVAILABLE', error: 'this daemon keeps no member list' });
2400
+ const id = ++this.memberSeq;
2401
+ return new Promise((resolve, reject) => {
2402
+ this.memberReplies.set(id, { resolve, reject });
2403
+ this.ch.send(build(id));
2404
+ });
2405
+ }
2406
+ memberStatus() { return this.memberRequest((id) => ({ t: 'member-status', id })); }
2407
+ membersList() { return this.memberRequest((id) => ({ t: 'members-list', id })); }
2408
+ memberApprove(userId) { return this.memberRequest((id) => ({ t: 'member-approve', id, userId })); }
2409
+ memberRemove(userId) { return this.memberRequest((id) => ({ t: 'member-remove', id, userId })); }
2410
+ /** Every team agent this user may see, scoped to their role (Part E, E2). */
2411
+ accessOverview() {
2412
+ if (!this.supportsAgentAccess())
2413
+ return Promise.resolve({ ok: false, code: 'NOT_TEAM_AGENT', error: 'this daemon does not keep agent access' });
2414
+ const id = ++this.memberSeq;
2415
+ return new Promise((resolve, reject) => {
2416
+ this.overviewReplies.set(id, { resolve, reject });
2417
+ this.ch.send({ t: 'access-overview', id });
2418
+ });
2419
+ }
2420
+ /** Each MCP server's binding and credential presence for one agent (owner or admin). */
2421
+ agentCredStatus(ref) {
2422
+ if (!this.supportsAgentAccess())
2423
+ return Promise.resolve({ ok: false, code: 'UNAVAILABLE', error: 'this daemon does not manage agent credentials' });
2424
+ const id = ++this.memberSeq;
2425
+ return new Promise((resolve, reject) => {
2426
+ this.agentCredStatusReplies.set(id, { resolve, reject });
2427
+ this.ch.send({ t: 'agent-cred-status', id, ref });
2428
+ });
2429
+ }
2430
+ /** The connected daemon keeps team run history (spec 2026-10-01 E5.6). */
2431
+ supportsAgentRuns() { return this.features.includes(AGENT_RUNS_FEATURE); }
2432
+ /** Every member's runs of a team agent, newest first, no content. A
2433
+ * refusal resolves; only a dropped connection rejects. */
2434
+ agentRuns(ref, opts = {}) {
2435
+ if (!this.supportsAgentRuns())
2436
+ return Promise.resolve({ ok: false, code: 'NOT_TEAM_AGENT', error: 'this daemon keeps no team run history' });
2437
+ const id = ++this.teamSurfaceSeq;
2438
+ return new Promise((resolve, reject) => {
2439
+ this.agentRunsReplies.set(id, { resolve, reject });
2440
+ this.ch.send({
2441
+ t: 'agent-runs', id, ref,
2442
+ ...(opts.limit !== undefined ? { limit: opts.limit } : {}),
2443
+ ...(opts.before !== undefined ? { before: opts.before } : {}),
2444
+ });
2445
+ });
2446
+ }
2447
+ /** The connected daemon links to team daemons from here (spec 2026-10-01 E5.5). */
2448
+ supportsPeerLinks() { return this.features.includes(PEER_LINKS_FEATURE); }
2449
+ peerRequest(build) {
2450
+ if (!this.supportsPeerLinks())
2451
+ return Promise.resolve({ ok: false, code: 'UNAVAILABLE', error: 'this daemon does not link to team daemons' });
2452
+ const id = ++this.teamSurfaceSeq;
2453
+ return new Promise((resolve, reject) => {
2454
+ this.peerReplies.set(id, { resolve, reject });
2455
+ this.ch.send(build(id));
2456
+ });
2457
+ }
2458
+ peerList() { return this.peerRequest((id) => ({ t: 'peer-list', id })); }
2459
+ peerAdd(url, code) { return this.peerRequest((id) => ({ t: 'peer-add', id, url, code })); }
2460
+ peerRemove(sidecarId) { return this.peerRequest((id) => ({ t: 'peer-remove', id, sidecarId })); }
2461
+ peerAllowSet(sidecarId, tools) { return this.peerRequest((id) => ({ t: 'peer-allow-set', id, sidecarId, tools })); }
2462
+ /** One of Part D's credential messages, answered by `agent-cred-result`.
2463
+ * Ids come from the MCP sequence so an agent login's `mcp-login-url` reaches
2464
+ * the waiter registered here and never collides with a server login. */
2465
+ agentCredRequest(build, onAuthUrl) {
2466
+ if (!this.supportsAgentAccess())
2467
+ return Promise.resolve({ ok: false, code: 'UNAVAILABLE', error: 'this daemon does not manage agent credentials' });
2468
+ const id = ++this.mcpSeq;
2469
+ if (onAuthUrl)
2470
+ this.mcpLoginUrlWaiters.set(id, onAuthUrl);
2471
+ return new Promise((resolve, reject) => {
2472
+ this.agentCredReplies.set(id, {
2473
+ resolve: (r) => { this.mcpLoginUrlWaiters.delete(id); resolve(r); },
2474
+ reject: (e) => { this.mcpLoginUrlWaiters.delete(id); reject(e); },
2475
+ });
2476
+ this.ch.send(build(id));
2477
+ });
2478
+ }
2479
+ accessBind(ref, server, binding) {
2480
+ return this.agentCredRequest((id) => ({ t: 'access-bind', id, ref, server, binding }));
2481
+ }
2482
+ /** The header values go out in this one message; nothing here keeps or returns them. */
2483
+ agentCredSet(ref, server, headers) {
2484
+ return this.agentCredRequest((id) => ({ t: 'agent-cred-set', id, ref, server, headers }));
2485
+ }
2486
+ agentMcpLogin(ref, server, onAuthUrl) {
2487
+ return this.agentCredRequest((id) => ({ t: 'agent-mcp-login', id, ref, server }), onAuthUrl);
2488
+ }
2489
+ agentCredClear(ref, server) {
2490
+ return this.agentCredRequest((id) => ({ t: 'agent-cred-clear', id, ref, server }));
2491
+ }
2116
2492
  /** True when the connected daemon keeps "This machine" standing approvals
2117
2493
  * (plan 3). Feature-detected from the welcome. */
2118
2494
  supportsApprovals() { return this.features.includes('approvals'); }
@@ -2133,13 +2509,15 @@ export class SidecarHost {
2133
2509
  const r = await this.approvalsRequest((id) => ({ t: 'approvals-accept', id, suggestionId, choice }));
2134
2510
  return r.ok ? { ok: true } : r;
2135
2511
  }
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 })));
2512
+ /** The daemon's standing rules and its (daemon.json) approval settings.
2513
+ * With `ref` on a team daemon: that agent's own rules. */
2514
+ async listDaemonApprovals(ref) {
2515
+ return this.withState(await this.approvalsRequest((id) => ({ t: 'approvals-list', id, ...(ref ? { ref } : {}) })));
2139
2516
  }
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 })));
2517
+ /** Remove one daemon standing rule; resolves with the complete state after.
2518
+ * With `ref` on a team daemon: that agent's rule (its owner only). */
2519
+ async revokeDaemonApproval(tool, ref) {
2520
+ return this.withState(await this.approvalsRequest((id) => ({ t: 'approvals-revoke', id, tool, ...(ref ? { ref } : {}) })));
2143
2521
  }
2144
2522
  withState(r) {
2145
2523
  if (!r.ok)
@@ -2360,6 +2738,9 @@ export class SidecarHost {
2360
2738
  * (`obj[method](...args)`) is uniform. */
2361
2739
  resolveProvider(p) {
2362
2740
  const pv = this.opts.providers;
2741
+ // A peer daemon answers mcp only, whatever else it may hold.
2742
+ if (this.opts.peerDaemon && p !== 'mcp')
2743
+ throw new Error(capabilityMissingMessage(p));
2363
2744
  let found;
2364
2745
  switch (p) {
2365
2746
  case 'confirm':
@@ -2373,7 +2754,7 @@ export class SidecarHost {
2373
2754
  break;
2374
2755
  }
2375
2756
  if (!found)
2376
- throw new Error(`capability ${p} is not available on this host`);
2757
+ throw new Error(capabilityMissingMessage(p));
2377
2758
  return found;
2378
2759
  }
2379
2760
  }