@agentconnect.md/daemon 1.0.0-rc.54 → 1.0.0-rc.55

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/index.js CHANGED
@@ -17551,7 +17551,10 @@ const AgentScopeDenied = object({
17551
17551
  *
17552
17552
  * SECURITY: `integration/upsert` and `RegisterOk.integrations[]` carry PLAINTEXT
17553
17553
  * platform tokens (botToken/appToken). These payloads MUST NEVER be logged — no
17554
- * body dump on decode error, no register/ok snapshot debug dump.
17554
+ * body dump on decode error, no register/ok snapshot debug dump. The daemon
17555
+ * persists them into the owning agent's local `agent.json` (same trust boundary
17556
+ * as hand-authored agents, which already keep tokens there) so integrations
17557
+ * survive a restart with the CP down.
17555
17558
  *
17556
17559
  * `signingSecret` is intentionally absent: Socket Mode authenticates with the
17557
17560
  * app-level token, so the daemon never needs a signing secret.
@@ -82470,7 +82473,23 @@ var CpAgentRegistry = class {
82470
82473
  }
82471
82474
  };
82472
82475
  //#endregion
82473
- //#region src/cp/cp-integration-registry.ts
82476
+ //#region src/agents/write-integration.ts
82477
+ /**
82478
+ * Persist CP-owned platform integrations onto the owning agent's on-disk
82479
+ * `agent.json`, which is the SINGLE SOURCE OF TRUTH — so integrations (and their
82480
+ * Slack tokens) survive a daemon restart with the Control Plane down (graceful
82481
+ * degradation: the daemon opens its Socket Mode connections from disk at start).
82482
+ *
82483
+ * Local hand-authored agents already store Slack tokens in `agent.json`
82484
+ * (agent-schema.ts SlackConfigSchema), so this adds no new secret-on-disk
82485
+ * concept. Tokens on disk are the same trust boundary as the daemon process;
82486
+ * they still MUST NEVER be logged.
82487
+ *
82488
+ * CRITICAL: like write-agent.ts, we operate on the RAW JSON TEXT (readFileSync →
82489
+ * JSON.parse → mutate only `integrations[]` → writeFileSync). We never serialize
82490
+ * the parsed/interpolated `LoadedAgent` — that has `${VAR}` already substituted
82491
+ * and `workspace.path` rewritten absolute. Editing the raw file preserves both.
82492
+ */
82474
82493
  /** Map the wire spec to the daemon's `Integration` shape (agent-schema). */
82475
82494
  function toIntegration(spec) {
82476
82495
  return {
@@ -82485,42 +82504,82 @@ function toIntegration(spec) {
82485
82504
  }
82486
82505
  };
82487
82506
  }
82507
+ /**
82508
+ * Upsert one integration into the owning agent's `agent.json` `integrations[]`
82509
+ * (replace by `integrationId`, else append). The owning agent is located by its
82510
+ * INTERNAL id (any directory layout). Returns false (with a warn) when no agent
82511
+ * with `spec.agentId` exists on disk — an orphan integration is not persisted;
82512
+ * it self-heals when the agent spec arrives and the CP re-sends (register/ok).
82513
+ */
82514
+ function writeIntegrationSpec(agentsDir, spec, deps) {
82515
+ const file = findAgentFileById(agentsDir, spec.agentId);
82516
+ if (!file) {
82517
+ deps.warn?.(`cp: integration ${spec.integrationId} references missing agent ${spec.agentId} — not persisted (will retry on next push)`);
82518
+ return false;
82519
+ }
82520
+ const raw = JSON.parse(readFileSync(file, "utf8"));
82521
+ const list = Array.isArray(raw.integrations) ? raw.integrations : [];
82522
+ const next = toIntegration(spec);
82523
+ const idx = list.findIndex((i) => typeof i === "object" && i !== null && i.id === next.id);
82524
+ if (idx >= 0) list[idx] = next;
82525
+ else list.push(next);
82526
+ raw.integrations = list;
82527
+ writeFileSync(file, JSON.stringify(raw, null, 2) + "\n");
82528
+ return true;
82529
+ }
82530
+ /**
82531
+ * Remove one integration (by id) from whichever agent.json holds it. Scans all
82532
+ * agent files since the owning agent is not part of `integration/remove`.
82533
+ * Returns whether an entry was found and removed. Unparseable files are skipped.
82534
+ */
82535
+ function removeIntegration(agentsDir, integrationId) {
82536
+ for (const file of findAgentFiles(agentsDir)) {
82537
+ let raw;
82538
+ try {
82539
+ raw = JSON.parse(readFileSync(file, "utf8"));
82540
+ } catch {
82541
+ continue;
82542
+ }
82543
+ if (!Array.isArray(raw.integrations)) continue;
82544
+ const list = raw.integrations;
82545
+ const idx = list.findIndex((i) => typeof i === "object" && i !== null && i.id === integrationId);
82546
+ if (idx === -1) continue;
82547
+ list.splice(idx, 1);
82548
+ writeFileSync(file, JSON.stringify(raw, null, 2) + "\n");
82549
+ return true;
82550
+ }
82551
+ return false;
82552
+ }
82553
+ //#endregion
82554
+ //#region src/cp/cp-integration-registry.ts
82488
82555
  var CpIntegrationRegistry = class {
82556
+ agentsDir;
82557
+ deps;
82489
82558
  onChange;
82490
- byId = /* @__PURE__ */ new Map();
82491
- constructor(onChange) {
82559
+ constructor(agentsDir, deps, onChange) {
82560
+ this.agentsDir = agentsDir;
82561
+ this.deps = deps;
82492
82562
  this.onChange = onChange;
82493
82563
  }
82494
- /** Add or replace one CP integration (integration/upsert EVT). */
82564
+ /** Add or replace one integration on the owning agent's disk file (integration/upsert EVT). */
82495
82565
  upsert(spec) {
82496
- this.byId.set(spec.integrationId, spec);
82566
+ writeIntegrationSpec(this.agentsDir, spec, this.deps);
82497
82567
  this.onChange();
82498
82568
  }
82499
- /** Drop one CP integration (integration/remove EVT). */
82569
+ /** Splice one integration out of whichever agent.json holds it (integration/remove EVT). */
82500
82570
  remove(integrationId) {
82501
- if (this.byId.delete(integrationId)) this.onChange();
82571
+ removeIntegration(this.agentsDir, integrationId);
82572
+ this.onChange();
82502
82573
  }
82503
82574
  /**
82504
- * Replace the whole CP-owned set from the `register/ok` reconcile roster (the CP
82505
- * is authoritative and the roster is already filtered to THIS daemon).
82575
+ * Apply the register/ok reconcile roster: upsert EACH entry and NOTHING else.
82576
+ * Never prunes integrations absent from the roster (deletion is only via
82577
+ * integration/remove).
82506
82578
  */
82507
82579
  converge(specs) {
82508
- this.byId = new Map((specs ?? []).map((s) => [s.integrationId, s]));
82580
+ for (const spec of specs ?? []) writeIntegrationSpec(this.agentsDir, spec, this.deps);
82509
82581
  this.onChange();
82510
82582
  }
82511
- /** CP integrations grouped by owning agentId, as daemon `Integration[]`. */
82512
- byAgent() {
82513
- const out = /* @__PURE__ */ new Map();
82514
- for (const spec of this.byId.values()) {
82515
- const list = out.get(spec.agentId) ?? [];
82516
- list.push(toIntegration(spec));
82517
- out.set(spec.agentId, list);
82518
- }
82519
- return out;
82520
- }
82521
- get size() {
82522
- return this.byId.size;
82523
- }
82524
82583
  };
82525
82584
  //#endregion
82526
82585
  //#region src/cp/config-apply.ts
@@ -82664,7 +82723,6 @@ var Daemon = class Daemon {
82664
82723
  cpCrons;
82665
82724
  cpAgents;
82666
82725
  cpIntegrations;
82667
- warnedOrphanIntegrations = /* @__PURE__ */ new Set();
82668
82726
  botUserIds = {};
82669
82727
  cpRouting;
82670
82728
  clock;
@@ -82727,7 +82785,7 @@ var Daemon = class Daemon {
82727
82785
  knownRuntimes: Object.keys(this.runtimes),
82728
82786
  warn: (m) => this.log.warn(m)
82729
82787
  }, () => void this.reconcile().catch((err) => this.log.error(`cp: agent reconcile failed: ${err.stack ?? err}`)));
82730
- this.cpIntegrations = new CpIntegrationRegistry(() => void this.reconcile().catch((err) => this.log.error(`cp: integration reconcile failed: ${err.stack ?? err}`)));
82788
+ this.cpIntegrations = new CpIntegrationRegistry(this.agentsDir, { warn: (m) => this.log.warn(m) }, () => void this.reconcile().catch((err) => this.log.error(`cp: integration reconcile failed: ${err.stack ?? err}`)));
82731
82789
  for (const a of this.effectiveAgents()) {
82732
82790
  this.agents.set(a.id, a);
82733
82791
  this.prefetchClone(a);
@@ -82835,41 +82893,7 @@ var Daemon = class Daemon {
82835
82893
  * routing (all of which read `agent.integrations`) see one merged view.
82836
82894
  */
82837
82895
  effectiveAgents() {
82838
- const files = [...this.fileAgents.values()];
82839
- const cpByAgent = this.cpIntegrations?.byAgent();
82840
- if (!cpByAgent || cpByAgent.size === 0) return files;
82841
- return files.map((a) => {
82842
- const cp = cpByAgent.get(a.id);
82843
- if (!cp || cp.length === 0) return a;
82844
- const byId = new Map(a.integrations.map((i) => [i.id, i]));
82845
- for (const i of cp) byId.set(i.id, i);
82846
- return {
82847
- ...a,
82848
- integrations: [...byId.values()]
82849
- };
82850
- });
82851
- }
82852
- /**
82853
- * Warn (once per orphan) about CP integrations whose owning agent is absent /
82854
- * inactive on disk: they are held in the registry but overlaid onto no agent, so
82855
- * no socket opens. Self-heals when the agent appears (a later reconcile connects
82856
- * it). Logs integrationId + agentId ONLY — never the token material.
82857
- */
82858
- warnOrphanIntegrations() {
82859
- const cpByAgent = this.cpIntegrations?.byAgent();
82860
- if (!cpByAgent) return;
82861
- const live = /* @__PURE__ */ new Set();
82862
- for (const [agentId, ints] of cpByAgent) {
82863
- if (this.fileAgents.has(agentId)) continue;
82864
- for (const int of ints) {
82865
- live.add(int.id);
82866
- if (!this.warnedOrphanIntegrations.has(int.id)) {
82867
- this.warnedOrphanIntegrations.add(int.id);
82868
- this.log.warn(`slack: integration ${int.id} references missing/inactive agent ${agentId} — not connected`);
82869
- }
82870
- }
82871
- }
82872
- for (const id of this.warnedOrphanIntegrations) if (!live.has(id)) this.warnedOrphanIntegrations.delete(id);
82896
+ return [...this.fileAgents.values()];
82873
82897
  }
82874
82898
  /**
82875
82899
  * Converge the running set to the desired effective agents. Driven by both the
@@ -82903,7 +82927,6 @@ var Daemon = class Daemon {
82903
82927
  async runReconcile() {
82904
82928
  const files = this.loadAgentList();
82905
82929
  this.fileAgents = new Map(files.map((a) => [a.id, a]));
82906
- this.warnOrphanIntegrations();
82907
82930
  const desired = this.effectiveAgents();
82908
82931
  const { toStart, toStop, toChange } = diffAgents(desired, this.agents);
82909
82932
  if (toStart.length || toStop.length || toChange.length) this.log.info(`reconcile: ${desired.length} desired agent(s) from ${this.agentsDir}; start=[${toStart.map((a) => a.id).join(", ")}] stop=[${toStop.join(", ")}] change=[${toChange.map((c) => {