@corenel/harness 0.22.0 → 0.24.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 (118) hide show
  1. package/README.md +48 -0
  2. package/dist/approvals/service.d.ts +3 -0
  3. package/dist/approvals/service.d.ts.map +1 -1
  4. package/dist/approvals/service.js +13 -0
  5. package/dist/approvals/service.js.map +1 -1
  6. package/dist/approvals/suggest.js +1 -1
  7. package/dist/approvals/suggest.js.map +1 -1
  8. package/dist/chat/engine.d.ts +3 -0
  9. package/dist/chat/engine.d.ts.map +1 -1
  10. package/dist/chat/engine.js +10 -6
  11. package/dist/chat/engine.js.map +1 -1
  12. package/dist/chat/types.d.ts +1 -0
  13. package/dist/chat/types.d.ts.map +1 -1
  14. package/dist/core/loop.d.ts.map +1 -1
  15. package/dist/core/loop.js +138 -81
  16. package/dist/core/loop.js.map +1 -1
  17. package/dist/core/spawn.d.ts +3 -1
  18. package/dist/core/spawn.d.ts.map +1 -1
  19. package/dist/core/spawn.js +16 -1
  20. package/dist/core/spawn.js.map +1 -1
  21. package/dist/dag/executeNode.js +4 -4
  22. package/dist/dag/executeNode.js.map +1 -1
  23. package/dist/guardrail/builtins.d.ts.map +1 -1
  24. package/dist/guardrail/builtins.js +2 -0
  25. package/dist/guardrail/builtins.js.map +1 -1
  26. package/dist/guardrail/guardrail.d.ts +9 -0
  27. package/dist/guardrail/guardrail.d.ts.map +1 -1
  28. package/dist/guardrail/guardrail.js +13 -0
  29. package/dist/guardrail/guardrail.js.map +1 -1
  30. package/dist/guardrail/index.d.ts +3 -3
  31. package/dist/guardrail/index.d.ts.map +1 -1
  32. package/dist/guardrail/index.js +2 -2
  33. package/dist/guardrail/index.js.map +1 -1
  34. package/dist/guardrail/permission-service.d.ts +44 -3
  35. package/dist/guardrail/permission-service.d.ts.map +1 -1
  36. package/dist/guardrail/permission-service.js +58 -11
  37. package/dist/guardrail/permission-service.js.map +1 -1
  38. package/dist/guardrail/store.d.ts +4 -0
  39. package/dist/guardrail/store.d.ts.map +1 -1
  40. package/dist/guardrail/store.js +6 -0
  41. package/dist/guardrail/store.js.map +1 -1
  42. package/dist/prompts/config.d.ts +6 -2
  43. package/dist/prompts/config.d.ts.map +1 -1
  44. package/dist/prompts/config.js +6 -3
  45. package/dist/prompts/config.js.map +1 -1
  46. package/dist/prompts/host.d.ts +3 -2
  47. package/dist/prompts/host.d.ts.map +1 -1
  48. package/dist/prompts/host.js.map +1 -1
  49. package/dist/prompts/index.d.ts +8 -1
  50. package/dist/prompts/index.d.ts.map +1 -1
  51. package/dist/prompts/index.js +9 -7
  52. package/dist/prompts/index.js.map +1 -1
  53. package/dist/prompts/library/defaults.generated.d.ts.map +1 -1
  54. package/dist/prompts/library/defaults.generated.js +15 -14
  55. package/dist/prompts/library/defaults.generated.js.map +1 -1
  56. package/dist/prompts/systemPrompts.d.ts +3 -0
  57. package/dist/prompts/systemPrompts.d.ts.map +1 -1
  58. package/dist/prompts/systemPrompts.js +1 -1
  59. package/dist/prompts/systemPrompts.js.map +1 -1
  60. package/dist/sessions/fileSessionStore.d.ts.map +1 -1
  61. package/dist/sessions/fileSessionStore.js +18 -4
  62. package/dist/sessions/fileSessionStore.js.map +1 -1
  63. package/dist/sessions/jsonl.d.ts +1 -0
  64. package/dist/sessions/jsonl.d.ts.map +1 -1
  65. package/dist/sessions/jsonl.js +10 -0
  66. package/dist/sessions/jsonl.js.map +1 -1
  67. package/dist/sidecar/accessTestRig.d.ts +9 -2
  68. package/dist/sidecar/accessTestRig.d.ts.map +1 -1
  69. package/dist/sidecar/accessTestRig.js +13 -6
  70. package/dist/sidecar/accessTestRig.js.map +1 -1
  71. package/dist/sidecar/host.d.ts +96 -10
  72. package/dist/sidecar/host.d.ts.map +1 -1
  73. package/dist/sidecar/host.js +328 -6
  74. package/dist/sidecar/host.js.map +1 -1
  75. package/dist/sidecar/memberGate.d.ts.map +1 -1
  76. package/dist/sidecar/memberGate.js +17 -0
  77. package/dist/sidecar/memberGate.js.map +1 -1
  78. package/dist/sidecar/server.d.ts +171 -6
  79. package/dist/sidecar/server.d.ts.map +1 -1
  80. package/dist/sidecar/server.js +793 -65
  81. package/dist/sidecar/server.js.map +1 -1
  82. package/dist/skills.d.ts +9 -1
  83. package/dist/skills.d.ts.map +1 -1
  84. package/dist/skills.js +22 -8
  85. package/dist/skills.js.map +1 -1
  86. package/dist/templateGuard.d.ts +21 -0
  87. package/dist/templateGuard.d.ts.map +1 -0
  88. package/dist/templateGuard.js +150 -0
  89. package/dist/templateGuard.js.map +1 -0
  90. package/dist/tools/builtins.d.ts +1 -0
  91. package/dist/tools/builtins.d.ts.map +1 -1
  92. package/dist/tools/builtins.js +33 -3
  93. package/dist/tools/builtins.js.map +1 -1
  94. package/dist/tools/callAgent.d.ts +6 -2
  95. package/dist/tools/callAgent.d.ts.map +1 -1
  96. package/dist/tools/callAgent.js +7 -3
  97. package/dist/tools/callAgent.js.map +1 -1
  98. package/dist/tools/files.d.ts.map +1 -1
  99. package/dist/tools/files.js +4 -0
  100. package/dist/tools/files.js.map +1 -1
  101. package/dist/tools/namespaces.d.ts +1 -1
  102. package/dist/tools/namespaces.d.ts.map +1 -1
  103. package/dist/tools/namespaces.js +6 -2
  104. package/dist/tools/namespaces.js.map +1 -1
  105. package/dist/tools/sessions.d.ts.map +1 -1
  106. package/dist/tools/sessions.js +1 -0
  107. package/dist/tools/sessions.js.map +1 -1
  108. package/dist/tools/skillTool.d.ts +29 -0
  109. package/dist/tools/skillTool.d.ts.map +1 -1
  110. package/dist/tools/skillTool.js +73 -10
  111. package/dist/tools/skillTool.js.map +1 -1
  112. package/dist/tools/web.d.ts.map +1 -1
  113. package/dist/tools/web.js +19 -5
  114. package/dist/tools/web.js.map +1 -1
  115. package/dist/tools/workflowTool.d.ts +2 -18
  116. package/dist/tools/workflowTool.d.ts.map +1 -1
  117. package/dist/tools/workflowTool.js.map +1 -1
  118. package/package.json +3 -3
@@ -1,7 +1,10 @@
1
- import { CHAT_FEATURE, CHAT_ID_RE, FS_CHANGED_FEATURE } from '@corenel/protocol';
1
+ import { CHAT_FEATURE, CHAT_ID_RE, FS_CHANGED_FEATURE, MCP_ALLOW_FEATURE } from '@corenel/protocol';
2
+ import { KNOWLEDGE_FEATURE, KNOWLEDGE_NOT_KEPT } from '@corenel/protocol';
2
3
  import { asEcPublicJwk, jwkThumbprint, verifySignature, randomToken, serverProofPayload, clientProofPayload, parseEnrollmentCode, bytesToB64url, codeProof, codeHash, } from './deviceKeys.js';
3
4
  import { capabilityMissingMessage } from './hostCapability.js';
4
- 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, FLEET_LOGINS_DENIED, SIGN_IN_PAUSED_DENIED, isClerkOrgId, WORKFLOW_RUNNER_FEATURE, readWorkflowOkReply, readWorkflowResultsListReply, readWorkflowResultsTakeReply, } from '@corenel/protocol';
5
+ import { SIDECAR_PROTOCOL, HOST_METHODS, errMessage, ATTENDANCE_LEAVE_FEATURE, ATTENDANCE_JOIN_FEATURE, RUN_TASK_FEATURE, RUN_PLAN_FEATURE, CREW_DEPLOY_FEATURE, FLEET_ITEMS_FEATURE, FLEET_USE_FEATURE, DEVICES_FEATURE, AGENT_ACCESS_FEATURE, AGENT_RUNS_FEATURE, PEER_LINKS_FEATURE, CHILD_EVENTS_FEATURE, PEER_DAEMON_FEATURE, TEAM_MEMBERS_FEATURE, SPEECH_FEATURE, SIDECAR_KNOWLEDGE_TOOLS, encodePcm16, FLEET_LOGINS_DENIED, SIGN_IN_PAUSED_DENIED, isClerkOrgId, WORKFLOW_RUNNER_FEATURE, readWorkflowOkReply, readWorkflowResultsListReply, readWorkflowResultsTakeReply, } from '@corenel/protocol';
6
+ /** The sidecar knowledge tools a welcome may list, never registered as they are (see the welcome). */
7
+ const SIDECAR_KNOWLEDGE_NAMES = new Set(SIDECAR_KNOWLEDGE_TOOLS);
5
8
  const NO_WORKFLOWS = 'this daemon does not run workflows';
6
9
  const WRONG_REPLY = 'the daemon answered with the wrong reply';
7
10
  /** A reply that failed its check (protocol workflowWire.ts readers). */
@@ -13,6 +16,11 @@ const MALFORMED_REPLY = 'the daemon sent a reply this app cannot read';
13
16
  * slow link. */
14
17
  export const WORKFLOW_REQUEST_TIMEOUT_MS = 60000;
15
18
  export const WORKFLOW_PUT_TIMEOUT_MS = 120000;
19
+ /** After a fleet workflow call is stopped, how long the host waits for the sidecar's answer before it settles the
20
+ * call itself (a lost reply, a hung daemon, a cancel that did not land): Stop never hangs. Shorter than
21
+ * WORKFLOW_REQUEST_TIMEOUT_MS because a person is waiting on Stop. A late answer is dropped. */
22
+ export const FLEET_CALL_STOP_GRACE_MS = 15000;
23
+ export const FLEET_CALL_STOPPED = 'The fleet workflow call was stopped.';
16
24
  const NO_ANSWER = 'the sidecar did not answer in time';
17
25
  /** The longest root or path an `fs-changed` may name; a real one is far shorter. */
18
26
  const MAX_FS_CHANGE_LEN = 4096;
@@ -306,6 +314,55 @@ export class SidecarHost {
306
314
  writable: true,
307
315
  value: new Map()
308
316
  });
317
+ Object.defineProperty(this, "fleetItemsSeq", {
318
+ enumerable: true,
319
+ configurable: true,
320
+ writable: true,
321
+ value: 0
322
+ });
323
+ Object.defineProperty(this, "knowledgeSeq", {
324
+ enumerable: true,
325
+ configurable: true,
326
+ writable: true,
327
+ value: 0
328
+ });
329
+ Object.defineProperty(this, "knowledgePending", {
330
+ enumerable: true,
331
+ configurable: true,
332
+ writable: true,
333
+ value: new Map()
334
+ });
335
+ Object.defineProperty(this, "fleetItemsPending", {
336
+ enumerable: true,
337
+ configurable: true,
338
+ writable: true,
339
+ value: new Map()
340
+ });
341
+ /** One id sequence for the three fleet-use requests; each has its own reply map. */
342
+ Object.defineProperty(this, "fleetUseSeq", {
343
+ enumerable: true,
344
+ configurable: true,
345
+ writable: true,
346
+ value: 0
347
+ });
348
+ Object.defineProperty(this, "fleetSkillCatalogPending", {
349
+ enumerable: true,
350
+ configurable: true,
351
+ writable: true,
352
+ value: new Map()
353
+ });
354
+ Object.defineProperty(this, "fleetWorkflowCatalogPending", {
355
+ enumerable: true,
356
+ configurable: true,
357
+ writable: true,
358
+ value: new Map()
359
+ });
360
+ Object.defineProperty(this, "fleetWorkflowCallPending", {
361
+ enumerable: true,
362
+ configurable: true,
363
+ writable: true,
364
+ value: new Map()
365
+ });
309
366
  Object.defineProperty(this, "crewDeploySeq", {
310
367
  enumerable: true,
311
368
  configurable: true,
@@ -722,6 +779,14 @@ export class SidecarHost {
722
779
  writable: true,
723
780
  value: []
724
781
  });
782
+ /** The sidecar knowledge tools its welcome listed: never registered, called
783
+ * only through invokeKnowledgeTool. */
784
+ Object.defineProperty(this, "knowledgeTools", {
785
+ enumerable: true,
786
+ configurable: true,
787
+ writable: true,
788
+ value: new Set()
789
+ });
725
790
  Object.defineProperty(this, "welcome", {
726
791
  enumerable: true,
727
792
  configurable: true,
@@ -917,6 +982,9 @@ export class SidecarHost {
917
982
  for (const name of this.imported)
918
983
  this.opts.unregisterTool?.(name);
919
984
  this.imported.length = 0;
985
+ // A sidecar_knowledge_* tool listed by this connection's welcome is not
986
+ // listed by the next one until it says so again.
987
+ this.knowledgeTools.clear();
920
988
  for (const p of this.invokePending.values())
921
989
  p.reject(new Error(reason));
922
990
  this.invokePending.clear();
@@ -985,6 +1053,21 @@ export class SidecarHost {
985
1053
  for (const p of this.crewDeployPending.values())
986
1054
  p.reject(new Error(reason));
987
1055
  this.crewDeployPending.clear();
1056
+ for (const p of this.fleetItemsPending.values())
1057
+ p.reject(new Error(reason));
1058
+ this.fleetItemsPending.clear();
1059
+ for (const p of this.knowledgePending.values())
1060
+ p.reject(new Error(reason));
1061
+ this.knowledgePending.clear();
1062
+ for (const p of this.fleetSkillCatalogPending.values())
1063
+ p.reject(new Error(reason));
1064
+ this.fleetSkillCatalogPending.clear();
1065
+ for (const p of this.fleetWorkflowCatalogPending.values())
1066
+ p.reject(new Error(reason));
1067
+ this.fleetWorkflowCatalogPending.clear();
1068
+ for (const p of this.fleetWorkflowCallPending.values())
1069
+ p.reject(new Error(reason));
1070
+ this.fleetWorkflowCallPending.clear();
988
1071
  for (const p of this.workflowsPending.values())
989
1072
  p.reject(new Error(reason));
990
1073
  this.workflowsPending.clear();
@@ -1115,7 +1198,20 @@ export class SidecarHost {
1115
1198
  }
1116
1199
  catch { /* the connection is up either way */ }
1117
1200
  }
1201
+ // Only what THIS welcome lists may be invoked: a later welcome on the
1202
+ // same host (a reconnect) replaces the knowledge tools, never adds to them.
1203
+ this.knowledgeTools.clear();
1118
1204
  for (const decl of m.tools) {
1205
+ /* The sidecar's knowledge tools are never offered to a model as
1206
+ * they are: that would bypass the knowledge gating (the run's host,
1207
+ * its account, its grant) and the knowledge injection thresholds.
1208
+ * The browser offers them as knowledge_* (src/knowledge/hostKnowledge.ts)
1209
+ * and runs them on this machine through invokeKnowledgeTool, so the
1210
+ * output is checked under the knowledge_* name the model called. */
1211
+ if (SIDECAR_KNOWLEDGE_NAMES.has(decl.name)) {
1212
+ this.knowledgeTools.add(decl.name);
1213
+ continue;
1214
+ }
1119
1215
  const tool = { ...decl, run: (args) => this.invoke(decl.name, args) };
1120
1216
  this.opts.registerTool(tool);
1121
1217
  this.imported.push(decl.name);
@@ -1562,7 +1658,11 @@ export class SidecarHost {
1562
1658
  if (!p)
1563
1659
  return;
1564
1660
  this.mcpAdminPending.delete(m.id);
1565
- p.resolve({ ok: true });
1661
+ p.resolve({
1662
+ ok: true,
1663
+ ...(typeof m.reloadError === 'string' ? { reloadError: m.reloadError } : {}),
1664
+ ...(Array.isArray(m.servers) ? { servers: m.servers } : {}),
1665
+ });
1566
1666
  return;
1567
1667
  }
1568
1668
  case 'mcp-admin-error': {
@@ -1575,7 +1675,11 @@ export class SidecarHost {
1575
1675
  this.mcpAdminPending.delete(m.id);
1576
1676
  // `reason` carried through so a caller can tell an ordinary refusal
1577
1677
  // ("this server wants a pasted token") from a real failure.
1578
- p.resolve(m.reason ? { ok: false, error: m.error, reason: m.reason } : { ok: false, error: m.error });
1678
+ p.resolve({
1679
+ ok: false, error: m.error,
1680
+ ...(m.reason ? { reason: m.reason } : {}),
1681
+ ...(m.code ? { code: m.code } : {}),
1682
+ });
1579
1683
  return;
1580
1684
  }
1581
1685
  /* A progress push, not a reply: the request stays pending. Delivered to
@@ -1702,6 +1806,51 @@ export class SidecarHost {
1702
1806
  p.resolve(m.reply);
1703
1807
  return;
1704
1808
  }
1809
+ case 'knowledge-list-result':
1810
+ case 'knowledge-set-result':
1811
+ case 'knowledge-files-result':
1812
+ case 'knowledge-file-result':
1813
+ case 'knowledge-changes-result': {
1814
+ const p = this.knowledgePending.get(m.id);
1815
+ // A reply of another kind under this id answers nothing.
1816
+ if (!p || p.t !== m.t)
1817
+ return;
1818
+ this.knowledgePending.delete(m.id);
1819
+ p.resolve(m);
1820
+ return;
1821
+ }
1822
+ case 'fleet-items-result': {
1823
+ const p = this.fleetItemsPending.get(m.id);
1824
+ if (!p)
1825
+ return;
1826
+ this.fleetItemsPending.delete(m.id);
1827
+ p.resolve(m.reply);
1828
+ return;
1829
+ }
1830
+ case 'fleet-skill-catalog-result': {
1831
+ const p = this.fleetSkillCatalogPending.get(m.id);
1832
+ if (!p)
1833
+ return;
1834
+ this.fleetSkillCatalogPending.delete(m.id);
1835
+ p.resolve(m.reply);
1836
+ return;
1837
+ }
1838
+ case 'fleet-workflow-catalog-result': {
1839
+ const p = this.fleetWorkflowCatalogPending.get(m.id);
1840
+ if (!p)
1841
+ return;
1842
+ this.fleetWorkflowCatalogPending.delete(m.id);
1843
+ p.resolve(m.reply);
1844
+ return;
1845
+ }
1846
+ case 'fleet-workflow-call-result': {
1847
+ const p = this.fleetWorkflowCallPending.get(m.id);
1848
+ if (!p)
1849
+ return;
1850
+ this.fleetWorkflowCallPending.delete(m.id);
1851
+ p.resolve(m.reply);
1852
+ return;
1853
+ }
1705
1854
  case 'workflow-package-result':
1706
1855
  case 'workflow-results-list-result':
1707
1856
  case 'workflow-results-take-result':
@@ -2421,6 +2570,39 @@ export class SidecarHost {
2421
2570
  answerPermission(id, decision, note) {
2422
2571
  this.ch.send({ t: 'permission-answer', id, decision, ...(note ? { note } : {}) });
2423
2572
  }
2573
+ /** Stop attending on this connection while it stays open for this client's
2574
+ * own calls: the daemon sends it no more permission asks, and an ask nobody
2575
+ * else attends gets its unattended policy at once. Sent only to a daemon
2576
+ * that advertises it (an older one would drop it unread); false when not. */
2577
+ leaveAttendance() {
2578
+ if (!this.features.includes(ATTENDANCE_LEAVE_FEATURE))
2579
+ return false;
2580
+ this.ch.send({ t: 'attendance-leave' });
2581
+ return true;
2582
+ }
2583
+ /** Whether this daemon takes `attendance-join`, so a connection that left
2584
+ * attendance can attend again without a new dial. */
2585
+ canRejoinAttendance() {
2586
+ return this.features.includes(ATTENDANCE_LEAVE_FEATURE) && this.features.includes(ATTENDANCE_JOIN_FEATURE);
2587
+ }
2588
+ /** True while an ACP session (its start entry lives as long as the session)
2589
+ * or a dictation stream is open on this connection: its frames keep coming
2590
+ * over this socket and its stop goes through it, so a client must not set
2591
+ * the connection aside unseen while one is. */
2592
+ hasLiveStreams() {
2593
+ return this.acpPending.size > 0 || this.sttStreams.size > 0;
2594
+ }
2595
+ /** Attend again on this connection after `leaveAttendance`: the daemon
2596
+ * replays the asks this connection may see, the registrations, the live
2597
+ * runs and the recently settled task runs, through the usual handlers.
2598
+ * Sent only to a daemon that advertises it; false when not, and the caller
2599
+ * dials a new connection instead. */
2600
+ joinAttendance() {
2601
+ if (!this.canRejoinAttendance())
2602
+ return false;
2603
+ this.ch.send({ t: 'attendance-join' });
2604
+ return true;
2605
+ }
2424
2606
  /** Push one definition file's content to the daemon, to be persisted under
2425
2607
  * this client's own `<stateDir>/hosts/<dirName>/` (Slice 2 host sync).
2426
2608
  * Resolves on `host-sync-result { ok: true }`; rejects with the daemon's
@@ -2584,8 +2766,8 @@ export class SidecarHost {
2584
2766
  /** The MCP servers configured on this machine, with credential PRESENCE.
2585
2767
  * There is no read path for a credential value anywhere in this feature. */
2586
2768
  mcpServers() { return this.sendMcpAdmin({ t: 'mcp-servers-list' }); }
2587
- /** Add a server to this machine's daemon.json. Takes effect on daemon
2588
- * restart -- the config plane cannot reach into a running daemon. */
2769
+ /** Add a server to this machine's daemon.json. Resolves after the daemon
2770
+ * applied it (`reloadError` when it could not yet). */
2589
2771
  mcpAddServer(name, url, hosted = false, authUrl) {
2590
2772
  /* Each field sent only when it has a value, so the message an older daemon
2591
2773
  * receives is byte-identical to what it received before these existed. */
@@ -2622,6 +2804,18 @@ export class SidecarHost {
2622
2804
  mcpSetCredential(name, header, value) {
2623
2805
  return this.sendMcpAdmin({ t: 'mcp-set-credential', name, header, value });
2624
2806
  }
2807
+ /** Whether this daemon takes `mcpSetAllow`. */
2808
+ supportsMcpAllow() { return this.features.includes(MCP_ALLOW_FEATURE); }
2809
+ /** Switch which of server `name`'s tools this machine's `mcpAllow` grants:
2810
+ * exactly `tools`, replacing that server's entries. Empty turns them all
2811
+ * off. Resolves after the daemon applied it. Refused here, sending nothing,
2812
+ * against a daemon without the feature: an older one would never answer. */
2813
+ mcpSetAllow(name, tools) {
2814
+ if (!this.supportsMcpAllow()) {
2815
+ return Promise.resolve({ ok: false, error: 'this daemon cannot switch MCP tools -- update it and reconnect' });
2816
+ }
2817
+ return this.sendMcpAdmin({ t: 'mcp-allow-set', name, tools });
2818
+ }
2625
2819
  sendMcpAdmin(body) {
2626
2820
  const id = ++this.mcpSeq;
2627
2821
  return new Promise((resolve, reject) => {
@@ -2687,6 +2881,134 @@ export class SidecarHost {
2687
2881
  this.ch.send(send(id));
2688
2882
  });
2689
2883
  }
2884
+ /** True when the connected sidecar keeps its own knowledge folders and
2885
+ * answers the knowledge-* messages. */
2886
+ supportsKnowledge() { return this.features.includes(KNOWLEDGE_FEATURE); }
2887
+ /** Send one knowledge request and resolve with the reply of kind `t` under
2888
+ * its id. Against a sidecar without the feature nothing is sent. `ok: false`
2889
+ * is the sidecar's considered answer and resolves; only a dropped connection
2890
+ * rejects. */
2891
+ knowledgeCall(t, request, replyOf, unsupported) {
2892
+ if (!this.supportsKnowledge())
2893
+ return Promise.resolve(unsupported);
2894
+ const id = ++this.knowledgeSeq;
2895
+ return new Promise((resolve, reject) => {
2896
+ this.knowledgePending.set(id, {
2897
+ t,
2898
+ resolve: (m) => {
2899
+ const reply = replyOf(m);
2900
+ if (reply === undefined)
2901
+ reject(new Error(`unexpected ${m.t}`));
2902
+ else
2903
+ resolve(reply);
2904
+ },
2905
+ reject,
2906
+ });
2907
+ this.ch.send(request(id));
2908
+ });
2909
+ }
2910
+ /** Run one of the sidecar's knowledge tools (sidecar_knowledge_*), which the
2911
+ * welcome listed and nothing registered. Any other name, or one the welcome
2912
+ * did not list, is refused without sending. */
2913
+ invokeKnowledgeTool(name, args) {
2914
+ if (!SIDECAR_KNOWLEDGE_NAMES.has(name) || !this.knowledgeTools.has(name)) {
2915
+ return Promise.reject(new Error(`sidecar tool not connected: ${name}`));
2916
+ }
2917
+ return this.invoke(name, args);
2918
+ }
2919
+ /** The connected sidecar's knowledge folders, and whether this connection manages them. */
2920
+ knowledgeList() {
2921
+ return this.knowledgeCall('knowledge-list-result', (id) => ({ t: 'knowledge-list', id }), (m) => (m.t === 'knowledge-list-result' ? m.reply : undefined), { ok: false, error: KNOWLEDGE_NOT_KEPT });
2922
+ }
2923
+ /** Add one folder or remove one by id; answers the new list. Refused by the
2924
+ * sidecar unless this connection manages its folders. */
2925
+ knowledgeSet(change) {
2926
+ return this.knowledgeCall('knowledge-set-result', (id) => ({ t: 'knowledge-set', id, ...change }), (m) => (m.t === 'knowledge-set-result' ? m.reply : undefined), { ok: false, error: KNOWLEDGE_NOT_KEPT });
2927
+ }
2928
+ /** The files in one knowledge folder, by its id. */
2929
+ knowledgeFiles(folder) {
2930
+ return this.knowledgeCall('knowledge-files-result', (id) => ({ t: 'knowledge-files', id, folder }), (m) => (m.t === 'knowledge-files-result' ? m.reply : undefined), { ok: false, error: KNOWLEDGE_NOT_KEPT });
2931
+ }
2932
+ /** One file's text, or with `stat` its size and mtime, in one knowledge folder. */
2933
+ knowledgeFile(folder, path, stat = false) {
2934
+ return this.knowledgeCall('knowledge-file-result', (id) => ({ t: 'knowledge-file', id, folder, path, ...(stat ? { stat: true } : {}) }), (m) => (m.t === 'knowledge-file-result' ? m.reply : undefined), { ok: false, error: KNOWLEDGE_NOT_KEPT });
2935
+ }
2936
+ /** What was written into the sidecar's knowledge folders, newest first. */
2937
+ knowledgeChanges() {
2938
+ return this.knowledgeCall('knowledge-changes-result', (id) => ({ t: 'knowledge-changes', id }), (m) => (m.t === 'knowledge-changes-result' ? m.reply : undefined), { ok: false, error: KNOWLEDGE_NOT_KEPT });
2939
+ }
2940
+ /** True when the connected sidecar lists its fleet items. */
2941
+ supportsFleetItems() { return this.features.includes(FLEET_ITEMS_FEATURE); }
2942
+ /** The fleet agents, skills and workflows installed on the connected
2943
+ * sidecar. `ok: false` is its considered answer and resolves; only a dropped
2944
+ * connection rejects. Against a sidecar without the feature nothing is sent. */
2945
+ fleetItems() {
2946
+ if (!this.supportsFleetItems())
2947
+ return Promise.resolve({ ok: false, error: 'this sidecar does not list fleet items' });
2948
+ const id = ++this.fleetItemsSeq;
2949
+ return new Promise((resolve, reject) => {
2950
+ this.fleetItemsPending.set(id, { resolve, reject });
2951
+ this.ch.send({ t: 'fleet-items', id });
2952
+ });
2953
+ }
2954
+ /** True when the connected sidecar offers its fleet skills and workflows for use. */
2955
+ supportsFleetUse() { return this.features.includes(FLEET_USE_FEATURE); }
2956
+ /** The fleet skills the connected sidecar offers now. `ok: false` is its considered answer and resolves; only
2957
+ * a dropped connection rejects. Against a sidecar without the feature nothing is sent. */
2958
+ fleetSkillCatalog() {
2959
+ if (!this.supportsFleetUse())
2960
+ return Promise.resolve({ ok: false, error: 'this sidecar does not offer fleet items' });
2961
+ const id = ++this.fleetUseSeq;
2962
+ return new Promise((resolve, reject) => {
2963
+ this.fleetSkillCatalogPending.set(id, { resolve, reject });
2964
+ this.ch.send({ t: 'fleet-skill-catalog', id });
2965
+ });
2966
+ }
2967
+ /** The fleet workflows the connected sidecar can run now. Same contract as fleetSkillCatalog. */
2968
+ fleetWorkflowCatalog() {
2969
+ if (!this.supportsFleetUse())
2970
+ return Promise.resolve({ ok: false, error: 'this sidecar does not offer fleet items' });
2971
+ const id = ++this.fleetUseSeq;
2972
+ return new Promise((resolve, reject) => {
2973
+ this.fleetWorkflowCatalogPending.set(id, { resolve, reject });
2974
+ this.ch.send({ t: 'fleet-workflow-catalog', id });
2975
+ });
2976
+ }
2977
+ /** Run one fleet workflow on the connected sidecar and wait for its final text. Aborting `signal` sends
2978
+ * `fleet-workflow-cancel` and waits up to FLEET_CALL_STOP_GRACE_MS for the sidecar's answer (the run settles as
2979
+ * stopped); with none by then the call is forgotten (a late answer is dropped) and resolves FLEET_CALL_STOPPED.
2980
+ * An already-aborted signal sends nothing. `ok: false` resolves; only a dropped connection rejects. */
2981
+ callFleetWorkflow(name, inputs, signal) {
2982
+ if (!this.supportsFleetUse())
2983
+ return Promise.resolve({ ok: false, error: 'this sidecar does not offer fleet items' });
2984
+ if (signal.aborted)
2985
+ return Promise.resolve({ ok: false, error: FLEET_CALL_STOPPED });
2986
+ const id = ++this.fleetUseSeq;
2987
+ return new Promise((resolve, reject) => {
2988
+ let timer;
2989
+ const onAbort = () => {
2990
+ if (!this.fleetWorkflowCallPending.has(id))
2991
+ return;
2992
+ this.ch.send({ t: 'fleet-workflow-cancel', id });
2993
+ timer = setTimeout(() => {
2994
+ if (!this.fleetWorkflowCallPending.delete(id))
2995
+ return;
2996
+ resolve({ ok: false, error: FLEET_CALL_STOPPED });
2997
+ }, FLEET_CALL_STOP_GRACE_MS);
2998
+ };
2999
+ const settle = () => {
3000
+ signal.removeEventListener('abort', onAbort);
3001
+ if (timer !== undefined)
3002
+ clearTimeout(timer);
3003
+ };
3004
+ this.fleetWorkflowCallPending.set(id, {
3005
+ resolve: (r) => { settle(); resolve(r); },
3006
+ reject: (e) => { settle(); reject(e); },
3007
+ });
3008
+ signal.addEventListener('abort', onAbort, { once: true });
3009
+ this.ch.send({ t: 'fleet-workflow-call', id, name, inputs });
3010
+ });
3011
+ }
2690
3012
  /** The machine's deploy list, statuses and audit. */
2691
3013
  crewDeployments() { return this.crewDeployRequest((id) => ({ t: 'crew-deployments', id })); }
2692
3014
  /** Deploy an agent or a workflow. `workspaceId`: where a deployed