@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.
- 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 +7 -1
- package/dist/audit/recorder.d.ts.map +1 -1
- package/dist/audit/recorder.js +28 -4
- 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 +2 -0
- package/dist/audit/runAudit.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/loop.d.ts +7 -0
- package/dist/core/loop.d.ts.map +1 -1
- package/dist/core/loop.js +134 -5
- package/dist/core/loop.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 +20 -3
- package/dist/core/spawn.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 +21 -10
- 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/fileSessionStore.d.ts +1 -0
- package/dist/sessions/fileSessionStore.d.ts.map +1 -1
- package/dist/sessions/fileSessionStore.js +11 -1
- package/dist/sessions/fileSessionStore.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 +146 -11
- package/dist/sidecar/host.d.ts.map +1 -1
- package/dist/sidecar/host.js +406 -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 +1809 -52
- 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 +32 -3
- 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/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,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
|
-
|
|
641
|
-
|
|
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: [
|
|
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
|
-
:
|
|
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
|
-
|
|
2138
|
-
|
|
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
|
-
|
|
2142
|
-
|
|
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(
|
|
2769
|
+
throw new Error(capabilityMissingMessage(p));
|
|
2377
2770
|
return found;
|
|
2378
2771
|
}
|
|
2379
2772
|
}
|