@securecoms/agent-bridge 0.17.0 → 0.19.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,137 @@ 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
+ const search = skillsJson.search;
7535
+ if (search?.keyFile) paths.push(search.keyFile);
7536
+ return paths.map((path) => {
7537
+ let st;
7538
+ try {
7539
+ st = statFn(path);
7540
+ } catch {
7541
+ return { path, ok: false, why: "not found" };
7542
+ }
7543
+ if ((st.mode & 63) !== 0) {
7544
+ return { path, ok: false, why: "readable by other users on this machine \u2014 tighten its permissions to owner-only" };
7545
+ }
7546
+ if (st.size === 0) return { path, ok: false, why: "the file is empty" };
7547
+ return { path, ok: true, why: "" };
7548
+ });
7549
+ }
7550
+ function lineHash(line) {
7551
+ return createHash("sha256").update(line.trim(), "utf8").digest("hex");
7552
+ }
7553
+ var LOCAL_REPORT_FILE = "local-config.json";
7554
+ function readLocalReport(home, identityId) {
7555
+ const p = join8(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
7556
+ if (!existsSync(p)) return null;
7557
+ let doc;
7558
+ try {
7559
+ doc = JSON.parse(readFileSync6(p, "utf8"));
7560
+ } catch {
7561
+ return null;
7562
+ }
7563
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
7564
+ const hasSearchKey = doc.has_search_key === true;
7565
+ const raw = doc.persona;
7566
+ const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
7567
+ Object.entries(raw).filter(([, v]) => typeof v === "string")
7568
+ ) : {};
7569
+ return { persona, has_search_key: hasSearchKey };
7570
+ }
7571
+ function readManagedMemory(home, identityId) {
7572
+ const p = join8(runtimeConfigDir(home, identityId), MEMORY_FILE);
7573
+ if (!existsSync(p)) return null;
7574
+ try {
7575
+ const body = readFileSync6(p, "utf8").trim();
7576
+ return body || null;
7577
+ } catch {
7578
+ return null;
7579
+ }
7580
+ }
7581
+ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
7582
+ const dir = runtimeConfigDir(home, identityId);
7583
+ const path = join8(dir, MEMORY_FILE);
7584
+ if (!existsSync(path)) return 0;
7585
+ const drop = new Set(hashes);
7586
+ const before = deps.readFile(path).split("\n");
7587
+ const kept = before.filter((l) => l.trim() === "" || !drop.has(lineHash(l)));
7588
+ const removed = before.filter((l) => l.trim() !== "" && drop.has(lineHash(l))).length;
7589
+ if (removed === 0) return 0;
7590
+ const body = kept.filter((l, i) => l.trim() !== "" || i < kept.length - 1).join("\n").trimEnd();
7591
+ atomicWrite(path, body ? `${body}
7592
+ ` : "", deps);
7593
+ return removed;
7594
+ }
7595
+
7464
7596
  // src/bridge-agent.ts
7597
+ import { homedir as homedir2 } from "node:os";
7465
7598
  var VOICE_MODE_GUIDANCE = "Reply immediately and conversationally, one short sentence per message; never bundle multiple points into one message.";
7466
7599
  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
7600
  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 +7649,7 @@ function resolveMessageId(messageId) {
7516
7649
  return messageId;
7517
7650
  }
7518
7651
  function fastReplyId(messageId) {
7519
- const h = createHash("sha256").update(`fast:${messageId}`).digest();
7652
+ const h = createHash2("sha256").update(`fast:${messageId}`).digest();
7520
7653
  h[6] = h[6] & 15 | 64;
7521
7654
  h[8] = h[8] & 63 | 128;
7522
7655
  const x = h.subarray(0, 16).toString("hex");
@@ -7646,6 +7779,7 @@ var BridgeAgent = class _BridgeAgent {
7646
7779
  onSupersedeUnopenable: (channelId, groupId) => this.healSupersedeUnopenable(channelId, groupId)
7647
7780
  });
7648
7781
  this.fetchImpl = deps.fetchImpl ?? fetch;
7782
+ this.home = deps.home ?? homedir2();
7649
7783
  this.injectedFastLlm = !!deps.fastLlm;
7650
7784
  this.fileVoiceConfig = cfg.voice;
7651
7785
  this.fastLlm = deps.fastLlm ?? (cfg.voice?.fastPath.enabled ? new FastLlmClient(cfg.voice.fastPath, this.fetchImpl) : void 0);
@@ -7678,8 +7812,8 @@ var BridgeAgent = class _BridgeAgent {
7678
7812
  const j = await res.json();
7679
7813
  return String(j.attachment_id);
7680
7814
  };
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"));
7815
+ this.attachmentsDirImpl = deps.attachmentsDir ?? join9(dirname3(cfg.storePath), "attachments");
7816
+ this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join9(dirname3(cfg.storePath), "vault-owner-pins.json"));
7683
7817
  this.action = {
7684
7818
  begin: (channelId) => this.actionBegin(channelId),
7685
7819
  start: (opts) => this.actionStart(opts),
@@ -7695,6 +7829,8 @@ var BridgeAgent = class _BridgeAgent {
7695
7829
  // stored for peek-then-join in ingestOnce
7696
7830
  custody;
7697
7831
  fetchImpl;
7832
+ /** Round 5: home for the managed runtime-config directory (test seam). */
7833
+ home;
7698
7834
  joinState = /* @__PURE__ */ new Map();
7699
7835
  /**
7700
7836
  * Phase 2: lazy channel-metadata cache (channel_id → { is_dm, name, at }) used
@@ -8036,6 +8172,84 @@ var BridgeAgent = class _BridgeAgent {
8036
8172
  if (this.voiceSettingsSource === "backend") return;
8037
8173
  process.stderr.write(
8038
8174
  `[bridge] voice settings unavailable (${e.message}); using ${this.voiceSettingsSource}
8175
+ `
8176
+ );
8177
+ }
8178
+ }
8179
+ /**
8180
+ * Fetch this agent's runtime config and apply it to the managed directory the
8181
+ * runtime reads (spec §8). Safe to call any time — at start(), or from the
8182
+ * receive pump on a `runtime_config_changed` ping — and never throws: a save
8183
+ * the owner already made must not be undone by a network blip here, and no
8184
+ * shape the backend can send may take the daemon down.
8185
+ *
8186
+ * Order matters. Prunes are applied BEFORE the memory is read back, so the
8187
+ * report the owner sees next is the file as it now stands.
8188
+ *
8189
+ * Every cycle rewrites, even at an unchanged version: directory permissions
8190
+ * are repaired only on a write, so skipping would leave a loosened directory
8191
+ * loose until the next save.
8192
+ */
8193
+ async refetchRuntimeConfig() {
8194
+ const base = `${this.cfg.apiBase}/api/v1/agents/${encodeURIComponent(this.cfg.identityId)}/runtime-config`;
8195
+ try {
8196
+ const res = await this.fetchImpl(`${base}/files`, {
8197
+ headers: { authorization: `Bearer ${this.cfg.apiKey}` }
8198
+ });
8199
+ if (!res.ok) throw new Error(`runtime-config ${res.status}`);
8200
+ const payload = await res.json();
8201
+ if (!payload || !payload.version) {
8202
+ process.stderr.write(
8203
+ `[bridge] runtime config not applied (version ${JSON.stringify(payload?.version ?? null)} \u2014 no saved config)
8204
+ `
8205
+ );
8206
+ return;
8207
+ }
8208
+ const skillsJson = payload.skills_json ?? {};
8209
+ writeManagedFiles(this.home, this.cfg.identityId, {
8210
+ ...payload.files ?? {},
8211
+ "skills.json": `${JSON.stringify(skillsJson, null, 2)}
8212
+ `
8213
+ });
8214
+ if (payload.memory_prunes?.length) {
8215
+ pruneManagedMemory(this.home, this.cfg.identityId, payload.memory_prunes);
8216
+ }
8217
+ const local = readLocalReport(this.home, this.cfg.identityId);
8218
+ const report = {
8219
+ applied_version: payload.version,
8220
+ paths: checkConfiguredPaths(skillsJson),
8221
+ // Read AFTER the prunes above, so the owner's next view of memory is
8222
+ // the file as it now stands, not the one they just pruned from.
8223
+ memory: readManagedMemory(this.home, this.cfg.identityId),
8224
+ // E13: pass through what the runtime reported. The daemon does not
8225
+ // interpret any of it.
8226
+ local_persona: local?.persona ?? null,
8227
+ // Only the runtime knows whether a search key file is configured — it
8228
+ // is an environment value on that process, invisible to the daemon and
8229
+ // to the backend. The backend needs it to generate a truthful tool
8230
+ // paragraph, so it rides along here.
8231
+ has_search_key: local?.has_search_key ?? false
8232
+ };
8233
+ const posted = await this.fetchImpl(`${base}/report`, {
8234
+ method: "POST",
8235
+ headers: {
8236
+ authorization: `Bearer ${this.cfg.apiKey}`,
8237
+ "content-type": "application/json"
8238
+ },
8239
+ body: JSON.stringify(report)
8240
+ });
8241
+ if (!posted.ok) {
8242
+ process.stderr.write(
8243
+ `[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
8244
+ `
8245
+ );
8246
+ return;
8247
+ }
8248
+ process.stderr.write(`[bridge] runtime config v${payload.version} applied
8249
+ `);
8250
+ } catch (e) {
8251
+ process.stderr.write(
8252
+ `[bridge] runtime config refetch failed: ${e?.message ?? e}
8039
8253
  `
8040
8254
  );
8041
8255
  }
@@ -8067,6 +8281,7 @@ var BridgeAgent = class _BridgeAgent {
8067
8281
  }
8068
8282
  await this.provider.publishKeyPackage();
8069
8283
  await this.refetchVoiceSettings();
8284
+ await this.refetchRuntimeConfig();
8070
8285
  this.startReceivePump();
8071
8286
  if (this.selfHealSweepEnabled) {
8072
8287
  this.bootSweepTimer = setTimeout(
@@ -9551,11 +9766,16 @@ var BridgeAgent = class _BridgeAgent {
9551
9766
  this.pingsReceived++;
9552
9767
  if (ping?.reconnected) {
9553
9768
  process.stderr.write("[bridge] delivery-ping WS (re)connected \u2014 push inbound active\n");
9769
+ await this.refetchRuntimeConfig();
9554
9770
  }
9555
9771
  if (ping?.voiceSettingsChanged) {
9556
9772
  await this.refetchVoiceSettings();
9557
9773
  continue;
9558
9774
  }
9775
+ if (ping?.runtimeConfigChanged) {
9776
+ await this.refetchRuntimeConfig();
9777
+ continue;
9778
+ }
9559
9779
  await this.ingestOnce();
9560
9780
  }
9561
9781
  } catch {
@@ -9994,13 +10214,13 @@ var BridgeAgent = class _BridgeAgent {
9994
10214
 
9995
10215
  // src/auth.ts
9996
10216
  import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
9997
- import { existsSync, readFileSync as readFileSync6, writeFileSync } from "node:fs";
10217
+ import { existsSync as existsSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
9998
10218
  function resolveToken(opts) {
9999
10219
  if (opts.token) return opts.token;
10000
10220
  const f = opts.tokenFile;
10001
- if (f && existsSync(f)) return readFileSync6(f, "utf8").trim();
10221
+ if (f && existsSync2(f)) return readFileSync7(f, "utf8").trim();
10002
10222
  const tok = randomBytes3(32).toString("base64url");
10003
- if (f) writeFileSync(f, tok, { mode: 384 });
10223
+ if (f) writeFileSync2(f, tok, { mode: 384 });
10004
10224
  return tok;
10005
10225
  }
10006
10226
  function tokenMatches(provided, expected) {
@@ -10621,17 +10841,17 @@ async function handleInbound(sock, agent, data, inflight) {
10621
10841
  }
10622
10842
 
10623
10843
  // 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";
10844
+ import { readFileSync as readFileSync9, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, rmSync, existsSync as existsSync3, realpathSync, readdirSync as readdirSync2, statSync as statSync4 } from "node:fs";
10845
+ import { join as join13, dirname as dirname7, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
10626
10846
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10627
- import { homedir as homedir2, userInfo } from "node:os";
10847
+ import { homedir as homedir3, userInfo } from "node:os";
10628
10848
  import { spawnSync } from "node:child_process";
10629
10849
 
10630
10850
  // src/service/install.ts
10631
10851
  import { dirname as dirname4 } from "node:path";
10632
10852
 
10633
10853
  // src/service/config.ts
10634
- import { basename as basename2, extname, isAbsolute, resolve } from "node:path";
10854
+ import { basename as basename3, extname, isAbsolute, resolve } from "node:path";
10635
10855
 
10636
10856
  // src/service/resolve-node.ts
10637
10857
  var REQUIRED_NODE_MAJOR = 24;
@@ -10709,7 +10929,7 @@ function deriveServiceConfig(input) {
10709
10929
  `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
10930
  );
10711
10931
  }
10712
- const stem = input.label ?? basename2(input.credentialPath, extname(input.credentialPath));
10932
+ const stem = input.label ?? basename3(input.credentialPath, extname(input.credentialPath));
10713
10933
  const slug = slugify(stem);
10714
10934
  const cred = isAbsolute(input.credentialPath) ? input.credentialPath : resolve(input.credentialPath);
10715
10935
  const args2 = ["--credential", cred];
@@ -10831,10 +11051,10 @@ function uninstallService(input, deps) {
10831
11051
  }
10832
11052
 
10833
11053
  // src/service/install-prep.ts
10834
- import { join as join10 } from "node:path";
11054
+ import { join as join11 } from "node:path";
10835
11055
 
10836
11056
  // src/service/wire.ts
10837
- import { join as join9, dirname as dirname5 } from "node:path";
11057
+ import { join as join10, dirname as dirname5 } from "node:path";
10838
11058
  import { fileURLToPath } from "node:url";
10839
11059
 
10840
11060
  // src/service/hermes-config.ts
@@ -11010,7 +11230,7 @@ function checkBindingConflict(prev, next, allowRebind) {
11010
11230
  }
11011
11231
  function connectorsSrcDir(importMetaUrl, harness) {
11012
11232
  const pkgRoot = dirname5(dirname5(fileURLToPath(importMetaUrl)));
11013
- return join9(pkgRoot, "connectors", harness);
11233
+ return join10(pkgRoot, "connectors", harness);
11014
11234
  }
11015
11235
  function hermesConfigPath(home) {
11016
11236
  return `${home}/.hermes/config.yaml`;
@@ -11268,10 +11488,10 @@ function assertReadableCredential(absPath, deps) {
11268
11488
  return { deviceId: json2.device_id, identityId: json2.identity_id, issuedAt };
11269
11489
  }
11270
11490
  function durableDir(home, slug) {
11271
- return join10(home, `securecoms-agent-${slug}`);
11491
+ return join11(home, `securecoms-agent-${slug}`);
11272
11492
  }
11273
11493
  function durableCredentialPath(home, slug) {
11274
- return join10(durableDir(home, slug), `${slug}.json`);
11494
+ return join11(durableDir(home, slug), `${slug}.json`);
11275
11495
  }
11276
11496
  function takeCustody(args2, deps) {
11277
11497
  const dir = durableDir(args2.home, args2.slug);
@@ -11318,7 +11538,7 @@ function readSiblingDefs(dir, prefix, deps) {
11318
11538
  for (const n of names) {
11319
11539
  if (!n.startsWith(prefix)) continue;
11320
11540
  try {
11321
- out.push(deps.readFile(join10(dir, n)));
11541
+ out.push(deps.readFile(join11(dir, n)));
11322
11542
  } catch {
11323
11543
  }
11324
11544
  }
@@ -11513,34 +11733,34 @@ function renderStatusTable(rows) {
11513
11733
 
11514
11734
  // src/service/connection-file.ts
11515
11735
  import {
11516
- chmodSync,
11517
- mkdirSync,
11518
- readFileSync as readFileSync7,
11519
- renameSync,
11520
- unlinkSync,
11521
- writeFileSync as writeFileSync2
11736
+ chmodSync as chmodSync2,
11737
+ mkdirSync as mkdirSync2,
11738
+ readFileSync as readFileSync8,
11739
+ renameSync as renameSync2,
11740
+ unlinkSync as unlinkSync2,
11741
+ writeFileSync as writeFileSync3
11522
11742
  } 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)
11743
+ import { randomUUID as randomUUID2 } from "node:crypto";
11744
+ import { join as join12 } from "node:path";
11745
+ var REAL_FS2 = {
11746
+ mkdir: (p, mode) => mkdirSync2(p, { recursive: true, mode }),
11747
+ chmod: (p, mode) => chmodSync2(p, mode),
11748
+ writeFile: (p, data, mode) => writeFileSync3(p, data, { mode }),
11749
+ rename: (a, b) => renameSync2(a, b),
11750
+ readFile: (p) => readFileSync8(p, "utf8"),
11751
+ unlink: (p) => unlinkSync2(p)
11532
11752
  };
11533
11753
  function securecomsDir(home) {
11534
- return join11(home, ".securecoms");
11754
+ return join12(home, ".securecoms");
11535
11755
  }
11536
11756
  function connectionsDir(home) {
11537
- return join11(securecomsDir(home), "connections");
11757
+ return join12(securecomsDir(home), "connections");
11538
11758
  }
11539
11759
  function connectionFilePath(home, identityId) {
11540
- return join11(connectionsDir(home), `${identityId}.json`);
11760
+ return join12(connectionsDir(home), `${identityId}.json`);
11541
11761
  }
11542
11762
  function newInstanceId() {
11543
- return randomUUID();
11763
+ return randomUUID2();
11544
11764
  }
11545
11765
  function buildConnectionFile(args2) {
11546
11766
  return {
@@ -11554,18 +11774,18 @@ function buildConnectionFile(args2) {
11554
11774
  written_at: args2.now
11555
11775
  };
11556
11776
  }
11557
- function writeConnectionFile(home, doc, deps = REAL_FS) {
11777
+ function writeConnectionFile(home, doc, deps = REAL_FS2) {
11558
11778
  deps.mkdir(securecomsDir(home), 448);
11559
11779
  deps.chmod(securecomsDir(home), 448);
11560
11780
  deps.mkdir(connectionsDir(home), 448);
11561
11781
  deps.chmod(connectionsDir(home), 448);
11562
11782
  const path = connectionFilePath(home, doc.identity_id);
11563
- const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
11783
+ const tmp = `${path}.${process.pid}.${randomUUID2()}.tmp`;
11564
11784
  deps.writeFile(tmp, JSON.stringify(doc, null, 2) + "\n", 384);
11565
11785
  deps.chmod(tmp, 384);
11566
11786
  deps.rename(tmp, path);
11567
11787
  }
11568
- function removeConnectionFileIfOwned(home, identityId, instanceId, deps = REAL_FS) {
11788
+ function removeConnectionFileIfOwned(home, identityId, instanceId, deps = REAL_FS2) {
11569
11789
  const path = connectionFilePath(home, identityId);
11570
11790
  try {
11571
11791
  const cur = JSON.parse(deps.readFile(path));
@@ -11606,10 +11826,10 @@ function realServiceDeps() {
11606
11826
  const r = spawnSync(cmd, args2, { encoding: "utf8" });
11607
11827
  return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
11608
11828
  },
11609
- writeFile: (p, data) => writeFileSync3(p, data, { mode: 420 }),
11829
+ writeFile: (p, data) => writeFileSync4(p, data, { mode: 420 }),
11610
11830
  removeFile: (p) => rmSync(p, { force: true }),
11611
- mkdirp: (p) => mkdirSync2(p, { recursive: true }),
11612
- fileExists: (p) => existsSync2(p),
11831
+ mkdirp: (p) => mkdirSync3(p, { recursive: true }),
11832
+ fileExists: (p) => existsSync3(p),
11613
11833
  user: userInfo().username
11614
11834
  };
11615
11835
  }
@@ -11632,7 +11852,7 @@ function realResolveNodeDeps() {
11632
11852
  };
11633
11853
  return {
11634
11854
  realpath: (p) => realpathSync(p),
11635
- fileExists: (p) => existsSync2(p),
11855
+ fileExists: (p) => existsSync3(p),
11636
11856
  nodeMajorOf: majorOf,
11637
11857
  arch: () => process.arch,
11638
11858
  archOf: (p) => {
@@ -11647,17 +11867,17 @@ function realResolveDeps() {
11647
11867
  const r = spawnSync(cmd, args2, { encoding: "utf8" });
11648
11868
  return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
11649
11869
  },
11650
- fileExists: (p) => existsSync2(p)
11870
+ fileExists: (p) => existsSync3(p)
11651
11871
  };
11652
11872
  }
11653
11873
  function realPrepDeps() {
11654
11874
  return {
11655
- readFile: (p) => readFileSync8(p, "utf8"),
11656
- writeFileMode: (p, data, mode) => writeFileSync3(p, data, { mode }),
11875
+ readFile: (p) => readFileSync9(p, "utf8"),
11876
+ writeFileMode: (p, data, mode) => writeFileSync4(p, data, { mode }),
11657
11877
  mkdirp: (p, mode) => {
11658
- mkdirSync2(p, { recursive: true, mode: mode ?? 493 });
11878
+ mkdirSync3(p, { recursive: true, mode: mode ?? 493 });
11659
11879
  },
11660
- fileExists: (p) => existsSync2(p),
11880
+ fileExists: (p) => existsSync3(p),
11661
11881
  realpath: (p) => realpathSync(p),
11662
11882
  listDir: (p) => {
11663
11883
  try {
@@ -11700,7 +11920,7 @@ function realGetHealth(port) {
11700
11920
  }
11701
11921
  function realReadFleet() {
11702
11922
  return readFleet({
11703
- home: homedir2(),
11923
+ home: homedir3(),
11704
11924
  listDir: (p) => {
11705
11925
  try {
11706
11926
  return readdirSync2(p);
@@ -11708,7 +11928,7 @@ function realReadFleet() {
11708
11928
  return [];
11709
11929
  }
11710
11930
  },
11711
- readFile: (p) => readFileSync8(p, "utf8")
11931
+ readFile: (p) => readFileSync9(p, "utf8")
11712
11932
  });
11713
11933
  }
11714
11934
  function runStatus() {
@@ -11730,62 +11950,62 @@ function runStatus() {
11730
11950
  }
11731
11951
  function realCopyConnector(srcDir, dstDir, harness) {
11732
11952
  const allowedExt = CONNECTOR_FILE_EXTENSIONS[harness];
11733
- mkdirSync2(dstDir, { recursive: true });
11953
+ mkdirSync3(dstDir, { recursive: true });
11734
11954
  for (const name of readdirSync2(srcDir)) {
11735
- const srcPath = join12(srcDir, name);
11736
- if (statSync3(srcPath).isDirectory()) {
11955
+ const srcPath = join13(srcDir, name);
11956
+ if (statSync4(srcPath).isDirectory()) {
11737
11957
  if (CONNECTOR_SKIP_DIRS.has(name)) continue;
11738
- realCopyConnector(srcPath, join12(dstDir, name), harness);
11958
+ realCopyConnector(srcPath, join13(dstDir, name), harness);
11739
11959
  continue;
11740
11960
  }
11741
11961
  if (name.includes(".test.")) continue;
11742
11962
  if (!allowedExt.some((ext) => name.endsWith(ext))) continue;
11743
- writeFileSync3(join12(dstDir, name), readFileSync8(srcPath));
11963
+ writeFileSync4(join13(dstDir, name), readFileSync9(srcPath));
11744
11964
  }
11745
11965
  }
11746
11966
  function realWriteBinding(dstDir, identityId) {
11747
- mkdirSync2(dstDir, { recursive: true });
11748
- writeFileSync3(join12(dstDir, "securecoms.binding.json"), JSON.stringify({ identity_id: identityId }) + "\n", { mode: 384 });
11967
+ mkdirSync3(dstDir, { recursive: true });
11968
+ writeFileSync4(join13(dstDir, "securecoms.binding.json"), JSON.stringify({ identity_id: identityId }) + "\n", { mode: 384 });
11749
11969
  }
11750
11970
  function realReadBinding(dstDir) {
11751
11971
  try {
11752
- const parsed = JSON.parse(readFileSync8(join12(dstDir, "securecoms.binding.json"), "utf8"));
11972
+ const parsed = JSON.parse(readFileSync9(join13(dstDir, "securecoms.binding.json"), "utf8"));
11753
11973
  return parsed.identity_id ?? null;
11754
11974
  } catch {
11755
11975
  return null;
11756
11976
  }
11757
11977
  }
11758
11978
  function realRemoveBinding(dstDir) {
11759
- rmSync(join12(dstDir, "securecoms.binding.json"), { force: true });
11979
+ rmSync(join13(dstDir, "securecoms.binding.json"), { force: true });
11760
11980
  }
11761
11981
  function realStatDir(p) {
11762
11982
  try {
11763
- return statSync3(p).isDirectory();
11983
+ return statSync4(p).isDirectory();
11764
11984
  } catch {
11765
11985
  return false;
11766
11986
  }
11767
11987
  }
11768
11988
  function realReadHermesConfig(p) {
11769
11989
  try {
11770
- return readFileSync8(p, "utf8");
11990
+ return readFileSync9(p, "utf8");
11771
11991
  } catch {
11772
11992
  return null;
11773
11993
  }
11774
11994
  }
11775
11995
  function realBackupHermesConfig(p, originalText) {
11776
- writeFileSync3(hermesConfigBackupPath(p, Date.now(), process.pid), originalText, "utf8");
11996
+ writeFileSync4(hermesConfigBackupPath(p, Date.now(), process.pid), originalText, "utf8");
11777
11997
  }
11778
11998
  function realWriteHermesConfig(p, newText) {
11779
- writeFileSync3(p, newText, "utf8");
11999
+ writeFileSync4(p, newText, "utf8");
11780
12000
  }
11781
12001
  function realWireArgv() {
11782
12002
  if (process.argv.includes("--harness")) return process.argv;
11783
- const detected = detectHarness(homedir2(), realStatDir);
12003
+ const detected = detectHarness(homedir3(), realStatDir);
11784
12004
  return detected ? [...process.argv, "--harness", detected] : process.argv;
11785
12005
  }
11786
12006
  function realWireDeps() {
11787
12007
  return {
11788
- home: homedir2(),
12008
+ home: homedir3(),
11789
12009
  argv: realWireArgv(),
11790
12010
  readFleet: realReadFleet,
11791
12011
  copyConnector: realCopyConnector,
@@ -11837,7 +12057,7 @@ async function runServiceVerb(verb) {
11837
12057
  explicitPort: portStr ? Number(portStr) : void 0,
11838
12058
  harness: installHarness,
11839
12059
  allowRebind: process.argv.includes("--rebind"),
11840
- home: homedir2(),
12060
+ home: homedir3(),
11841
12061
  connectorsSrcDir: connectorsSrcDir(import.meta.url, installHarness === "hermes" ? "hermes" : "openclaw"),
11842
12062
  autoWireDeps: {
11843
12063
  statDir: realStatDir,
@@ -11864,7 +12084,7 @@ async function runServiceVerb(verb) {
11864
12084
  label,
11865
12085
  execPath: installExecPath,
11866
12086
  binScript,
11867
- home: homedir2(),
12087
+ home: homedir3(),
11868
12088
  uid: userInfo().uid
11869
12089
  })
11870
12090
  });
@@ -11878,7 +12098,7 @@ async function runServiceVerb(verb) {
11878
12098
  label,
11879
12099
  execPath: process.execPath,
11880
12100
  binScript,
11881
- home: homedir2(),
12101
+ home: homedir3(),
11882
12102
  uid: userInfo().uid
11883
12103
  };
11884
12104
  const r = uninstallService(input, realServiceDeps());
@@ -11929,11 +12149,11 @@ async function main() {
11929
12149
  `);
11930
12150
  process.exit(1);
11931
12151
  }
11932
- const tokenFile = daemon.tokenFile ?? join12(dirname7(credentialPath), "bridge-token");
12152
+ const tokenFile = daemon.tokenFile ?? join13(dirname7(credentialPath), "bridge-token");
11933
12153
  const token = resolveToken({ tokenFile, token: daemon.token });
11934
12154
  const agent = new BridgeAgent(cfg);
11935
12155
  await agent.start();
11936
- const tls = daemon.tls ? { cert: readFileSync8(daemon.tlsCert, "utf8"), key: readFileSync8(daemon.tlsKey, "utf8") } : void 0;
12156
+ const tls = daemon.tls ? { cert: readFileSync9(daemon.tlsCert, "utf8"), key: readFileSync9(daemon.tlsKey, "utf8") } : void 0;
11937
12157
  const { server, closeAllSseStreams } = createHttpServer(agent, { token, allowOrigins: daemon.allowOrigins, tls, websocket: true });
11938
12158
  const agentWs = attachAgentWebSocket(server, agent, { token, allowOrigins: daemon.allowOrigins });
11939
12159
  let down = false;
@@ -11955,7 +12175,7 @@ async function main() {
11955
12175
  const instanceId = newInstanceId();
11956
12176
  const identityId = agent.health().identity_id;
11957
12177
  const slug = slugFromCredentialPath(credentialPath);
11958
- process.once("exit", () => removeConnectionFileIfOwned(homedir2(), identityId, instanceId));
12178
+ process.once("exit", () => removeConnectionFileIfOwned(homedir3(), identityId, instanceId));
11959
12179
  server.listen(daemon.port, daemon.bind, () => {
11960
12180
  const scheme = tls ? "https" : "http";
11961
12181
  const wsScheme = tls ? "wss" : "ws";
@@ -11966,7 +12186,7 @@ async function main() {
11966
12186
  const addr = server.address();
11967
12187
  const boundPort = typeof addr === "object" && addr ? addr.port : daemon.port;
11968
12188
  try {
11969
- writeConnectionFile(homedir2(), buildConnectionFile({
12189
+ writeConnectionFile(homedir3(), buildConnectionFile({
11970
12190
  identityId,
11971
12191
  port: boundPort,
11972
12192
  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,137 @@ 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
+ const search = skillsJson.search;
7382
+ if (search?.keyFile) paths.push(search.keyFile);
7383
+ return paths.map((path) => {
7384
+ let st;
7385
+ try {
7386
+ st = statFn(path);
7387
+ } catch {
7388
+ return { path, ok: false, why: "not found" };
7389
+ }
7390
+ if ((st.mode & 63) !== 0) {
7391
+ return { path, ok: false, why: "readable by other users on this machine \u2014 tighten its permissions to owner-only" };
7392
+ }
7393
+ if (st.size === 0) return { path, ok: false, why: "the file is empty" };
7394
+ return { path, ok: true, why: "" };
7395
+ });
7396
+ }
7397
+ function lineHash(line) {
7398
+ return createHash("sha256").update(line.trim(), "utf8").digest("hex");
7399
+ }
7400
+ var LOCAL_REPORT_FILE = "local-config.json";
7401
+ function readLocalReport(home, identityId) {
7402
+ const p = join6(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
7403
+ if (!existsSync(p)) return null;
7404
+ let doc;
7405
+ try {
7406
+ doc = JSON.parse(readFileSync5(p, "utf8"));
7407
+ } catch {
7408
+ return null;
7409
+ }
7410
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
7411
+ const hasSearchKey = doc.has_search_key === true;
7412
+ const raw = doc.persona;
7413
+ const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
7414
+ Object.entries(raw).filter(([, v]) => typeof v === "string")
7415
+ ) : {};
7416
+ return { persona, has_search_key: hasSearchKey };
7417
+ }
7418
+ function readManagedMemory(home, identityId) {
7419
+ const p = join6(runtimeConfigDir(home, identityId), MEMORY_FILE);
7420
+ if (!existsSync(p)) return null;
7421
+ try {
7422
+ const body = readFileSync5(p, "utf8").trim();
7423
+ return body || null;
7424
+ } catch {
7425
+ return null;
7426
+ }
7427
+ }
7428
+ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
7429
+ const dir = runtimeConfigDir(home, identityId);
7430
+ const path = join6(dir, MEMORY_FILE);
7431
+ if (!existsSync(path)) return 0;
7432
+ const drop = new Set(hashes);
7433
+ const before = deps.readFile(path).split("\n");
7434
+ const kept = before.filter((l) => l.trim() === "" || !drop.has(lineHash(l)));
7435
+ const removed = before.filter((l) => l.trim() !== "" && drop.has(lineHash(l))).length;
7436
+ if (removed === 0) return 0;
7437
+ const body = kept.filter((l, i) => l.trim() !== "" || i < kept.length - 1).join("\n").trimEnd();
7438
+ atomicWrite(path, body ? `${body}
7439
+ ` : "", deps);
7440
+ return removed;
7441
+ }
7442
+
7311
7443
  // src/bridge-agent.ts
7444
+ import { homedir as homedir2 } from "node:os";
7312
7445
  var VOICE_MODE_GUIDANCE = "Reply immediately and conversationally, one short sentence per message; never bundle multiple points into one message.";
7313
7446
  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
7447
  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 +7496,7 @@ function resolveMessageId(messageId) {
7363
7496
  return messageId;
7364
7497
  }
7365
7498
  function fastReplyId(messageId) {
7366
- const h = createHash("sha256").update(`fast:${messageId}`).digest();
7499
+ const h = createHash2("sha256").update(`fast:${messageId}`).digest();
7367
7500
  h[6] = h[6] & 15 | 64;
7368
7501
  h[8] = h[8] & 63 | 128;
7369
7502
  const x = h.subarray(0, 16).toString("hex");
@@ -7493,6 +7626,7 @@ var BridgeAgent = class _BridgeAgent {
7493
7626
  onSupersedeUnopenable: (channelId, groupId) => this.healSupersedeUnopenable(channelId, groupId)
7494
7627
  });
7495
7628
  this.fetchImpl = deps.fetchImpl ?? fetch;
7629
+ this.home = deps.home ?? homedir2();
7496
7630
  this.injectedFastLlm = !!deps.fastLlm;
7497
7631
  this.fileVoiceConfig = cfg.voice;
7498
7632
  this.fastLlm = deps.fastLlm ?? (cfg.voice?.fastPath.enabled ? new FastLlmClient(cfg.voice.fastPath, this.fetchImpl) : void 0);
@@ -7525,8 +7659,8 @@ var BridgeAgent = class _BridgeAgent {
7525
7659
  const j = await res.json();
7526
7660
  return String(j.attachment_id);
7527
7661
  };
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"));
7662
+ this.attachmentsDirImpl = deps.attachmentsDir ?? join7(dirname(cfg.storePath), "attachments");
7663
+ this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join7(dirname(cfg.storePath), "vault-owner-pins.json"));
7530
7664
  this.action = {
7531
7665
  begin: (channelId) => this.actionBegin(channelId),
7532
7666
  start: (opts) => this.actionStart(opts),
@@ -7542,6 +7676,8 @@ var BridgeAgent = class _BridgeAgent {
7542
7676
  // stored for peek-then-join in ingestOnce
7543
7677
  custody;
7544
7678
  fetchImpl;
7679
+ /** Round 5: home for the managed runtime-config directory (test seam). */
7680
+ home;
7545
7681
  joinState = /* @__PURE__ */ new Map();
7546
7682
  /**
7547
7683
  * Phase 2: lazy channel-metadata cache (channel_id → { is_dm, name, at }) used
@@ -7883,6 +8019,84 @@ var BridgeAgent = class _BridgeAgent {
7883
8019
  if (this.voiceSettingsSource === "backend") return;
7884
8020
  process.stderr.write(
7885
8021
  `[bridge] voice settings unavailable (${e.message}); using ${this.voiceSettingsSource}
8022
+ `
8023
+ );
8024
+ }
8025
+ }
8026
+ /**
8027
+ * Fetch this agent's runtime config and apply it to the managed directory the
8028
+ * runtime reads (spec §8). Safe to call any time — at start(), or from the
8029
+ * receive pump on a `runtime_config_changed` ping — and never throws: a save
8030
+ * the owner already made must not be undone by a network blip here, and no
8031
+ * shape the backend can send may take the daemon down.
8032
+ *
8033
+ * Order matters. Prunes are applied BEFORE the memory is read back, so the
8034
+ * report the owner sees next is the file as it now stands.
8035
+ *
8036
+ * Every cycle rewrites, even at an unchanged version: directory permissions
8037
+ * are repaired only on a write, so skipping would leave a loosened directory
8038
+ * loose until the next save.
8039
+ */
8040
+ async refetchRuntimeConfig() {
8041
+ const base = `${this.cfg.apiBase}/api/v1/agents/${encodeURIComponent(this.cfg.identityId)}/runtime-config`;
8042
+ try {
8043
+ const res = await this.fetchImpl(`${base}/files`, {
8044
+ headers: { authorization: `Bearer ${this.cfg.apiKey}` }
8045
+ });
8046
+ if (!res.ok) throw new Error(`runtime-config ${res.status}`);
8047
+ const payload = await res.json();
8048
+ if (!payload || !payload.version) {
8049
+ process.stderr.write(
8050
+ `[bridge] runtime config not applied (version ${JSON.stringify(payload?.version ?? null)} \u2014 no saved config)
8051
+ `
8052
+ );
8053
+ return;
8054
+ }
8055
+ const skillsJson = payload.skills_json ?? {};
8056
+ writeManagedFiles(this.home, this.cfg.identityId, {
8057
+ ...payload.files ?? {},
8058
+ "skills.json": `${JSON.stringify(skillsJson, null, 2)}
8059
+ `
8060
+ });
8061
+ if (payload.memory_prunes?.length) {
8062
+ pruneManagedMemory(this.home, this.cfg.identityId, payload.memory_prunes);
8063
+ }
8064
+ const local = readLocalReport(this.home, this.cfg.identityId);
8065
+ const report = {
8066
+ applied_version: payload.version,
8067
+ paths: checkConfiguredPaths(skillsJson),
8068
+ // Read AFTER the prunes above, so the owner's next view of memory is
8069
+ // the file as it now stands, not the one they just pruned from.
8070
+ memory: readManagedMemory(this.home, this.cfg.identityId),
8071
+ // E13: pass through what the runtime reported. The daemon does not
8072
+ // interpret any of it.
8073
+ local_persona: local?.persona ?? null,
8074
+ // Only the runtime knows whether a search key file is configured — it
8075
+ // is an environment value on that process, invisible to the daemon and
8076
+ // to the backend. The backend needs it to generate a truthful tool
8077
+ // paragraph, so it rides along here.
8078
+ has_search_key: local?.has_search_key ?? false
8079
+ };
8080
+ const posted = await this.fetchImpl(`${base}/report`, {
8081
+ method: "POST",
8082
+ headers: {
8083
+ authorization: `Bearer ${this.cfg.apiKey}`,
8084
+ "content-type": "application/json"
8085
+ },
8086
+ body: JSON.stringify(report)
8087
+ });
8088
+ if (!posted.ok) {
8089
+ process.stderr.write(
8090
+ `[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
8091
+ `
8092
+ );
8093
+ return;
8094
+ }
8095
+ process.stderr.write(`[bridge] runtime config v${payload.version} applied
8096
+ `);
8097
+ } catch (e) {
8098
+ process.stderr.write(
8099
+ `[bridge] runtime config refetch failed: ${e?.message ?? e}
7886
8100
  `
7887
8101
  );
7888
8102
  }
@@ -7914,6 +8128,7 @@ var BridgeAgent = class _BridgeAgent {
7914
8128
  }
7915
8129
  await this.provider.publishKeyPackage();
7916
8130
  await this.refetchVoiceSettings();
8131
+ await this.refetchRuntimeConfig();
7917
8132
  this.startReceivePump();
7918
8133
  if (this.selfHealSweepEnabled) {
7919
8134
  this.bootSweepTimer = setTimeout(
@@ -9398,11 +9613,16 @@ var BridgeAgent = class _BridgeAgent {
9398
9613
  this.pingsReceived++;
9399
9614
  if (ping?.reconnected) {
9400
9615
  process.stderr.write("[bridge] delivery-ping WS (re)connected \u2014 push inbound active\n");
9616
+ await this.refetchRuntimeConfig();
9401
9617
  }
9402
9618
  if (ping?.voiceSettingsChanged) {
9403
9619
  await this.refetchVoiceSettings();
9404
9620
  continue;
9405
9621
  }
9622
+ if (ping?.runtimeConfigChanged) {
9623
+ await this.refetchRuntimeConfig();
9624
+ continue;
9625
+ }
9406
9626
  await this.ingestOnce();
9407
9627
  }
9408
9628
  } catch {
@@ -9840,13 +10060,13 @@ var BridgeAgent = class _BridgeAgent {
9840
10060
  };
9841
10061
 
9842
10062
  // 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";
10063
+ import { readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
10064
+ import { dirname as dirname2, join as join8 } from "node:path";
9845
10065
  var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
9846
10066
  function loadConfig(opts) {
9847
- const raw = readFileSync5(opts.credentialPath, "utf8");
10067
+ const raw = readFileSync6(opts.credentialPath, "utf8");
9848
10068
  try {
9849
- const mode = statSync2(opts.credentialPath).mode & 511;
10069
+ const mode = statSync3(opts.credentialPath).mode & 511;
9850
10070
  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
10071
  `);
9852
10072
  } catch {
@@ -9858,7 +10078,7 @@ function loadConfig(opts) {
9858
10078
  throw new Error("credential file is not valid JSON");
9859
10079
  }
9860
10080
  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");
10081
+ const storePath = opts.storePath ?? join8(dirname2(opts.credentialPath), "bridge-state.db");
9862
10082
  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
10083
  const rateLimitCapacity = typeof json2.rate_limit_capacity === "number" ? json2.rate_limit_capacity : 20;
9864
10084
  const rateLimitRefillPerMinute = typeof json2.rate_limit_refill_per_minute === "number" ? json2.rate_limit_refill_per_minute : 20;
@@ -9954,13 +10174,13 @@ import { z as z3 } from "zod";
9954
10174
 
9955
10175
  // src/auth.ts
9956
10176
  import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
9957
- import { existsSync, readFileSync as readFileSync6, writeFileSync } from "node:fs";
10177
+ import { existsSync as existsSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
9958
10178
  function resolveToken(opts) {
9959
10179
  if (opts.token) return opts.token;
9960
10180
  const f = opts.tokenFile;
9961
- if (f && existsSync(f)) return readFileSync6(f, "utf8").trim();
10181
+ if (f && existsSync2(f)) return readFileSync7(f, "utf8").trim();
9962
10182
  const tok = randomBytes3(32).toString("base64url");
9963
- if (f) writeFileSync(f, tok, { mode: 384 });
10183
+ if (f) writeFileSync2(f, tok, { mode: 384 });
9964
10184
  return tok;
9965
10185
  }
9966
10186
  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.19.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",