@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.
- package/dist/approvals/service.d.ts.map +1 -1
- package/dist/approvals/service.js +4 -1
- package/dist/approvals/service.js.map +1 -1
- package/dist/audit/log.d.ts.map +1 -1
- package/dist/audit/log.js +12 -1
- package/dist/audit/log.js.map +1 -1
- package/dist/audit/recorder.d.ts +12 -1
- package/dist/audit/recorder.d.ts.map +1 -1
- package/dist/audit/recorder.js +36 -5
- package/dist/audit/recorder.js.map +1 -1
- package/dist/audit/runAudit.d.ts +2 -1
- package/dist/audit/runAudit.d.ts.map +1 -1
- package/dist/audit/runAudit.js +4 -0
- package/dist/audit/runAudit.js.map +1 -1
- package/dist/conversation.d.ts +33 -0
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +44 -1
- package/dist/conversation.js.map +1 -1
- package/dist/core/compactHistory.d.ts +27 -18
- package/dist/core/compactHistory.d.ts.map +1 -1
- package/dist/core/compactHistory.js +106 -40
- package/dist/core/compactHistory.js.map +1 -1
- package/dist/core/dontsRecent.d.ts +5 -0
- package/dist/core/dontsRecent.d.ts.map +1 -0
- package/dist/core/dontsRecent.js +23 -0
- package/dist/core/dontsRecent.js.map +1 -0
- package/dist/core/foldBlocks.d.ts +43 -0
- package/dist/core/foldBlocks.d.ts.map +1 -0
- package/dist/core/foldBlocks.js +114 -0
- package/dist/core/foldBlocks.js.map +1 -0
- package/dist/core/foldTypes.d.ts +25 -0
- package/dist/core/foldTypes.d.ts.map +1 -0
- package/dist/core/foldTypes.js +2 -0
- package/dist/core/foldTypes.js.map +1 -0
- package/dist/core/loop.d.ts +26 -5
- package/dist/core/loop.d.ts.map +1 -1
- package/dist/core/loop.js +274 -74
- package/dist/core/loop.js.map +1 -1
- package/dist/core/recovery.d.ts +1 -0
- package/dist/core/recovery.d.ts.map +1 -1
- package/dist/core/recovery.js +12 -0
- package/dist/core/recovery.js.map +1 -1
- package/dist/core/spawn.d.ts +13 -1
- package/dist/core/spawn.d.ts.map +1 -1
- package/dist/core/spawn.js +10 -1
- package/dist/core/spawn.js.map +1 -1
- package/dist/core/summarize.d.ts +9 -3
- package/dist/core/summarize.d.ts.map +1 -1
- package/dist/core/summarize.js +97 -42
- package/dist/core/summarize.js.map +1 -1
- package/dist/dag/executeNode.d.ts +3 -1
- package/dist/dag/executeNode.d.ts.map +1 -1
- package/dist/dag/executeNode.js +1 -0
- package/dist/dag/executeNode.js.map +1 -1
- package/dist/guardrail/permission-service.d.ts.map +1 -1
- package/dist/guardrail/permission-service.js +31 -11
- package/dist/guardrail/permission-service.js.map +1 -1
- package/dist/guardrail/serialize.d.ts.map +1 -1
- package/dist/guardrail/serialize.js +5 -0
- package/dist/guardrail/serialize.js.map +1 -1
- package/dist/guardrail/validate.d.ts.map +1 -1
- package/dist/guardrail/validate.js +21 -6
- package/dist/guardrail/validate.js.map +1 -1
- package/dist/memory/store.d.ts +3 -0
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +16 -1
- package/dist/memory/store.js.map +1 -1
- package/dist/orchestrator/record.d.ts +7 -0
- package/dist/orchestrator/record.d.ts.map +1 -1
- package/dist/orchestrator/record.js +14 -2
- package/dist/orchestrator/record.js.map +1 -1
- package/dist/prompts/library/defaults.generated.d.ts +2 -0
- package/dist/prompts/library/defaults.generated.d.ts.map +1 -1
- package/dist/prompts/library/defaults.generated.js +6 -0
- package/dist/prompts/library/defaults.generated.js.map +1 -1
- package/dist/prompts/library/index.d.ts +3 -0
- package/dist/prompts/library/index.d.ts.map +1 -1
- package/dist/prompts/library/index.js +3 -0
- package/dist/prompts/library/index.js.map +1 -1
- package/dist/sessions/directivesStore.d.ts +9 -0
- package/dist/sessions/directivesStore.d.ts.map +1 -0
- package/dist/sessions/directivesStore.js +54 -0
- package/dist/sessions/directivesStore.js.map +1 -0
- package/dist/sessions/fileSessionStore.d.ts +3 -2
- package/dist/sessions/fileSessionStore.d.ts.map +1 -1
- package/dist/sessions/fileSessionStore.js +30 -9
- package/dist/sessions/fileSessionStore.js.map +1 -1
- package/dist/sessions/foldStorage.d.ts +15 -0
- package/dist/sessions/foldStorage.d.ts.map +1 -0
- package/dist/sessions/foldStorage.js +121 -0
- package/dist/sessions/foldStorage.js.map +1 -0
- package/dist/sessions/jsonl.d.ts +6 -0
- package/dist/sessions/jsonl.d.ts.map +1 -1
- package/dist/sessions/jsonl.js +15 -1
- package/dist/sessions/jsonl.js.map +1 -1
- package/dist/sidecar/accessTestRig.d.ts +32 -0
- package/dist/sidecar/accessTestRig.d.ts.map +1 -0
- package/dist/sidecar/accessTestRig.js +107 -0
- package/dist/sidecar/accessTestRig.js.map +1 -0
- package/dist/sidecar/askView.d.ts +20 -0
- package/dist/sidecar/askView.d.ts.map +1 -0
- package/dist/sidecar/askView.js +38 -0
- package/dist/sidecar/askView.js.map +1 -0
- package/dist/sidecar/host.d.ts +139 -11
- package/dist/sidecar/host.d.ts.map +1 -1
- package/dist/sidecar/host.js +394 -13
- package/dist/sidecar/host.js.map +1 -1
- package/dist/sidecar/hostCapability.d.ts +6 -0
- package/dist/sidecar/hostCapability.d.ts.map +1 -0
- package/dist/sidecar/hostCapability.js +7 -0
- package/dist/sidecar/hostCapability.js.map +1 -0
- package/dist/sidecar/identity.d.ts +5 -0
- package/dist/sidecar/identity.d.ts.map +1 -1
- package/dist/sidecar/memberGate.d.ts +5 -0
- package/dist/sidecar/memberGate.d.ts.map +1 -0
- package/dist/sidecar/memberGate.js +128 -0
- package/dist/sidecar/memberGate.js.map +1 -0
- package/dist/sidecar/server.d.ts +425 -11
- package/dist/sidecar/server.d.ts.map +1 -1
- package/dist/sidecar/server.js +1820 -53
- package/dist/sidecar/server.js.map +1 -1
- package/dist/skills.d.ts +7 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +18 -5
- package/dist/skills.js.map +1 -1
- package/dist/tools/builtins.d.ts +17 -0
- package/dist/tools/builtins.d.ts.map +1 -1
- package/dist/tools/builtins.js +22 -2
- package/dist/tools/builtins.js.map +1 -1
- package/dist/tools/callAgent.d.ts +14 -1
- package/dist/tools/callAgent.d.ts.map +1 -1
- package/dist/tools/callAgent.js +18 -2
- package/dist/tools/callAgent.js.map +1 -1
- package/dist/tools/createCrewAgent.d.ts +39 -0
- package/dist/tools/createCrewAgent.d.ts.map +1 -0
- package/dist/tools/createCrewAgent.js +121 -0
- package/dist/tools/createCrewAgent.js.map +1 -0
- package/dist/tools/foldTools.d.ts +5 -0
- package/dist/tools/foldTools.d.ts.map +1 -0
- package/dist/tools/foldTools.js +123 -0
- package/dist/tools/foldTools.js.map +1 -0
- package/dist/tools/skillTool.d.ts.map +1 -1
- package/dist/tools/skillTool.js +7 -2
- package/dist/tools/skillTool.js.map +1 -1
- package/package.json +2 -2
package/dist/sidecar/host.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { asEcPublicJwk, jwkThumbprint, verifySignature, randomToken, serverProofPayload, clientProofPayload, parseEnrollmentCode, bytesToB64url, codeProof, codeHash, } from './deviceKeys.js';
|
|
2
|
-
import {
|
|
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
|
-
|
|
641
|
-
|
|
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: [
|
|
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
|
-
:
|
|
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
|
-
|
|
2138
|
-
|
|
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
|
-
|
|
2142
|
-
|
|
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(
|
|
2757
|
+
throw new Error(capabilityMissingMessage(p));
|
|
2377
2758
|
return found;
|
|
2378
2759
|
}
|
|
2379
2760
|
}
|