@securecoms/agent-bridge 0.17.0 → 0.18.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/bin.js CHANGED
@@ -4834,6 +4834,9 @@ var FRAME_PARSERS = {
4834
4834
  }),
4835
4835
  voice_settings_changed: (d) => ({
4836
4836
  voiceSettingsChanged: { identityId: str(d?.identity_id), version: Number(d?.version ?? 0) }
4837
+ }),
4838
+ runtime_config_changed: (d) => ({
4839
+ runtimeConfigChanged: { identityId: str(d?.identity_id), version: Number(d?.version ?? 0) }
4837
4840
  })
4838
4841
  };
4839
4842
  var S2C_FRAME_NAMES = new Set(Object.keys(FRAME_PARSERS));
@@ -6388,7 +6391,7 @@ var SqliteMlsStateStore = class {
6388
6391
 
6389
6392
  // src/bridge-agent.ts
6390
6393
  import { decryptFile as decryptFile2, encryptFile as encryptFile2, computeFileDigest as computeFileDigest2, base64ToBytes as base64ToBytes4, bytesToBase64 as bytesToBase644 } from "@agentvault/crypto";
6391
- import { createHash } from "node:crypto";
6394
+ import { createHash as createHash2 } from "node:crypto";
6392
6395
 
6393
6396
  // src/service/resolve-bin.ts
6394
6397
  import { join as join2, dirname as dirname2 } from "node:path";
@@ -6441,7 +6444,7 @@ function loadPackageMeta(importMetaUrl) {
6441
6444
 
6442
6445
  // src/bridge-agent.ts
6443
6446
  import { promises as fsp } from "node:fs";
6444
- import { join as join8, dirname as dirname3 } from "node:path";
6447
+ import { join as join9, dirname as dirname3 } from "node:path";
6445
6448
 
6446
6449
  // src/latency.ts
6447
6450
  var DEFAULT_CAPACITY = 1024;
@@ -7461,7 +7464,135 @@ async function runSummarize(opts) {
7461
7464
  }
7462
7465
  }
7463
7466
 
7467
+ // src/service/runtime-config-dir.ts
7468
+ import { createHash, randomUUID } from "node:crypto";
7469
+ import {
7470
+ chmodSync,
7471
+ existsSync,
7472
+ mkdirSync,
7473
+ readFileSync as readFileSync6,
7474
+ renameSync,
7475
+ statSync as statSync3,
7476
+ unlinkSync,
7477
+ writeFileSync
7478
+ } from "node:fs";
7479
+ import { basename as basename2, join as join8 } from "node:path";
7480
+ var MEMORY_FILE = "MEMORY.md";
7481
+ var REAL_FS = {
7482
+ mkdir: (p, mode) => mkdirSync(p, { recursive: true, mode }),
7483
+ chmod: (p, mode) => chmodSync(p, mode),
7484
+ writeFile: (p, data, mode) => writeFileSync(p, data, { mode }),
7485
+ rename: (a, b) => renameSync(a, b),
7486
+ readFile: (p) => readFileSync6(p, "utf8"),
7487
+ unlink: (p) => unlinkSync(p)
7488
+ };
7489
+ function runtimeConfigDir(home, identityId) {
7490
+ return join8(home, ".securecoms", "runtime-config", identityId);
7491
+ }
7492
+ function assertManagedName(name) {
7493
+ if (!name || name.includes("/") || name.includes("\\") || name === "." || name === ".." || name !== basename2(name)) {
7494
+ throw new Error(`refusing managed config file name ${JSON.stringify(name)}: not a plain filename`);
7495
+ }
7496
+ }
7497
+ function atomicWrite(path, body, deps) {
7498
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
7499
+ deps.writeFile(tmp, body, 384);
7500
+ deps.chmod(tmp, 384);
7501
+ try {
7502
+ deps.rename(tmp, path);
7503
+ } catch (err) {
7504
+ try {
7505
+ deps.unlink(tmp);
7506
+ } catch {
7507
+ }
7508
+ throw err;
7509
+ }
7510
+ }
7511
+ function writeManagedFiles(home, identityId, files, deps = REAL_FS) {
7512
+ const entries = Object.entries(files);
7513
+ for (const [name] of entries) assertManagedName(name);
7514
+ const parent2 = join8(home, ".securecoms", "runtime-config");
7515
+ const dir = runtimeConfigDir(home, identityId);
7516
+ for (const d of [join8(home, ".securecoms"), parent2, dir]) {
7517
+ deps.mkdir(d, 448);
7518
+ deps.chmod(d, 448);
7519
+ }
7520
+ for (const [name, body] of entries) atomicWrite(join8(dir, name), body, deps);
7521
+ }
7522
+ function checkConfiguredPaths(skillsJson, statFn = (p) => {
7523
+ const s = statSync3(p);
7524
+ return { mode: s.mode, size: s.size };
7525
+ }) {
7526
+ const paths = [];
7527
+ const endpoints = skillsJson.endpoints ?? {};
7528
+ for (const e of Object.values(endpoints)) if (e?.headersFile) paths.push(e.headersFile);
7529
+ const databases = skillsJson.databases ?? {};
7530
+ for (const d of Object.values(databases)) {
7531
+ if (d?.sqlite) paths.push(d.sqlite);
7532
+ if (d?.postgresUrlFile) paths.push(d.postgresUrlFile);
7533
+ }
7534
+ return paths.map((path) => {
7535
+ let st;
7536
+ try {
7537
+ st = statFn(path);
7538
+ } catch {
7539
+ return { path, ok: false, why: "not found" };
7540
+ }
7541
+ if ((st.mode & 63) !== 0) {
7542
+ return { path, ok: false, why: "readable by other users on this machine \u2014 tighten its permissions to owner-only" };
7543
+ }
7544
+ if (st.size === 0) return { path, ok: false, why: "the file is empty" };
7545
+ return { path, ok: true, why: "" };
7546
+ });
7547
+ }
7548
+ function lineHash(line) {
7549
+ return createHash("sha256").update(line.trim(), "utf8").digest("hex");
7550
+ }
7551
+ var LOCAL_REPORT_FILE = "local-config.json";
7552
+ function readLocalReport(home, identityId) {
7553
+ const p = join8(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
7554
+ if (!existsSync(p)) return null;
7555
+ let doc;
7556
+ try {
7557
+ doc = JSON.parse(readFileSync6(p, "utf8"));
7558
+ } catch {
7559
+ return null;
7560
+ }
7561
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
7562
+ const hasSearchKey = doc.has_search_key === true;
7563
+ const raw = doc.persona;
7564
+ const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
7565
+ Object.entries(raw).filter(([, v]) => typeof v === "string")
7566
+ ) : {};
7567
+ return { persona, has_search_key: hasSearchKey };
7568
+ }
7569
+ function readManagedMemory(home, identityId) {
7570
+ const p = join8(runtimeConfigDir(home, identityId), MEMORY_FILE);
7571
+ if (!existsSync(p)) return null;
7572
+ try {
7573
+ const body = readFileSync6(p, "utf8").trim();
7574
+ return body || null;
7575
+ } catch {
7576
+ return null;
7577
+ }
7578
+ }
7579
+ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
7580
+ const dir = runtimeConfigDir(home, identityId);
7581
+ const path = join8(dir, MEMORY_FILE);
7582
+ if (!existsSync(path)) return 0;
7583
+ const drop = new Set(hashes);
7584
+ const before = deps.readFile(path).split("\n");
7585
+ const kept = before.filter((l) => l.trim() === "" || !drop.has(lineHash(l)));
7586
+ const removed = before.filter((l) => l.trim() !== "" && drop.has(lineHash(l))).length;
7587
+ if (removed === 0) return 0;
7588
+ const body = kept.filter((l, i) => l.trim() !== "" || i < kept.length - 1).join("\n").trimEnd();
7589
+ atomicWrite(path, body ? `${body}
7590
+ ` : "", deps);
7591
+ return removed;
7592
+ }
7593
+
7464
7594
  // src/bridge-agent.ts
7595
+ import { homedir as homedir2 } from "node:os";
7465
7596
  var VOICE_MODE_GUIDANCE = "Reply immediately and conversationally, one short sentence per message; never bundle multiple points into one message.";
7466
7597
  var FAST_PATH_SPOKEN_GUIDANCE = "A one-sentence spoken acknowledgement has already been sent for this turn. Reply only with information it did not contain, in one or two short sentences; if there is nothing to add, send nothing.";
7467
7598
  var ROOM_VOICE_GUIDANCE_MENTION_ONLY = "You are in a group voice room. Reply ONLY when a message @mentions your display name. Keep replies to one short sentence. When you should not reply, send nothing.";
@@ -7516,7 +7647,7 @@ function resolveMessageId(messageId) {
7516
7647
  return messageId;
7517
7648
  }
7518
7649
  function fastReplyId(messageId) {
7519
- const h = createHash("sha256").update(`fast:${messageId}`).digest();
7650
+ const h = createHash2("sha256").update(`fast:${messageId}`).digest();
7520
7651
  h[6] = h[6] & 15 | 64;
7521
7652
  h[8] = h[8] & 63 | 128;
7522
7653
  const x = h.subarray(0, 16).toString("hex");
@@ -7646,6 +7777,7 @@ var BridgeAgent = class _BridgeAgent {
7646
7777
  onSupersedeUnopenable: (channelId, groupId) => this.healSupersedeUnopenable(channelId, groupId)
7647
7778
  });
7648
7779
  this.fetchImpl = deps.fetchImpl ?? fetch;
7780
+ this.home = deps.home ?? homedir2();
7649
7781
  this.injectedFastLlm = !!deps.fastLlm;
7650
7782
  this.fileVoiceConfig = cfg.voice;
7651
7783
  this.fastLlm = deps.fastLlm ?? (cfg.voice?.fastPath.enabled ? new FastLlmClient(cfg.voice.fastPath, this.fetchImpl) : void 0);
@@ -7678,8 +7810,8 @@ var BridgeAgent = class _BridgeAgent {
7678
7810
  const j = await res.json();
7679
7811
  return String(j.attachment_id);
7680
7812
  };
7681
- this.attachmentsDirImpl = deps.attachmentsDir ?? join8(dirname3(cfg.storePath), "attachments");
7682
- this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join8(dirname3(cfg.storePath), "vault-owner-pins.json"));
7813
+ this.attachmentsDirImpl = deps.attachmentsDir ?? join9(dirname3(cfg.storePath), "attachments");
7814
+ this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join9(dirname3(cfg.storePath), "vault-owner-pins.json"));
7683
7815
  this.action = {
7684
7816
  begin: (channelId) => this.actionBegin(channelId),
7685
7817
  start: (opts) => this.actionStart(opts),
@@ -7695,6 +7827,8 @@ var BridgeAgent = class _BridgeAgent {
7695
7827
  // stored for peek-then-join in ingestOnce
7696
7828
  custody;
7697
7829
  fetchImpl;
7830
+ /** Round 5: home for the managed runtime-config directory (test seam). */
7831
+ home;
7698
7832
  joinState = /* @__PURE__ */ new Map();
7699
7833
  /**
7700
7834
  * Phase 2: lazy channel-metadata cache (channel_id → { is_dm, name, at }) used
@@ -8036,6 +8170,84 @@ var BridgeAgent = class _BridgeAgent {
8036
8170
  if (this.voiceSettingsSource === "backend") return;
8037
8171
  process.stderr.write(
8038
8172
  `[bridge] voice settings unavailable (${e.message}); using ${this.voiceSettingsSource}
8173
+ `
8174
+ );
8175
+ }
8176
+ }
8177
+ /**
8178
+ * Fetch this agent's runtime config and apply it to the managed directory the
8179
+ * runtime reads (spec §8). Safe to call any time — at start(), or from the
8180
+ * receive pump on a `runtime_config_changed` ping — and never throws: a save
8181
+ * the owner already made must not be undone by a network blip here, and no
8182
+ * shape the backend can send may take the daemon down.
8183
+ *
8184
+ * Order matters. Prunes are applied BEFORE the memory is read back, so the
8185
+ * report the owner sees next is the file as it now stands.
8186
+ *
8187
+ * Every cycle rewrites, even at an unchanged version: directory permissions
8188
+ * are repaired only on a write, so skipping would leave a loosened directory
8189
+ * loose until the next save.
8190
+ */
8191
+ async refetchRuntimeConfig() {
8192
+ const base = `${this.cfg.apiBase}/api/v1/agents/${encodeURIComponent(this.cfg.identityId)}/runtime-config`;
8193
+ try {
8194
+ const res = await this.fetchImpl(`${base}/files`, {
8195
+ headers: { authorization: `Bearer ${this.cfg.apiKey}` }
8196
+ });
8197
+ if (!res.ok) throw new Error(`runtime-config ${res.status}`);
8198
+ const payload = await res.json();
8199
+ if (!payload || !payload.version) {
8200
+ process.stderr.write(
8201
+ `[bridge] runtime config not applied (version ${JSON.stringify(payload?.version ?? null)} \u2014 no saved config)
8202
+ `
8203
+ );
8204
+ return;
8205
+ }
8206
+ const skillsJson = payload.skills_json ?? {};
8207
+ writeManagedFiles(this.home, this.cfg.identityId, {
8208
+ ...payload.files ?? {},
8209
+ "skills.json": `${JSON.stringify(skillsJson, null, 2)}
8210
+ `
8211
+ });
8212
+ if (payload.memory_prunes?.length) {
8213
+ pruneManagedMemory(this.home, this.cfg.identityId, payload.memory_prunes);
8214
+ }
8215
+ const local = readLocalReport(this.home, this.cfg.identityId);
8216
+ const report = {
8217
+ applied_version: payload.version,
8218
+ paths: checkConfiguredPaths(skillsJson),
8219
+ // Read AFTER the prunes above, so the owner's next view of memory is
8220
+ // the file as it now stands, not the one they just pruned from.
8221
+ memory: readManagedMemory(this.home, this.cfg.identityId),
8222
+ // E13: pass through what the runtime reported. The daemon does not
8223
+ // interpret any of it.
8224
+ local_persona: local?.persona ?? null,
8225
+ // Only the runtime knows whether a search key file is configured — it
8226
+ // is an environment value on that process, invisible to the daemon and
8227
+ // to the backend. The backend needs it to generate a truthful tool
8228
+ // paragraph, so it rides along here.
8229
+ has_search_key: local?.has_search_key ?? false
8230
+ };
8231
+ const posted = await this.fetchImpl(`${base}/report`, {
8232
+ method: "POST",
8233
+ headers: {
8234
+ authorization: `Bearer ${this.cfg.apiKey}`,
8235
+ "content-type": "application/json"
8236
+ },
8237
+ body: JSON.stringify(report)
8238
+ });
8239
+ if (!posted.ok) {
8240
+ process.stderr.write(
8241
+ `[bridge] runtime config v${payload.version} written, but reporting it failed (${posted.status}) \u2014 the console will keep showing it as not applied until the next cycle
8242
+ `
8243
+ );
8244
+ return;
8245
+ }
8246
+ process.stderr.write(`[bridge] runtime config v${payload.version} applied
8247
+ `);
8248
+ } catch (e) {
8249
+ process.stderr.write(
8250
+ `[bridge] runtime config refetch failed: ${e?.message ?? e}
8039
8251
  `
8040
8252
  );
8041
8253
  }
@@ -8067,6 +8279,7 @@ var BridgeAgent = class _BridgeAgent {
8067
8279
  }
8068
8280
  await this.provider.publishKeyPackage();
8069
8281
  await this.refetchVoiceSettings();
8282
+ await this.refetchRuntimeConfig();
8070
8283
  this.startReceivePump();
8071
8284
  if (this.selfHealSweepEnabled) {
8072
8285
  this.bootSweepTimer = setTimeout(
@@ -9551,11 +9764,16 @@ var BridgeAgent = class _BridgeAgent {
9551
9764
  this.pingsReceived++;
9552
9765
  if (ping?.reconnected) {
9553
9766
  process.stderr.write("[bridge] delivery-ping WS (re)connected \u2014 push inbound active\n");
9767
+ await this.refetchRuntimeConfig();
9554
9768
  }
9555
9769
  if (ping?.voiceSettingsChanged) {
9556
9770
  await this.refetchVoiceSettings();
9557
9771
  continue;
9558
9772
  }
9773
+ if (ping?.runtimeConfigChanged) {
9774
+ await this.refetchRuntimeConfig();
9775
+ continue;
9776
+ }
9559
9777
  await this.ingestOnce();
9560
9778
  }
9561
9779
  } catch {
@@ -9994,13 +10212,13 @@ var BridgeAgent = class _BridgeAgent {
9994
10212
 
9995
10213
  // src/auth.ts
9996
10214
  import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
9997
- import { existsSync, readFileSync as readFileSync6, writeFileSync } from "node:fs";
10215
+ import { existsSync as existsSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
9998
10216
  function resolveToken(opts) {
9999
10217
  if (opts.token) return opts.token;
10000
10218
  const f = opts.tokenFile;
10001
- if (f && existsSync(f)) return readFileSync6(f, "utf8").trim();
10219
+ if (f && existsSync2(f)) return readFileSync7(f, "utf8").trim();
10002
10220
  const tok = randomBytes3(32).toString("base64url");
10003
- if (f) writeFileSync(f, tok, { mode: 384 });
10221
+ if (f) writeFileSync2(f, tok, { mode: 384 });
10004
10222
  return tok;
10005
10223
  }
10006
10224
  function tokenMatches(provided, expected) {
@@ -10621,17 +10839,17 @@ async function handleInbound(sock, agent, data, inflight) {
10621
10839
  }
10622
10840
 
10623
10841
  // src/bin.ts
10624
- import { readFileSync as readFileSync8, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, rmSync, existsSync as existsSync2, realpathSync, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
10625
- import { join as join12, dirname as dirname7, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
10842
+ import { readFileSync as readFileSync9, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, rmSync, existsSync as existsSync3, realpathSync, readdirSync as readdirSync2, statSync as statSync4 } from "node:fs";
10843
+ import { join as join13, dirname as dirname7, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
10626
10844
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10627
- import { homedir as homedir2, userInfo } from "node:os";
10845
+ import { homedir as homedir3, userInfo } from "node:os";
10628
10846
  import { spawnSync } from "node:child_process";
10629
10847
 
10630
10848
  // src/service/install.ts
10631
10849
  import { dirname as dirname4 } from "node:path";
10632
10850
 
10633
10851
  // src/service/config.ts
10634
- import { basename as basename2, extname, isAbsolute, resolve } from "node:path";
10852
+ import { basename as basename3, extname, isAbsolute, resolve } from "node:path";
10635
10853
 
10636
10854
  // src/service/resolve-node.ts
10637
10855
  var REQUIRED_NODE_MAJOR = 24;
@@ -10709,7 +10927,7 @@ function deriveServiceConfig(input) {
10709
10927
  `install: refusing to bake a version-embedded Node path into the service definition: ${input.execPath}. Re-run install under a system/Homebrew Node, or pass --exec-path <stable-path>.`
10710
10928
  );
10711
10929
  }
10712
- const stem = input.label ?? basename2(input.credentialPath, extname(input.credentialPath));
10930
+ const stem = input.label ?? basename3(input.credentialPath, extname(input.credentialPath));
10713
10931
  const slug = slugify(stem);
10714
10932
  const cred = isAbsolute(input.credentialPath) ? input.credentialPath : resolve(input.credentialPath);
10715
10933
  const args2 = ["--credential", cred];
@@ -10831,10 +11049,10 @@ function uninstallService(input, deps) {
10831
11049
  }
10832
11050
 
10833
11051
  // src/service/install-prep.ts
10834
- import { join as join10 } from "node:path";
11052
+ import { join as join11 } from "node:path";
10835
11053
 
10836
11054
  // src/service/wire.ts
10837
- import { join as join9, dirname as dirname5 } from "node:path";
11055
+ import { join as join10, dirname as dirname5 } from "node:path";
10838
11056
  import { fileURLToPath } from "node:url";
10839
11057
 
10840
11058
  // src/service/hermes-config.ts
@@ -11010,7 +11228,7 @@ function checkBindingConflict(prev, next, allowRebind) {
11010
11228
  }
11011
11229
  function connectorsSrcDir(importMetaUrl, harness) {
11012
11230
  const pkgRoot = dirname5(dirname5(fileURLToPath(importMetaUrl)));
11013
- return join9(pkgRoot, "connectors", harness);
11231
+ return join10(pkgRoot, "connectors", harness);
11014
11232
  }
11015
11233
  function hermesConfigPath(home) {
11016
11234
  return `${home}/.hermes/config.yaml`;
@@ -11268,10 +11486,10 @@ function assertReadableCredential(absPath, deps) {
11268
11486
  return { deviceId: json2.device_id, identityId: json2.identity_id, issuedAt };
11269
11487
  }
11270
11488
  function durableDir(home, slug) {
11271
- return join10(home, `securecoms-agent-${slug}`);
11489
+ return join11(home, `securecoms-agent-${slug}`);
11272
11490
  }
11273
11491
  function durableCredentialPath(home, slug) {
11274
- return join10(durableDir(home, slug), `${slug}.json`);
11492
+ return join11(durableDir(home, slug), `${slug}.json`);
11275
11493
  }
11276
11494
  function takeCustody(args2, deps) {
11277
11495
  const dir = durableDir(args2.home, args2.slug);
@@ -11318,7 +11536,7 @@ function readSiblingDefs(dir, prefix, deps) {
11318
11536
  for (const n of names) {
11319
11537
  if (!n.startsWith(prefix)) continue;
11320
11538
  try {
11321
- out.push(deps.readFile(join10(dir, n)));
11539
+ out.push(deps.readFile(join11(dir, n)));
11322
11540
  } catch {
11323
11541
  }
11324
11542
  }
@@ -11513,34 +11731,34 @@ function renderStatusTable(rows) {
11513
11731
 
11514
11732
  // src/service/connection-file.ts
11515
11733
  import {
11516
- chmodSync,
11517
- mkdirSync,
11518
- readFileSync as readFileSync7,
11519
- renameSync,
11520
- unlinkSync,
11521
- writeFileSync as writeFileSync2
11734
+ chmodSync as chmodSync2,
11735
+ mkdirSync as mkdirSync2,
11736
+ readFileSync as readFileSync8,
11737
+ renameSync as renameSync2,
11738
+ unlinkSync as unlinkSync2,
11739
+ writeFileSync as writeFileSync3
11522
11740
  } from "node:fs";
11523
- import { randomUUID } from "node:crypto";
11524
- import { join as join11 } from "node:path";
11525
- var REAL_FS = {
11526
- mkdir: (p, mode) => mkdirSync(p, { recursive: true, mode }),
11527
- chmod: (p, mode) => chmodSync(p, mode),
11528
- writeFile: (p, data, mode) => writeFileSync2(p, data, { mode }),
11529
- rename: (a, b) => renameSync(a, b),
11530
- readFile: (p) => readFileSync7(p, "utf8"),
11531
- unlink: (p) => unlinkSync(p)
11741
+ import { randomUUID as randomUUID2 } from "node:crypto";
11742
+ import { join as join12 } from "node:path";
11743
+ var REAL_FS2 = {
11744
+ mkdir: (p, mode) => mkdirSync2(p, { recursive: true, mode }),
11745
+ chmod: (p, mode) => chmodSync2(p, mode),
11746
+ writeFile: (p, data, mode) => writeFileSync3(p, data, { mode }),
11747
+ rename: (a, b) => renameSync2(a, b),
11748
+ readFile: (p) => readFileSync8(p, "utf8"),
11749
+ unlink: (p) => unlinkSync2(p)
11532
11750
  };
11533
11751
  function securecomsDir(home) {
11534
- return join11(home, ".securecoms");
11752
+ return join12(home, ".securecoms");
11535
11753
  }
11536
11754
  function connectionsDir(home) {
11537
- return join11(securecomsDir(home), "connections");
11755
+ return join12(securecomsDir(home), "connections");
11538
11756
  }
11539
11757
  function connectionFilePath(home, identityId) {
11540
- return join11(connectionsDir(home), `${identityId}.json`);
11758
+ return join12(connectionsDir(home), `${identityId}.json`);
11541
11759
  }
11542
11760
  function newInstanceId() {
11543
- return randomUUID();
11761
+ return randomUUID2();
11544
11762
  }
11545
11763
  function buildConnectionFile(args2) {
11546
11764
  return {
@@ -11554,18 +11772,18 @@ function buildConnectionFile(args2) {
11554
11772
  written_at: args2.now
11555
11773
  };
11556
11774
  }
11557
- function writeConnectionFile(home, doc, deps = REAL_FS) {
11775
+ function writeConnectionFile(home, doc, deps = REAL_FS2) {
11558
11776
  deps.mkdir(securecomsDir(home), 448);
11559
11777
  deps.chmod(securecomsDir(home), 448);
11560
11778
  deps.mkdir(connectionsDir(home), 448);
11561
11779
  deps.chmod(connectionsDir(home), 448);
11562
11780
  const path = connectionFilePath(home, doc.identity_id);
11563
- const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
11781
+ const tmp = `${path}.${process.pid}.${randomUUID2()}.tmp`;
11564
11782
  deps.writeFile(tmp, JSON.stringify(doc, null, 2) + "\n", 384);
11565
11783
  deps.chmod(tmp, 384);
11566
11784
  deps.rename(tmp, path);
11567
11785
  }
11568
- function removeConnectionFileIfOwned(home, identityId, instanceId, deps = REAL_FS) {
11786
+ function removeConnectionFileIfOwned(home, identityId, instanceId, deps = REAL_FS2) {
11569
11787
  const path = connectionFilePath(home, identityId);
11570
11788
  try {
11571
11789
  const cur = JSON.parse(deps.readFile(path));
@@ -11606,10 +11824,10 @@ function realServiceDeps() {
11606
11824
  const r = spawnSync(cmd, args2, { encoding: "utf8" });
11607
11825
  return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
11608
11826
  },
11609
- writeFile: (p, data) => writeFileSync3(p, data, { mode: 420 }),
11827
+ writeFile: (p, data) => writeFileSync4(p, data, { mode: 420 }),
11610
11828
  removeFile: (p) => rmSync(p, { force: true }),
11611
- mkdirp: (p) => mkdirSync2(p, { recursive: true }),
11612
- fileExists: (p) => existsSync2(p),
11829
+ mkdirp: (p) => mkdirSync3(p, { recursive: true }),
11830
+ fileExists: (p) => existsSync3(p),
11613
11831
  user: userInfo().username
11614
11832
  };
11615
11833
  }
@@ -11632,7 +11850,7 @@ function realResolveNodeDeps() {
11632
11850
  };
11633
11851
  return {
11634
11852
  realpath: (p) => realpathSync(p),
11635
- fileExists: (p) => existsSync2(p),
11853
+ fileExists: (p) => existsSync3(p),
11636
11854
  nodeMajorOf: majorOf,
11637
11855
  arch: () => process.arch,
11638
11856
  archOf: (p) => {
@@ -11647,17 +11865,17 @@ function realResolveDeps() {
11647
11865
  const r = spawnSync(cmd, args2, { encoding: "utf8" });
11648
11866
  return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
11649
11867
  },
11650
- fileExists: (p) => existsSync2(p)
11868
+ fileExists: (p) => existsSync3(p)
11651
11869
  };
11652
11870
  }
11653
11871
  function realPrepDeps() {
11654
11872
  return {
11655
- readFile: (p) => readFileSync8(p, "utf8"),
11656
- writeFileMode: (p, data, mode) => writeFileSync3(p, data, { mode }),
11873
+ readFile: (p) => readFileSync9(p, "utf8"),
11874
+ writeFileMode: (p, data, mode) => writeFileSync4(p, data, { mode }),
11657
11875
  mkdirp: (p, mode) => {
11658
- mkdirSync2(p, { recursive: true, mode: mode ?? 493 });
11876
+ mkdirSync3(p, { recursive: true, mode: mode ?? 493 });
11659
11877
  },
11660
- fileExists: (p) => existsSync2(p),
11878
+ fileExists: (p) => existsSync3(p),
11661
11879
  realpath: (p) => realpathSync(p),
11662
11880
  listDir: (p) => {
11663
11881
  try {
@@ -11700,7 +11918,7 @@ function realGetHealth(port) {
11700
11918
  }
11701
11919
  function realReadFleet() {
11702
11920
  return readFleet({
11703
- home: homedir2(),
11921
+ home: homedir3(),
11704
11922
  listDir: (p) => {
11705
11923
  try {
11706
11924
  return readdirSync2(p);
@@ -11708,7 +11926,7 @@ function realReadFleet() {
11708
11926
  return [];
11709
11927
  }
11710
11928
  },
11711
- readFile: (p) => readFileSync8(p, "utf8")
11929
+ readFile: (p) => readFileSync9(p, "utf8")
11712
11930
  });
11713
11931
  }
11714
11932
  function runStatus() {
@@ -11730,62 +11948,62 @@ function runStatus() {
11730
11948
  }
11731
11949
  function realCopyConnector(srcDir, dstDir, harness) {
11732
11950
  const allowedExt = CONNECTOR_FILE_EXTENSIONS[harness];
11733
- mkdirSync2(dstDir, { recursive: true });
11951
+ mkdirSync3(dstDir, { recursive: true });
11734
11952
  for (const name of readdirSync2(srcDir)) {
11735
- const srcPath = join12(srcDir, name);
11736
- if (statSync3(srcPath).isDirectory()) {
11953
+ const srcPath = join13(srcDir, name);
11954
+ if (statSync4(srcPath).isDirectory()) {
11737
11955
  if (CONNECTOR_SKIP_DIRS.has(name)) continue;
11738
- realCopyConnector(srcPath, join12(dstDir, name), harness);
11956
+ realCopyConnector(srcPath, join13(dstDir, name), harness);
11739
11957
  continue;
11740
11958
  }
11741
11959
  if (name.includes(".test.")) continue;
11742
11960
  if (!allowedExt.some((ext) => name.endsWith(ext))) continue;
11743
- writeFileSync3(join12(dstDir, name), readFileSync8(srcPath));
11961
+ writeFileSync4(join13(dstDir, name), readFileSync9(srcPath));
11744
11962
  }
11745
11963
  }
11746
11964
  function realWriteBinding(dstDir, identityId) {
11747
- mkdirSync2(dstDir, { recursive: true });
11748
- writeFileSync3(join12(dstDir, "securecoms.binding.json"), JSON.stringify({ identity_id: identityId }) + "\n", { mode: 384 });
11965
+ mkdirSync3(dstDir, { recursive: true });
11966
+ writeFileSync4(join13(dstDir, "securecoms.binding.json"), JSON.stringify({ identity_id: identityId }) + "\n", { mode: 384 });
11749
11967
  }
11750
11968
  function realReadBinding(dstDir) {
11751
11969
  try {
11752
- const parsed = JSON.parse(readFileSync8(join12(dstDir, "securecoms.binding.json"), "utf8"));
11970
+ const parsed = JSON.parse(readFileSync9(join13(dstDir, "securecoms.binding.json"), "utf8"));
11753
11971
  return parsed.identity_id ?? null;
11754
11972
  } catch {
11755
11973
  return null;
11756
11974
  }
11757
11975
  }
11758
11976
  function realRemoveBinding(dstDir) {
11759
- rmSync(join12(dstDir, "securecoms.binding.json"), { force: true });
11977
+ rmSync(join13(dstDir, "securecoms.binding.json"), { force: true });
11760
11978
  }
11761
11979
  function realStatDir(p) {
11762
11980
  try {
11763
- return statSync3(p).isDirectory();
11981
+ return statSync4(p).isDirectory();
11764
11982
  } catch {
11765
11983
  return false;
11766
11984
  }
11767
11985
  }
11768
11986
  function realReadHermesConfig(p) {
11769
11987
  try {
11770
- return readFileSync8(p, "utf8");
11988
+ return readFileSync9(p, "utf8");
11771
11989
  } catch {
11772
11990
  return null;
11773
11991
  }
11774
11992
  }
11775
11993
  function realBackupHermesConfig(p, originalText) {
11776
- writeFileSync3(hermesConfigBackupPath(p, Date.now(), process.pid), originalText, "utf8");
11994
+ writeFileSync4(hermesConfigBackupPath(p, Date.now(), process.pid), originalText, "utf8");
11777
11995
  }
11778
11996
  function realWriteHermesConfig(p, newText) {
11779
- writeFileSync3(p, newText, "utf8");
11997
+ writeFileSync4(p, newText, "utf8");
11780
11998
  }
11781
11999
  function realWireArgv() {
11782
12000
  if (process.argv.includes("--harness")) return process.argv;
11783
- const detected = detectHarness(homedir2(), realStatDir);
12001
+ const detected = detectHarness(homedir3(), realStatDir);
11784
12002
  return detected ? [...process.argv, "--harness", detected] : process.argv;
11785
12003
  }
11786
12004
  function realWireDeps() {
11787
12005
  return {
11788
- home: homedir2(),
12006
+ home: homedir3(),
11789
12007
  argv: realWireArgv(),
11790
12008
  readFleet: realReadFleet,
11791
12009
  copyConnector: realCopyConnector,
@@ -11837,7 +12055,7 @@ async function runServiceVerb(verb) {
11837
12055
  explicitPort: portStr ? Number(portStr) : void 0,
11838
12056
  harness: installHarness,
11839
12057
  allowRebind: process.argv.includes("--rebind"),
11840
- home: homedir2(),
12058
+ home: homedir3(),
11841
12059
  connectorsSrcDir: connectorsSrcDir(import.meta.url, installHarness === "hermes" ? "hermes" : "openclaw"),
11842
12060
  autoWireDeps: {
11843
12061
  statDir: realStatDir,
@@ -11864,7 +12082,7 @@ async function runServiceVerb(verb) {
11864
12082
  label,
11865
12083
  execPath: installExecPath,
11866
12084
  binScript,
11867
- home: homedir2(),
12085
+ home: homedir3(),
11868
12086
  uid: userInfo().uid
11869
12087
  })
11870
12088
  });
@@ -11878,7 +12096,7 @@ async function runServiceVerb(verb) {
11878
12096
  label,
11879
12097
  execPath: process.execPath,
11880
12098
  binScript,
11881
- home: homedir2(),
12099
+ home: homedir3(),
11882
12100
  uid: userInfo().uid
11883
12101
  };
11884
12102
  const r = uninstallService(input, realServiceDeps());
@@ -11929,11 +12147,11 @@ async function main() {
11929
12147
  `);
11930
12148
  process.exit(1);
11931
12149
  }
11932
- const tokenFile = daemon.tokenFile ?? join12(dirname7(credentialPath), "bridge-token");
12150
+ const tokenFile = daemon.tokenFile ?? join13(dirname7(credentialPath), "bridge-token");
11933
12151
  const token = resolveToken({ tokenFile, token: daemon.token });
11934
12152
  const agent = new BridgeAgent(cfg);
11935
12153
  await agent.start();
11936
- const tls = daemon.tls ? { cert: readFileSync8(daemon.tlsCert, "utf8"), key: readFileSync8(daemon.tlsKey, "utf8") } : void 0;
12154
+ const tls = daemon.tls ? { cert: readFileSync9(daemon.tlsCert, "utf8"), key: readFileSync9(daemon.tlsKey, "utf8") } : void 0;
11937
12155
  const { server, closeAllSseStreams } = createHttpServer(agent, { token, allowOrigins: daemon.allowOrigins, tls, websocket: true });
11938
12156
  const agentWs = attachAgentWebSocket(server, agent, { token, allowOrigins: daemon.allowOrigins });
11939
12157
  let down = false;
@@ -11955,7 +12173,7 @@ async function main() {
11955
12173
  const instanceId = newInstanceId();
11956
12174
  const identityId = agent.health().identity_id;
11957
12175
  const slug = slugFromCredentialPath(credentialPath);
11958
- process.once("exit", () => removeConnectionFileIfOwned(homedir2(), identityId, instanceId));
12176
+ process.once("exit", () => removeConnectionFileIfOwned(homedir3(), identityId, instanceId));
11959
12177
  server.listen(daemon.port, daemon.bind, () => {
11960
12178
  const scheme = tls ? "https" : "http";
11961
12179
  const wsScheme = tls ? "wss" : "ws";
@@ -11966,7 +12184,7 @@ async function main() {
11966
12184
  const addr = server.address();
11967
12185
  const boundPort = typeof addr === "object" && addr ? addr.port : daemon.port;
11968
12186
  try {
11969
- writeConnectionFile(homedir2(), buildConnectionFile({
12187
+ writeConnectionFile(homedir3(), buildConnectionFile({
11970
12188
  identityId,
11971
12189
  port: boundPort,
11972
12190
  tokenFile,
@@ -144,6 +144,13 @@ export interface BridgeAgentDeps {
144
144
  transport?: any;
145
145
  backend?: any;
146
146
  fetchImpl?: typeof fetch;
147
+ /**
148
+ * Round 5 (runtime config delivery): the home directory under which the
149
+ * managed runtime-config directory lives. Injected only by tests, so they can
150
+ * write into a temp dir instead of the developer's real ~; production leaves
151
+ * it undefined and gets homedir().
152
+ */
153
+ home?: string;
147
154
  /** Injectable so tests can assert recorded stages; defaults to a fresh one. */
148
155
  latency?: LatencyRecorder;
149
156
  /**
@@ -341,6 +348,8 @@ export declare class BridgeAgent {
341
348
  private readonly backend;
342
349
  private readonly custody;
343
350
  private readonly fetchImpl;
351
+ /** Round 5: home for the managed runtime-config directory (test seam). */
352
+ private readonly home;
344
353
  private readonly joinState;
345
354
  /**
346
355
  * Phase 2: lazy channel-metadata cache (channel_id → { is_dm, name, at }) used
@@ -642,6 +651,21 @@ export declare class BridgeAgent {
642
651
  * `voice_settings_changed` ping); never throws.
643
652
  */
644
653
  refetchVoiceSettings(): Promise<void>;
654
+ /**
655
+ * Fetch this agent's runtime config and apply it to the managed directory the
656
+ * runtime reads (spec §8). Safe to call any time — at start(), or from the
657
+ * receive pump on a `runtime_config_changed` ping — and never throws: a save
658
+ * the owner already made must not be undone by a network blip here, and no
659
+ * shape the backend can send may take the daemon down.
660
+ *
661
+ * Order matters. Prunes are applied BEFORE the memory is read back, so the
662
+ * report the owner sees next is the file as it now stands.
663
+ *
664
+ * Every cycle rewrites, even at an unchanged version: directory permissions
665
+ * are repaired only on a write, so skipping would leave a loosened directory
666
+ * loose until the next save.
667
+ */
668
+ refetchRuntimeConfig(): Promise<void>;
645
669
  /**
646
670
  * Enforce the inbound-buffer cap with a DROP-OLDEST policy.
647
671
  *
package/dist/index.js CHANGED
@@ -4546,6 +4546,9 @@ var FRAME_PARSERS = {
4546
4546
  }),
4547
4547
  voice_settings_changed: (d) => ({
4548
4548
  voiceSettingsChanged: { identityId: str(d?.identity_id), version: Number(d?.version ?? 0) }
4549
+ }),
4550
+ runtime_config_changed: (d) => ({
4551
+ runtimeConfigChanged: { identityId: str(d?.identity_id), version: Number(d?.version ?? 0) }
4549
4552
  })
4550
4553
  };
4551
4554
  var S2C_FRAME_NAMES = new Set(Object.keys(FRAME_PARSERS));
@@ -6100,7 +6103,7 @@ var SqliteMlsStateStore = class {
6100
6103
 
6101
6104
  // src/bridge-agent.ts
6102
6105
  import { decryptFile as decryptFile2, encryptFile as encryptFile2, computeFileDigest as computeFileDigest2, base64ToBytes as base64ToBytes4, bytesToBase64 as bytesToBase644 } from "@agentvault/crypto";
6103
- import { createHash } from "node:crypto";
6106
+ import { createHash as createHash2 } from "node:crypto";
6104
6107
 
6105
6108
  // src/service/resolve-bin.ts
6106
6109
  import { createRequire } from "node:module";
@@ -6112,7 +6115,7 @@ function loadPackageMeta(importMetaUrl) {
6112
6115
 
6113
6116
  // src/bridge-agent.ts
6114
6117
  import { promises as fsp } from "node:fs";
6115
- import { join as join6, dirname } from "node:path";
6118
+ import { join as join7, dirname } from "node:path";
6116
6119
 
6117
6120
  // src/latency.ts
6118
6121
  var DEFAULT_CAPACITY = 1024;
@@ -7308,7 +7311,135 @@ async function runSummarize(opts) {
7308
7311
  }
7309
7312
  }
7310
7313
 
7314
+ // src/service/runtime-config-dir.ts
7315
+ import { createHash, randomUUID } from "node:crypto";
7316
+ import {
7317
+ chmodSync,
7318
+ existsSync,
7319
+ mkdirSync,
7320
+ readFileSync as readFileSync5,
7321
+ renameSync,
7322
+ statSync as statSync2,
7323
+ unlinkSync,
7324
+ writeFileSync
7325
+ } from "node:fs";
7326
+ import { basename as basename2, join as join6 } from "node:path";
7327
+ var MEMORY_FILE = "MEMORY.md";
7328
+ var REAL_FS = {
7329
+ mkdir: (p, mode) => mkdirSync(p, { recursive: true, mode }),
7330
+ chmod: (p, mode) => chmodSync(p, mode),
7331
+ writeFile: (p, data, mode) => writeFileSync(p, data, { mode }),
7332
+ rename: (a, b) => renameSync(a, b),
7333
+ readFile: (p) => readFileSync5(p, "utf8"),
7334
+ unlink: (p) => unlinkSync(p)
7335
+ };
7336
+ function runtimeConfigDir(home, identityId) {
7337
+ return join6(home, ".securecoms", "runtime-config", identityId);
7338
+ }
7339
+ function assertManagedName(name) {
7340
+ if (!name || name.includes("/") || name.includes("\\") || name === "." || name === ".." || name !== basename2(name)) {
7341
+ throw new Error(`refusing managed config file name ${JSON.stringify(name)}: not a plain filename`);
7342
+ }
7343
+ }
7344
+ function atomicWrite(path, body, deps) {
7345
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
7346
+ deps.writeFile(tmp, body, 384);
7347
+ deps.chmod(tmp, 384);
7348
+ try {
7349
+ deps.rename(tmp, path);
7350
+ } catch (err) {
7351
+ try {
7352
+ deps.unlink(tmp);
7353
+ } catch {
7354
+ }
7355
+ throw err;
7356
+ }
7357
+ }
7358
+ function writeManagedFiles(home, identityId, files, deps = REAL_FS) {
7359
+ const entries = Object.entries(files);
7360
+ for (const [name] of entries) assertManagedName(name);
7361
+ const parent2 = join6(home, ".securecoms", "runtime-config");
7362
+ const dir = runtimeConfigDir(home, identityId);
7363
+ for (const d of [join6(home, ".securecoms"), parent2, dir]) {
7364
+ deps.mkdir(d, 448);
7365
+ deps.chmod(d, 448);
7366
+ }
7367
+ for (const [name, body] of entries) atomicWrite(join6(dir, name), body, deps);
7368
+ }
7369
+ function checkConfiguredPaths(skillsJson, statFn = (p) => {
7370
+ const s = statSync2(p);
7371
+ return { mode: s.mode, size: s.size };
7372
+ }) {
7373
+ const paths = [];
7374
+ const endpoints = skillsJson.endpoints ?? {};
7375
+ for (const e of Object.values(endpoints)) if (e?.headersFile) paths.push(e.headersFile);
7376
+ const databases = skillsJson.databases ?? {};
7377
+ for (const d of Object.values(databases)) {
7378
+ if (d?.sqlite) paths.push(d.sqlite);
7379
+ if (d?.postgresUrlFile) paths.push(d.postgresUrlFile);
7380
+ }
7381
+ return paths.map((path) => {
7382
+ let st;
7383
+ try {
7384
+ st = statFn(path);
7385
+ } catch {
7386
+ return { path, ok: false, why: "not found" };
7387
+ }
7388
+ if ((st.mode & 63) !== 0) {
7389
+ return { path, ok: false, why: "readable by other users on this machine \u2014 tighten its permissions to owner-only" };
7390
+ }
7391
+ if (st.size === 0) return { path, ok: false, why: "the file is empty" };
7392
+ return { path, ok: true, why: "" };
7393
+ });
7394
+ }
7395
+ function lineHash(line) {
7396
+ return createHash("sha256").update(line.trim(), "utf8").digest("hex");
7397
+ }
7398
+ var LOCAL_REPORT_FILE = "local-config.json";
7399
+ function readLocalReport(home, identityId) {
7400
+ const p = join6(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
7401
+ if (!existsSync(p)) return null;
7402
+ let doc;
7403
+ try {
7404
+ doc = JSON.parse(readFileSync5(p, "utf8"));
7405
+ } catch {
7406
+ return null;
7407
+ }
7408
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
7409
+ const hasSearchKey = doc.has_search_key === true;
7410
+ const raw = doc.persona;
7411
+ const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
7412
+ Object.entries(raw).filter(([, v]) => typeof v === "string")
7413
+ ) : {};
7414
+ return { persona, has_search_key: hasSearchKey };
7415
+ }
7416
+ function readManagedMemory(home, identityId) {
7417
+ const p = join6(runtimeConfigDir(home, identityId), MEMORY_FILE);
7418
+ if (!existsSync(p)) return null;
7419
+ try {
7420
+ const body = readFileSync5(p, "utf8").trim();
7421
+ return body || null;
7422
+ } catch {
7423
+ return null;
7424
+ }
7425
+ }
7426
+ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
7427
+ const dir = runtimeConfigDir(home, identityId);
7428
+ const path = join6(dir, MEMORY_FILE);
7429
+ if (!existsSync(path)) return 0;
7430
+ const drop = new Set(hashes);
7431
+ const before = deps.readFile(path).split("\n");
7432
+ const kept = before.filter((l) => l.trim() === "" || !drop.has(lineHash(l)));
7433
+ const removed = before.filter((l) => l.trim() !== "" && drop.has(lineHash(l))).length;
7434
+ if (removed === 0) return 0;
7435
+ const body = kept.filter((l, i) => l.trim() !== "" || i < kept.length - 1).join("\n").trimEnd();
7436
+ atomicWrite(path, body ? `${body}
7437
+ ` : "", deps);
7438
+ return removed;
7439
+ }
7440
+
7311
7441
  // src/bridge-agent.ts
7442
+ import { homedir as homedir2 } from "node:os";
7312
7443
  var VOICE_MODE_GUIDANCE = "Reply immediately and conversationally, one short sentence per message; never bundle multiple points into one message.";
7313
7444
  var FAST_PATH_SPOKEN_GUIDANCE = "A one-sentence spoken acknowledgement has already been sent for this turn. Reply only with information it did not contain, in one or two short sentences; if there is nothing to add, send nothing.";
7314
7445
  var ROOM_VOICE_GUIDANCE_MENTION_ONLY = "You are in a group voice room. Reply ONLY when a message @mentions your display name. Keep replies to one short sentence. When you should not reply, send nothing.";
@@ -7363,7 +7494,7 @@ function resolveMessageId(messageId) {
7363
7494
  return messageId;
7364
7495
  }
7365
7496
  function fastReplyId(messageId) {
7366
- const h = createHash("sha256").update(`fast:${messageId}`).digest();
7497
+ const h = createHash2("sha256").update(`fast:${messageId}`).digest();
7367
7498
  h[6] = h[6] & 15 | 64;
7368
7499
  h[8] = h[8] & 63 | 128;
7369
7500
  const x = h.subarray(0, 16).toString("hex");
@@ -7493,6 +7624,7 @@ var BridgeAgent = class _BridgeAgent {
7493
7624
  onSupersedeUnopenable: (channelId, groupId) => this.healSupersedeUnopenable(channelId, groupId)
7494
7625
  });
7495
7626
  this.fetchImpl = deps.fetchImpl ?? fetch;
7627
+ this.home = deps.home ?? homedir2();
7496
7628
  this.injectedFastLlm = !!deps.fastLlm;
7497
7629
  this.fileVoiceConfig = cfg.voice;
7498
7630
  this.fastLlm = deps.fastLlm ?? (cfg.voice?.fastPath.enabled ? new FastLlmClient(cfg.voice.fastPath, this.fetchImpl) : void 0);
@@ -7525,8 +7657,8 @@ var BridgeAgent = class _BridgeAgent {
7525
7657
  const j = await res.json();
7526
7658
  return String(j.attachment_id);
7527
7659
  };
7528
- this.attachmentsDirImpl = deps.attachmentsDir ?? join6(dirname(cfg.storePath), "attachments");
7529
- this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join6(dirname(cfg.storePath), "vault-owner-pins.json"));
7660
+ this.attachmentsDirImpl = deps.attachmentsDir ?? join7(dirname(cfg.storePath), "attachments");
7661
+ this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join7(dirname(cfg.storePath), "vault-owner-pins.json"));
7530
7662
  this.action = {
7531
7663
  begin: (channelId) => this.actionBegin(channelId),
7532
7664
  start: (opts) => this.actionStart(opts),
@@ -7542,6 +7674,8 @@ var BridgeAgent = class _BridgeAgent {
7542
7674
  // stored for peek-then-join in ingestOnce
7543
7675
  custody;
7544
7676
  fetchImpl;
7677
+ /** Round 5: home for the managed runtime-config directory (test seam). */
7678
+ home;
7545
7679
  joinState = /* @__PURE__ */ new Map();
7546
7680
  /**
7547
7681
  * Phase 2: lazy channel-metadata cache (channel_id → { is_dm, name, at }) used
@@ -7883,6 +8017,84 @@ var BridgeAgent = class _BridgeAgent {
7883
8017
  if (this.voiceSettingsSource === "backend") return;
7884
8018
  process.stderr.write(
7885
8019
  `[bridge] voice settings unavailable (${e.message}); using ${this.voiceSettingsSource}
8020
+ `
8021
+ );
8022
+ }
8023
+ }
8024
+ /**
8025
+ * Fetch this agent's runtime config and apply it to the managed directory the
8026
+ * runtime reads (spec §8). Safe to call any time — at start(), or from the
8027
+ * receive pump on a `runtime_config_changed` ping — and never throws: a save
8028
+ * the owner already made must not be undone by a network blip here, and no
8029
+ * shape the backend can send may take the daemon down.
8030
+ *
8031
+ * Order matters. Prunes are applied BEFORE the memory is read back, so the
8032
+ * report the owner sees next is the file as it now stands.
8033
+ *
8034
+ * Every cycle rewrites, even at an unchanged version: directory permissions
8035
+ * are repaired only on a write, so skipping would leave a loosened directory
8036
+ * loose until the next save.
8037
+ */
8038
+ async refetchRuntimeConfig() {
8039
+ const base = `${this.cfg.apiBase}/api/v1/agents/${encodeURIComponent(this.cfg.identityId)}/runtime-config`;
8040
+ try {
8041
+ const res = await this.fetchImpl(`${base}/files`, {
8042
+ headers: { authorization: `Bearer ${this.cfg.apiKey}` }
8043
+ });
8044
+ if (!res.ok) throw new Error(`runtime-config ${res.status}`);
8045
+ const payload = await res.json();
8046
+ if (!payload || !payload.version) {
8047
+ process.stderr.write(
8048
+ `[bridge] runtime config not applied (version ${JSON.stringify(payload?.version ?? null)} \u2014 no saved config)
8049
+ `
8050
+ );
8051
+ return;
8052
+ }
8053
+ const skillsJson = payload.skills_json ?? {};
8054
+ writeManagedFiles(this.home, this.cfg.identityId, {
8055
+ ...payload.files ?? {},
8056
+ "skills.json": `${JSON.stringify(skillsJson, null, 2)}
8057
+ `
8058
+ });
8059
+ if (payload.memory_prunes?.length) {
8060
+ pruneManagedMemory(this.home, this.cfg.identityId, payload.memory_prunes);
8061
+ }
8062
+ const local = readLocalReport(this.home, this.cfg.identityId);
8063
+ const report = {
8064
+ applied_version: payload.version,
8065
+ paths: checkConfiguredPaths(skillsJson),
8066
+ // Read AFTER the prunes above, so the owner's next view of memory is
8067
+ // the file as it now stands, not the one they just pruned from.
8068
+ memory: readManagedMemory(this.home, this.cfg.identityId),
8069
+ // E13: pass through what the runtime reported. The daemon does not
8070
+ // interpret any of it.
8071
+ local_persona: local?.persona ?? null,
8072
+ // Only the runtime knows whether a search key file is configured — it
8073
+ // is an environment value on that process, invisible to the daemon and
8074
+ // to the backend. The backend needs it to generate a truthful tool
8075
+ // paragraph, so it rides along here.
8076
+ has_search_key: local?.has_search_key ?? false
8077
+ };
8078
+ const posted = await this.fetchImpl(`${base}/report`, {
8079
+ method: "POST",
8080
+ headers: {
8081
+ authorization: `Bearer ${this.cfg.apiKey}`,
8082
+ "content-type": "application/json"
8083
+ },
8084
+ body: JSON.stringify(report)
8085
+ });
8086
+ if (!posted.ok) {
8087
+ process.stderr.write(
8088
+ `[bridge] runtime config v${payload.version} written, but reporting it failed (${posted.status}) \u2014 the console will keep showing it as not applied until the next cycle
8089
+ `
8090
+ );
8091
+ return;
8092
+ }
8093
+ process.stderr.write(`[bridge] runtime config v${payload.version} applied
8094
+ `);
8095
+ } catch (e) {
8096
+ process.stderr.write(
8097
+ `[bridge] runtime config refetch failed: ${e?.message ?? e}
7886
8098
  `
7887
8099
  );
7888
8100
  }
@@ -7914,6 +8126,7 @@ var BridgeAgent = class _BridgeAgent {
7914
8126
  }
7915
8127
  await this.provider.publishKeyPackage();
7916
8128
  await this.refetchVoiceSettings();
8129
+ await this.refetchRuntimeConfig();
7917
8130
  this.startReceivePump();
7918
8131
  if (this.selfHealSweepEnabled) {
7919
8132
  this.bootSweepTimer = setTimeout(
@@ -9398,11 +9611,16 @@ var BridgeAgent = class _BridgeAgent {
9398
9611
  this.pingsReceived++;
9399
9612
  if (ping?.reconnected) {
9400
9613
  process.stderr.write("[bridge] delivery-ping WS (re)connected \u2014 push inbound active\n");
9614
+ await this.refetchRuntimeConfig();
9401
9615
  }
9402
9616
  if (ping?.voiceSettingsChanged) {
9403
9617
  await this.refetchVoiceSettings();
9404
9618
  continue;
9405
9619
  }
9620
+ if (ping?.runtimeConfigChanged) {
9621
+ await this.refetchRuntimeConfig();
9622
+ continue;
9623
+ }
9406
9624
  await this.ingestOnce();
9407
9625
  }
9408
9626
  } catch {
@@ -9840,13 +10058,13 @@ var BridgeAgent = class _BridgeAgent {
9840
10058
  };
9841
10059
 
9842
10060
  // src/config.ts
9843
- import { readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
9844
- import { dirname as dirname2, join as join7 } from "node:path";
10061
+ import { readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
10062
+ import { dirname as dirname2, join as join8 } from "node:path";
9845
10063
  var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
9846
10064
  function loadConfig(opts) {
9847
- const raw = readFileSync5(opts.credentialPath, "utf8");
10065
+ const raw = readFileSync6(opts.credentialPath, "utf8");
9848
10066
  try {
9849
- const mode = statSync2(opts.credentialPath).mode & 511;
10067
+ const mode = statSync3(opts.credentialPath).mode & 511;
9850
10068
  if (mode & 63) process.stderr.write(`[bridge] WARNING: credential file ${opts.credentialPath} permission ${mode.toString(8)} is not 0600 \u2014 tighten it (chmod 600).
9851
10069
  `);
9852
10070
  } catch {
@@ -9858,7 +10076,7 @@ function loadConfig(opts) {
9858
10076
  throw new Error("credential file is not valid JSON");
9859
10077
  }
9860
10078
  for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new Error(`credential file missing required field: ${k}`);
9861
- const storePath = opts.storePath ?? join7(dirname2(opts.credentialPath), "bridge-state.db");
10079
+ const storePath = opts.storePath ?? join8(dirname2(opts.credentialPath), "bridge-state.db");
9862
10080
  const agentName = typeof json2.agent_name === "string" && json2.agent_name ? json2.agent_name : typeof json2.display_name === "string" && json2.display_name ? json2.display_name : void 0;
9863
10081
  const rateLimitCapacity = typeof json2.rate_limit_capacity === "number" ? json2.rate_limit_capacity : 20;
9864
10082
  const rateLimitRefillPerMinute = typeof json2.rate_limit_refill_per_minute === "number" ? json2.rate_limit_refill_per_minute : 20;
@@ -9954,13 +10172,13 @@ import { z as z3 } from "zod";
9954
10172
 
9955
10173
  // src/auth.ts
9956
10174
  import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
9957
- import { existsSync, readFileSync as readFileSync6, writeFileSync } from "node:fs";
10175
+ import { existsSync as existsSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
9958
10176
  function resolveToken(opts) {
9959
10177
  if (opts.token) return opts.token;
9960
10178
  const f = opts.tokenFile;
9961
- if (f && existsSync(f)) return readFileSync6(f, "utf8").trim();
10179
+ if (f && existsSync2(f)) return readFileSync7(f, "utf8").trim();
9962
10180
  const tok = randomBytes3(32).toString("base64url");
9963
- if (f) writeFileSync(f, tok, { mode: 384 });
10181
+ if (f) writeFileSync2(f, tok, { mode: 384 });
9964
10182
  return tok;
9965
10183
  }
9966
10184
  function tokenMatches(provided, expected) {
@@ -0,0 +1,44 @@
1
+ export declare const MEMORY_FILE = "MEMORY.md";
2
+ export interface PathCheck {
3
+ path: string;
4
+ ok: boolean;
5
+ why: string;
6
+ }
7
+ export interface FsDeps {
8
+ mkdir: (p: string, mode: number) => void;
9
+ chmod: (p: string, mode: number) => void;
10
+ writeFile: (p: string, data: string, mode: number) => void;
11
+ rename: (a: string, b: string) => void;
12
+ readFile: (p: string) => string;
13
+ unlink: (p: string) => void;
14
+ }
15
+ export declare function runtimeConfigDir(home: string, identityId: string): string;
16
+ export declare function writeManagedFiles(home: string, identityId: string, files: Record<string, string>, deps?: FsDeps): void;
17
+ /** Every path skills.json names, checked on disk. Contents are never read. */
18
+ export declare function checkConfiguredPaths(skillsJson: Record<string, unknown>, statFn?: (p: string) => {
19
+ mode: number;
20
+ size: number;
21
+ }): PathCheck[];
22
+ export declare function lineHash(line: string): string;
23
+ export declare const LOCAL_REPORT_FILE = "local-config.json";
24
+ /**
25
+ * What the runtime says its OWN persona files currently contain (spec E13).
26
+ *
27
+ * The runtime writes this; the daemon only reads it. It exists because the
28
+ * daemon cannot see the runtime's data dir, so without it an owner's
29
+ * hand-written persona would be invisible in the web app and the first save
30
+ * would replace it sight unseen. It is inert: nothing here decides behaviour,
31
+ * and it never affects which file the runtime actually reads.
32
+ */
33
+ export interface LocalReport {
34
+ persona: Record<string, string>;
35
+ has_search_key: boolean;
36
+ }
37
+ export declare function readLocalReport(home: string, identityId: string): LocalReport | null;
38
+ export declare function readManagedMemory(home: string, identityId: string): string | null;
39
+ /**
40
+ * Remove exactly the named lines. Line-level, never a whole-file replace: the
41
+ * agent may append a fact between the owner reading and pruning, and that fact
42
+ * must survive (spec §7).
43
+ */
44
+ export declare function pruneManagedMemory(home: string, identityId: string, hashes: string[], deps?: FsDeps): number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securecoms/agent-bridge",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",