@securecoms/agent-bridge 0.33.1 → 0.35.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/index.js CHANGED
@@ -18585,6 +18585,8 @@ function isEpochConflict(e) {
18585
18585
  var LABEL = "agentvault-connect-v1";
18586
18586
  var DEFAULT_STUCK_THRESHOLD = 2;
18587
18587
  var DEFAULT_QUARANTINE_THRESHOLD = 8;
18588
+ var KEPT_WELCOME_COMMIT_DEFER_MS = 24 * 60 * 60 * 1e3;
18589
+ var KEPT_WELCOME_COMMIT_DEFER_MAX_ROWS = 32;
18588
18590
  var DELIVERY_PULL_PAGE = 100;
18589
18591
  var DELIVERY_MAX_RETRIES = 5;
18590
18592
  var DrainPageFullError = class extends Error {
@@ -18789,6 +18791,15 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
18789
18791
  * {@link errorStats}; mutated only through {@link recordDecryptError} /
18790
18792
  * {@link recordDecryptSuccess}.
18791
18793
  */
18794
+ /** {@link KEPT_WELCOME_COMMIT_DEFER_MS}: channel -> when its first commit was
18795
+ * deferred behind the kept Welcome `welcomeId`. Restarted for a different
18796
+ * Welcome; cleared when the channel decrypts, or when a pass that saw the
18797
+ * whole queue kept no Welcome for it (fix round 1, L2: the Welcome
18798
+ * resolved, so a later re-Welcome gets a fresh, bounded window). An entry
18799
+ * past its bound stays (expired) until then, so the bound cannot restart
18800
+ * itself pass after pass. In memory: a reload restarts it, and the
18801
+ * per-pass row cap is what bounds the page cost across reloads. */
18802
+ keptWelcomeDeferSince = /* @__PURE__ */ new Map();
18792
18803
  errors = /* @__PURE__ */ new Map();
18793
18804
  stuckThreshold;
18794
18805
  /** Consecutive-error threshold at which a wedged channel's row is quarantined
@@ -19007,8 +19018,33 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
19007
19018
  * channel resumes normal processing and can quarantine again later if it
19008
19019
  * re-wedges (the onWedged guard is reset so the hook can fire again).
19009
19020
  */
19021
+ /** See {@link MlsGroupKeyProviderOpts.deferCommitsWhileWelcomePending}. The
19022
+ * caller has already established that this pass KEPT a no-state Welcome
19023
+ * for `channelId`. True: defer the commit row. Never throws. */
19024
+ deferKeptWelcomeCommit(channelId, welcomeId) {
19025
+ const pred = this.o.deferCommitsWhileWelcomePending;
19026
+ if (!pred)
19027
+ return false;
19028
+ let boundMs = 0;
19029
+ try {
19030
+ const v = pred(channelId);
19031
+ boundMs = v === true ? KEPT_WELCOME_COMMIT_DEFER_MS : typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.min(v, KEPT_WELCOME_COMMIT_DEFER_MS) : 0;
19032
+ } catch {
19033
+ boundMs = 0;
19034
+ }
19035
+ if (boundMs <= 0)
19036
+ return false;
19037
+ const now = this.nowMs();
19038
+ const entry = this.keptWelcomeDeferSince.get(channelId);
19039
+ if (entry === void 0 || entry.welcomeId !== welcomeId) {
19040
+ this.keptWelcomeDeferSince.set(channelId, { since: now, welcomeId });
19041
+ return true;
19042
+ }
19043
+ return now - entry.since < boundMs;
19044
+ }
19010
19045
  recordDecryptSuccess(channelId) {
19011
19046
  this.lastDecryptAt.set(channelId, Date.now());
19047
+ this.keptWelcomeDeferSince.delete(channelId);
19012
19048
  const e = this.errors.get(channelId);
19013
19049
  if (!e)
19014
19050
  return;
@@ -19404,6 +19440,12 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
19404
19440
  * server's REJOIN_THRESHOLD, there was nothing to apply, or a commit would
19405
19441
  * not apply (which means the state is genuinely FORKED, not merely behind,
19406
19442
  * and replay cannot fix a fork).
19443
+ *
19444
+ * THROWS (retryable, never `false`, #1290): CustodyPersistError when the
19445
+ * channel is under the custody hold (nothing applied), and the C2 classes
19446
+ * (PendingCommitUnresolvedError from the guarded MAIN write) after evicting
19447
+ * the advanced cache, so the persisted state is unchanged. A /sync request
19448
+ * that fails also throws, before anything is applied.
19407
19449
  */
19408
19450
  async resyncByReplay(channelId, groupId) {
19409
19451
  if (!await this.hasPersistedChannelState(channelId))
@@ -19423,12 +19465,26 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
19423
19465
  let applied = 0;
19424
19466
  try {
19425
19467
  await this.locks.run(channelId, async () => {
19468
+ if (await this.custodyHeld(channelId))
19469
+ throw new CustodyPersistError(channelId);
19426
19470
  const mgr = await this.manager(channelId);
19427
19471
  try {
19428
19472
  for (const c of commits) {
19429
19473
  if (c.epoch <= Number(mgr.epoch))
19430
19474
  continue;
19431
- await mgr.decrypt(hexToBytes22(c.payload));
19475
+ const commitBytes = hexToBytes22(c.payload);
19476
+ try {
19477
+ await mgr.decrypt(commitBytes);
19478
+ } catch (e) {
19479
+ if (await probeOwnRemoval(mgr, commitBytes) !== null) {
19480
+ this.noteOwnLeafRemoved(channelId, {
19481
+ group_id: groupId,
19482
+ epoch: c.epoch,
19483
+ message_id: c.id
19484
+ });
19485
+ }
19486
+ throw e;
19487
+ }
19432
19488
  applied++;
19433
19489
  }
19434
19490
  if (applied > 0)
@@ -19440,6 +19496,10 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
19440
19496
  }
19441
19497
  });
19442
19498
  } catch (e) {
19499
+ if (isSdkError(e, "CustodyPersistError") || e instanceof CustodyPersistError || e instanceof PendingCommitUnresolvedError || e instanceof GroupSupersededError || e instanceof AttestedOrphanRefusedError) {
19500
+ writeServiceLog("warn", "resync.replay_held", { applied }, e);
19501
+ throw e;
19502
+ }
19443
19503
  writeServiceLog("warn", "resync.replay_commit_failed", { applied }, e);
19444
19504
  return false;
19445
19505
  }
@@ -22140,6 +22200,8 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
22140
22200
  out.push({
22141
22201
  queueId: `replay-${m.id}`,
22142
22202
  messageId: envelope.message_id,
22203
+ // #1246 C-1: the group-messages row id is the same mls_messages key.
22204
+ ...typeof m.id === "string" && m.id ? { serverMessageId: m.id } : {},
22143
22205
  channelId,
22144
22206
  plaintext,
22145
22207
  envelope,
@@ -23040,6 +23102,8 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
23040
23102
  const heldBundleRefs = () => heldBundleRefsMemo ??= this.readHeldBundleRefs();
23041
23103
  const w2Acked = /* @__PURE__ */ new Map();
23042
23104
  const w2Kept = /* @__PURE__ */ new Set();
23105
+ const keptWelcomeId = /* @__PURE__ */ new Map();
23106
+ const deferredThisPass = /* @__PURE__ */ new Map();
23043
23107
  const unpinnedRefusals = /* @__PURE__ */ new Map();
23044
23108
  const welcomes = rows.filter((r) => r.message_type === "welcome");
23045
23109
  const nonWelcomes = rows.filter((r) => r.message_type !== "welcome");
@@ -23198,6 +23262,8 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
23198
23262
  writeServiceLog("warn", "welcome.unopenable");
23199
23263
  } else {
23200
23264
  w2Kept.add(wcid);
23265
+ if (!keptWelcomeId.has(wcid))
23266
+ keptWelcomeId.set(wcid, row.queue_id);
23201
23267
  if (typeof process !== "undefined" && process.env?.SECURECOMS_TRACE_DELIVERY) {
23202
23268
  writeServiceLog("debug", "delivery.welcome_no_local_state");
23203
23269
  }
@@ -23208,6 +23274,10 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
23208
23274
  const cid = row.connect_channel_id;
23209
23275
  if (!cid)
23210
23276
  continue;
23277
+ if (w2Kept.has(cid) && (deferredThisPass.get(cid) ?? 0) < KEPT_WELCOME_COMMIT_DEFER_MAX_ROWS && this.deferKeptWelcomeCommit(cid, keptWelcomeId.get(cid) ?? "")) {
23278
+ deferredThisPass.set(cid, (deferredThisPass.get(cid) ?? 0) + 1);
23279
+ continue;
23280
+ }
23211
23281
  let advancedEpoch;
23212
23282
  let custodyHeld = false;
23213
23283
  await this.locks.run(cid, async () => {
@@ -23364,6 +23434,12 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
23364
23434
  this.leftStreak.set(cid, { pulls: 1, since: pullNow });
23365
23435
  this.scheduleAutoResolve(cid);
23366
23436
  }
23437
+ if (rows.length < DELIVERY_PULL_PAGE) {
23438
+ for (const cid of [...this.keptWelcomeDeferSince.keys()]) {
23439
+ if (!w2Kept.has(cid))
23440
+ this.keptWelcomeDeferSince.delete(cid);
23441
+ }
23442
+ }
23367
23443
  for (const [cid, gid] of w2Acked) {
23368
23444
  if (w2Kept.has(cid))
23369
23445
  continue;
@@ -23643,7 +23719,9 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
23643
23719
  voiceDefaultResponder: row.voice_default_responder ?? void 0,
23644
23720
  ...typeof row.identity_signature === "string" && row.identity_signature ? { identitySignature: row.identity_signature } : {},
23645
23721
  payloadHex: row.payload,
23646
- openedAt
23722
+ openedAt,
23723
+ // #1246 C-1: the server row id, beside the sender-chosen envelope id.
23724
+ ...typeof row.message_id === "string" && row.message_id ? { serverMessageId: row.message_id } : {}
23647
23725
  };
23648
23726
  const custodiedAt = this.nowMs();
23649
23727
  this.custody.set(row.queue_id, entry);
@@ -24650,8 +24728,8 @@ function openDeliveryPings(o) {
24650
24728
  return Promise.resolve({ value: void 0, done: true });
24651
24729
  if (queue.length)
24652
24730
  return Promise.resolve({ value: queue.shift(), done: false });
24653
- return new Promise((resolve) => {
24654
- waiter = resolve;
24731
+ return new Promise((resolve2) => {
24732
+ waiter = resolve2;
24655
24733
  });
24656
24734
  }
24657
24735
  };
@@ -24971,8 +25049,8 @@ var PingSubscriber = class {
24971
25049
  return Promise.resolve({ value: void 0, done: true });
24972
25050
  if (this.queue.length)
24973
25051
  return Promise.resolve({ value: this.queue.shift(), done: false });
24974
- return new Promise((resolve) => {
24975
- this.waiter = resolve;
25052
+ return new Promise((resolve2) => {
25053
+ this.waiter = resolve2;
24976
25054
  });
24977
25055
  }
24978
25056
  };
@@ -26165,6 +26243,11 @@ var FakeConnectBackend = class _FakeConnectBackend {
26165
26243
  // Owner-signed tool grants (spec §7.0): like Plan 0's backend, each row carries
26166
26244
  // the envelope's `identity_proof.signature` as `identity_signature` beside the
26167
26245
  // unsigned canonical payload. `omitSignature` models a pre-Plan-0 row.
26246
+ /** `rowMessageId` (#1246 C-1): the server row id to deliver, when it is NOT
26247
+ * the envelope's id. The REST send path keys the row by the envelope id
26248
+ * (connect_messages.py, `MLSMessage(id=message_id)`); the WS `message_mls`
26249
+ * path stores through `store_mls_message`, whose row id is server-generated,
26250
+ * so there the two differ and the envelope id is the sender's free choice. */
26168
26251
  async ingestApplication(envelope, opts) {
26169
26252
  const channelId = envelope.channel_id;
26170
26253
  const canonical = await canonicalEnvelopeBytes(envelope);
@@ -26176,7 +26259,7 @@ var FakeConnectBackend = class _FakeConnectBackend {
26176
26259
  continue;
26177
26260
  this.q(dev).push({
26178
26261
  queue_id: "q-" + this._seq++,
26179
- message_id: envelope.message_id,
26262
+ message_id: opts?.rowMessageId ?? envelope.message_id,
26180
26263
  group_id: groupId,
26181
26264
  message_type: "application",
26182
26265
  sender_device_id: senderDeviceId ?? "",
@@ -26560,6 +26643,7 @@ function openSchema(db) {
26560
26643
  ensureRuntimeInboxDeliveryAttempts(db);
26561
26644
  ensureRuntimeReplyIntent(db);
26562
26645
  ensureRuntimeReplyIntentEnvelopeGroup(db);
26646
+ ensureRuntimeReplyIntentCredit(db);
26563
26647
  ensureGroupLink(db);
26564
26648
  ensureMlsCustody(db);
26565
26649
  ensureToolGrantTables(db);
@@ -26640,6 +26724,19 @@ function ensureRuntimeReplyIntentEnvelopeGroup(db) {
26640
26724
  throw e;
26641
26725
  }
26642
26726
  }
26727
+ function ensureRuntimeReplyIntentCredit(db) {
26728
+ const hasColumn = () => db.prepare("PRAGMA table_info(runtime_reply_intent)").all().some((c) => c.name === "reply_credit");
26729
+ const cols = db.prepare("PRAGMA table_info(runtime_reply_intent)").all();
26730
+ if (cols.length === 0 || cols.some((c) => c.name === "reply_credit"))
26731
+ return;
26732
+ try {
26733
+ db.exec("ALTER TABLE runtime_reply_intent ADD COLUMN reply_credit TEXT");
26734
+ } catch (e) {
26735
+ if (hasColumn())
26736
+ return;
26737
+ throw e;
26738
+ }
26739
+ }
26643
26740
  var RECALL_STATE_COLUMNS = [
26644
26741
  ["recall_enabled", "INTEGER NOT NULL DEFAULT 0"],
26645
26742
  ["recall_mode_epoch", "INTEGER NOT NULL DEFAULT 0"],
@@ -27691,8 +27788,8 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
27691
27788
  this.db.prepare("DELETE FROM runtime_reply_intent WHERE identity_id=? AND updated_at < ?").run(this.id, now - RUNTIME_REPLY_INTENT_RETENTION_MS);
27692
27789
  const info = this.db.prepare(`INSERT OR IGNORE INTO runtime_reply_intent
27693
27790
  (identity_id, scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
27694
- sealed_body, sealed_envelope, state, created_at, updated_at)
27695
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'pending', ?, ?)`).run(this.id, input.scope, input.part, input.inReplyTo, input.channelId, input.replyMessageId, input.bodyMac, sealed, now, now);
27791
+ sealed_body, sealed_envelope, state, created_at, updated_at, reply_credit)
27792
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'pending', ?, ?, ?)`).run(this.id, input.scope, input.part, input.inReplyTo, input.channelId, input.replyMessageId, input.bodyMac, sealed, now, now, input.replyCredit ?? null);
27696
27793
  const count = this.db.prepare("SELECT COUNT(*) AS n FROM runtime_reply_intent WHERE identity_id=?").get(this.id);
27697
27794
  if (count.n > RUNTIME_REPLY_INTENT_MAX) {
27698
27795
  this.db.prepare(`DELETE FROM runtime_reply_intent
@@ -27702,7 +27799,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
27702
27799
  )`).run(this.id, this.id, RUNTIME_REPLY_INTENT_MAX);
27703
27800
  }
27704
27801
  const row = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
27705
- sealed_body, sealed_envelope, sealed_envelope_group, state, created_at
27802
+ sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
27706
27803
  FROM runtime_reply_intent WHERE identity_id=? AND scope=? AND part=?`).get(this.id, input.scope, input.part);
27707
27804
  if (!row)
27708
27805
  throw new Error("reply intent vanished after claim");
@@ -27711,7 +27808,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
27711
27808
  /** SEC-06 #18: every intent answering `inReplyTo`, in claim order. */
27712
27809
  async listReplyIntents(inReplyTo) {
27713
27810
  const rows = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
27714
- sealed_body, sealed_envelope, sealed_envelope_group, state, created_at
27811
+ sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
27715
27812
  FROM runtime_reply_intent WHERE identity_id=? AND in_reply_to=?
27716
27813
  ORDER BY created_at ASC, rowid ASC`).all(this.id, inReplyTo);
27717
27814
  const out = [];
@@ -27722,7 +27819,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
27722
27819
  /** SEC-06 #18: re-read one intent by its key (null when absent). */
27723
27820
  async getReplyIntent(scope, part) {
27724
27821
  const row = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
27725
- sealed_body, sealed_envelope, sealed_envelope_group, state, created_at
27822
+ sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
27726
27823
  FROM runtime_reply_intent WHERE identity_id=? AND scope=? AND part=?`).get(this.id, scope, part);
27727
27824
  return row ? this.openReplyIntent(row) : null;
27728
27825
  }
@@ -27796,7 +27893,8 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
27796
27893
  envelope,
27797
27894
  envelopeGroupId: envelopeGroupFor(envelope, await openOrNull(row.sealed_envelope_group)),
27798
27895
  state: row.state === "sent" ? "sent" : row.state === "skipped" ? "skipped" : "pending",
27799
- createdAt: Number(row.created_at)
27896
+ createdAt: Number(row.created_at),
27897
+ replyCredit: typeof row.reply_credit === "string" ? row.reply_credit : null
27800
27898
  };
27801
27899
  }
27802
27900
  async recordMemory(input) {
@@ -29566,8 +29664,8 @@ function decodeReplyBody(bytes) {
29566
29664
  if (b?.kind === "a2a" && b.a2a && typeof b.a2a === "object") return b;
29567
29665
  throw new Error("reply intent body is malformed");
29568
29666
  }
29569
- function replyBodyMac(key, replyId, channelId, body) {
29570
- return createHmac("sha256", key).update(JSON.stringify(["reply-body", replyId, channelId])).update("\0").update(body).digest("hex");
29667
+ function replyBodyMac(key, replyId, channelId, body, credit) {
29668
+ return createHmac("sha256", key).update(JSON.stringify(credit === void 0 ? ["reply-body", replyId, channelId] : ["reply-body", replyId, channelId, credit])).update("\0").update(body).digest("hex");
29571
29669
  }
29572
29670
  function partOrdinal(part) {
29573
29671
  const m = /^p:(\d+)$/.exec(part);
@@ -29608,8 +29706,96 @@ function loadPackageMeta(importMetaUrl) {
29608
29706
  }
29609
29707
 
29610
29708
  // src/bridge-agent.ts
29611
- import { existsSync as existsSync5, promises as fsp, readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
29612
- import { join as join17, dirname as dirname5 } from "node:path";
29709
+ import { existsSync as existsSync5, promises as fsp2, readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
29710
+
29711
+ // src/outbox.ts
29712
+ import { constants, mkdirSync, lstatSync, chmodSync, realpathSync, promises as fsp } from "node:fs";
29713
+ import { basename, dirname, join, resolve } from "node:path";
29714
+ var VAULT_OUTBOX_DIRNAME = "vault-outbox";
29715
+ var PathOutsideOutboxError = class extends Error {
29716
+ constructor() {
29717
+ super("path_outside_outbox");
29718
+ this.name = "PathOutsideOutboxError";
29719
+ }
29720
+ };
29721
+ var POSIX = process.platform !== "win32";
29722
+ function ownedPrivateDir(st) {
29723
+ if (st.isSymbolicLink() || !st.isDirectory()) return false;
29724
+ if (!POSIX) return true;
29725
+ const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
29726
+ if (uid !== void 0 && st.uid !== uid) return false;
29727
+ return (st.mode & 511) === 448;
29728
+ }
29729
+ function ensureOutbox(dir) {
29730
+ mkdirSync(dir, { recursive: true, mode: 448 });
29731
+ let st = lstatSync(dir);
29732
+ if (st.isSymbolicLink() || !st.isDirectory()) throw new PathOutsideOutboxError();
29733
+ const uid = POSIX && typeof process.getuid === "function" ? process.getuid() : void 0;
29734
+ if (uid !== void 0 && st.uid !== uid) throw new PathOutsideOutboxError();
29735
+ if (POSIX && (st.mode & 511) !== 448) {
29736
+ chmodSync(dir, 448);
29737
+ st = lstatSync(dir);
29738
+ }
29739
+ if (!ownedPrivateDir(st)) throw new PathOutsideOutboxError();
29740
+ return { dir, real: realpathSync(dir), dev: st.dev, ino: st.ino };
29741
+ }
29742
+ var NOFOLLOW = constants.O_NOFOLLOW ?? 0;
29743
+ var NONBLOCK = constants.O_NONBLOCK ?? 0;
29744
+ async function readOutboxFile(pin, callerPath) {
29745
+ if (!pin) throw new PathOutsideOutboxError();
29746
+ try {
29747
+ const st = await fsp.lstat(pin.dir);
29748
+ if (!ownedPrivateDir(st) || st.dev !== pin.dev || st.ino !== pin.ino) throw new PathOutsideOutboxError();
29749
+ } catch {
29750
+ throw new PathOutsideOutboxError();
29751
+ }
29752
+ const requested = resolve(pin.dir, callerPath);
29753
+ const name = basename(requested);
29754
+ let realParent;
29755
+ try {
29756
+ realParent = await fsp.realpath(dirname(requested));
29757
+ } catch {
29758
+ throw new PathOutsideOutboxError();
29759
+ }
29760
+ if (realParent !== pin.real) throw new PathOutsideOutboxError();
29761
+ const target = join(pin.real, name);
29762
+ const pre = await fsp.lstat(target);
29763
+ if (pre.isSymbolicLink() || !pre.isFile()) throw new PathOutsideOutboxError();
29764
+ let handle;
29765
+ try {
29766
+ handle = await fsp.open(target, constants.O_RDONLY | NOFOLLOW | NONBLOCK);
29767
+ } catch (e) {
29768
+ if (e.code === "ENOENT") throw e;
29769
+ throw new PathOutsideOutboxError();
29770
+ }
29771
+ try {
29772
+ const st = await handle.stat();
29773
+ if (!st.isFile() || st.dev !== pre.dev || st.ino !== pre.ino || st.nlink !== 1) {
29774
+ throw new PathOutsideOutboxError();
29775
+ }
29776
+ return { bytes: new Uint8Array(await handle.readFile()), path: target, dev: st.dev, ino: st.ino };
29777
+ } finally {
29778
+ await handle.close().catch(() => {
29779
+ });
29780
+ }
29781
+ }
29782
+ async function consumeOutboxFile(pin, read) {
29783
+ try {
29784
+ const dir = await fsp.lstat(pin.dir);
29785
+ if (!ownedPrivateDir(dir) || dir.dev !== pin.dev || dir.ino !== pin.ino) return false;
29786
+ if (await fsp.realpath(pin.dir) !== pin.real) return false;
29787
+ const target = join(pin.real, basename(read.path));
29788
+ const st = await fsp.lstat(target);
29789
+ if (st.isSymbolicLink() || st.dev !== read.dev || st.ino !== read.ino) return false;
29790
+ await fsp.unlink(target);
29791
+ return true;
29792
+ } catch {
29793
+ return false;
29794
+ }
29795
+ }
29796
+
29797
+ // src/bridge-agent.ts
29798
+ import { join as join18, dirname as dirname6 } from "node:path";
29613
29799
  import { fileURLToPath as fileURLToPath2 } from "node:url";
29614
29800
 
29615
29801
  // src/latency.ts
@@ -30107,11 +30293,21 @@ function pinCoverage(pinStripped, ownerMessage, coverTexts) {
30107
30293
  }
30108
30294
 
30109
30295
  // src/attachments.ts
30110
- import { basename } from "node:path";
30296
+ import { basename as basename2 } from "node:path";
30111
30297
  var VAULT_REF_MAX_SIZE_HINT = 100 * 1024 * 1024 - 16;
30112
30298
  var VAULT_REF_MAX_NAME = 255;
30113
30299
  var VAULT_REF_MAX_MIME = 127;
30114
30300
  var CANONICAL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
30301
+ var InvalidBlobIdError = class extends Error {
30302
+ constructor() {
30303
+ super("invalid_blob_id");
30304
+ this.name = "InvalidBlobIdError";
30305
+ }
30306
+ };
30307
+ function assertBlobId(blobId) {
30308
+ if (typeof blobId !== "string" || !CANONICAL_UUID.test(blobId)) throw new InvalidBlobIdError();
30309
+ return blobId;
30310
+ }
30115
30311
  var VAULT_PIN = /^blake2b:[0-9a-f]{64}$/;
30116
30312
  var HINT_FORBIDDEN = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/;
30117
30313
  var MIME_TYPE = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$/;
@@ -30150,7 +30346,7 @@ function toVaultRefEntry(body) {
30150
30346
  return entry;
30151
30347
  }
30152
30348
  function sanitizeFilename(name, fallback) {
30153
- const base = basename(name ?? "");
30349
+ const base = basename2(name ?? "");
30154
30350
  const cleaned = base.replace(/[/\\]/g, "").replace(/^\.+$/, "");
30155
30351
  return cleaned.length > 0 ? cleaned : fallback;
30156
30352
  }
@@ -30180,7 +30376,7 @@ function mimeForName(name) {
30180
30376
  async function prepareAttachment(filePath2, caption, channelId, deps) {
30181
30377
  await deps.ready();
30182
30378
  const bytes = await deps.readFile(filePath2);
30183
- const name = basename(filePath2);
30379
+ const name = basename2(filePath2);
30184
30380
  const { encryptedData, fileKey, fileNonce, digest } = await deps.encryptFile(bytes);
30185
30381
  const blobId = await deps.uploadBlob(encryptedData, channelId);
30186
30382
  const body = {
@@ -30198,8 +30394,9 @@ async function prepareAttachment(filePath2, caption, channelId, deps) {
30198
30394
  return body;
30199
30395
  }
30200
30396
  async function materializeAttachment(body, ctx, deps) {
30397
+ const blobId = assertBlobId(body.blobId);
30201
30398
  await deps.ready();
30202
- const ciphertext = await deps.downloadBlob(body.blobId);
30399
+ const ciphertext = await deps.downloadBlob(blobId);
30203
30400
  const sha25612 = await deps.computeFileDigest(ciphertext);
30204
30401
  if (sha25612 !== body.digest) {
30205
30402
  throw new Error(`attachment digest mismatch for blob ${body.blobId}`);
@@ -30225,13 +30422,13 @@ function ensureSodiumReady2() {
30225
30422
  // src/memory/hermes-session-map.ts
30226
30423
  import { readFileSync } from "node:fs";
30227
30424
  import { homedir } from "node:os";
30228
- import { join } from "node:path";
30425
+ import { join as join2 } from "node:path";
30229
30426
  function expandHome(p) {
30230
- return p === "~" || p.startsWith("~/") ? join(homedir(), p.slice(1)) : p;
30427
+ return p === "~" || p.startsWith("~/") ? join2(homedir(), p.slice(1)) : p;
30231
30428
  }
30232
30429
  function resolveHermesSession(baseDir, conversationId) {
30233
30430
  try {
30234
- const path = join(expandHome(baseDir), "sessions", "sessions.json");
30431
+ const path = join2(expandHome(baseDir), "sessions", "sessions.json");
30235
30432
  const map = JSON.parse(readFileSync(path, "utf8"));
30236
30433
  const entry = map[`agent:main:securecoms:dm:${conversationId}`];
30237
30434
  const sid = entry?.session_id;
@@ -30243,11 +30440,11 @@ function resolveHermesSession(baseDir, conversationId) {
30243
30440
 
30244
30441
  // src/memory/hermes-store.ts
30245
30442
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
30246
- import { join as join2 } from "node:path";
30443
+ import { join as join3 } from "node:path";
30247
30444
  var HermesStore = class {
30248
30445
  dbPath;
30249
30446
  constructor(baseDir) {
30250
- this.dbPath = join2(expandHome(baseDir), "state.db");
30447
+ this.dbPath = join3(expandHome(baseDir), "state.db");
30251
30448
  }
30252
30449
  /** Recent-K conversational turns for a session, chronological. Fail-safe → []. */
30253
30450
  readRecent(sessionId, limit) {
@@ -30389,10 +30586,10 @@ var HermesMemoryProvider = class {
30389
30586
 
30390
30587
  // src/memory/openclaw-session-map.ts
30391
30588
  import { readFileSync as readFileSync2 } from "node:fs";
30392
- import { join as join3 } from "node:path";
30589
+ import { join as join4 } from "node:path";
30393
30590
  function resolveOwnerDmSession(baseDir, agent) {
30394
30591
  try {
30395
- const path = join3(expandHome(baseDir), "agents", agent, "sessions", "sessions.json");
30592
+ const path = join4(expandHome(baseDir), "agents", agent, "sessions", "sessions.json");
30396
30593
  const map = JSON.parse(readFileSync2(path, "utf8"));
30397
30594
  let best = null;
30398
30595
  for (const [key, entry] of Object.entries(map)) {
@@ -30410,11 +30607,11 @@ function resolveOwnerDmSession(baseDir, agent) {
30410
30607
 
30411
30608
  // src/memory/openclaw-session-log.ts
30412
30609
  import { readFileSync as readFileSync3 } from "node:fs";
30413
- import { join as join4 } from "node:path";
30610
+ import { join as join5 } from "node:path";
30414
30611
  function readRecentTurns(baseDir, agent, sessionId, k) {
30415
30612
  let raw;
30416
30613
  try {
30417
- raw = readFileSync3(join4(expandHome(baseDir), "agents", agent, "sessions", `${sessionId}.jsonl`), "utf8");
30614
+ raw = readFileSync3(join5(expandHome(baseDir), "agents", agent, "sessions", `${sessionId}.jsonl`), "utf8");
30418
30615
  } catch {
30419
30616
  return [];
30420
30617
  }
@@ -30445,7 +30642,7 @@ function readRecentTurns(baseDir, agent, sessionId, k) {
30445
30642
 
30446
30643
  // src/memory/openclaw-workspace-facts.ts
30447
30644
  import { readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
30448
- import { join as join5 } from "node:path";
30645
+ import { join as join6 } from "node:path";
30449
30646
  var DEFAULT_FACTS_BUDGET = 8e3;
30450
30647
  var MEMORY_MD_CAP = 6e3;
30451
30648
  var MIN_FRAGMENT = 200;
@@ -30456,7 +30653,7 @@ function resolveWorkspaceDir(baseDir, agent, override) {
30456
30653
  const base = expandHome(baseDir);
30457
30654
  const isMain = agent === "" || agent === "main";
30458
30655
  try {
30459
- const cfg = JSON.parse(readFileSync4(join5(base, "openclaw.json"), "utf8"));
30656
+ const cfg = JSON.parse(readFileSync4(join6(base, "openclaw.json"), "utf8"));
30460
30657
  const agents = cfg?.agents;
30461
30658
  if (agents && typeof agents === "object") {
30462
30659
  const list = Array.isArray(agents.list) ? agents.list : [];
@@ -30469,7 +30666,7 @@ function resolveWorkspaceDir(baseDir, agent, override) {
30469
30666
  }
30470
30667
  } catch {
30471
30668
  }
30472
- return isMain ? join5(base, "workspace") : join5(base, `workspace-${agent}`);
30669
+ return isMain ? join6(base, "workspace") : join6(base, `workspace-${agent}`);
30473
30670
  }
30474
30671
  var TRUNC = "\u2026[truncated]";
30475
30672
  function headSlice(s, n) {
@@ -30499,7 +30696,7 @@ function readWorkspaceFacts(baseDir, agent, opts) {
30499
30696
  let truncated = false;
30500
30697
  let remaining = budget;
30501
30698
  try {
30502
- const r = readBounded(join5(dir, "MEMORY.md"));
30699
+ const r = readBounded(join6(dir, "MEMORY.md"));
30503
30700
  if (r.oversize) truncated = true;
30504
30701
  else if (r.text && r.text.trim()) {
30505
30702
  const { text, clipped } = headSlice(r.text, Math.min(MEMORY_MD_CAP, budget));
@@ -30512,7 +30709,7 @@ ${TRUNC}` : ""}`);
30512
30709
  } catch {
30513
30710
  }
30514
30711
  try {
30515
- const dailies = readdirSync(join5(dir, "memory")).filter((f) => DAILY_RE.test(f)).sort().reverse().slice(0, 2);
30712
+ const dailies = readdirSync(join6(dir, "memory")).filter((f) => DAILY_RE.test(f)).sort().reverse().slice(0, 2);
30516
30713
  for (const f of dailies) {
30517
30714
  if (remaining < MIN_FRAGMENT) {
30518
30715
  truncated = true;
@@ -30520,7 +30717,7 @@ ${TRUNC}` : ""}`);
30520
30717
  }
30521
30718
  let r;
30522
30719
  try {
30523
- r = readBounded(join5(dir, "memory", f));
30720
+ r = readBounded(join6(dir, "memory", f));
30524
30721
  } catch {
30525
30722
  continue;
30526
30723
  }
@@ -31259,21 +31456,21 @@ function parsePayloadMemory(raw) {
31259
31456
 
31260
31457
  // src/memory/embedder/client.ts
31261
31458
  import { connect } from "node:net";
31262
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync7, rmSync as rmSync6, statSync as statSync2 } from "node:fs";
31459
+ import { existsSync as existsSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync7, rmSync as rmSync6, statSync as statSync2 } from "node:fs";
31263
31460
  import { fileURLToPath } from "node:url";
31264
31461
 
31265
31462
  // src/memory/embedder/download.ts
31266
31463
  import { createHash as createHash2, randomBytes as randomBytes6 } from "node:crypto";
31267
- import { chmodSync as chmodSync2, rmSync as rmSync2 } from "node:fs";
31464
+ import { chmodSync as chmodSync3, rmSync as rmSync2 } from "node:fs";
31268
31465
  import { open } from "node:fs/promises";
31269
- import { join as join7 } from "node:path";
31466
+ import { join as join8 } from "node:path";
31270
31467
 
31271
31468
  // src/memory/embedder/layout.ts
31272
- import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync } from "node:fs";
31469
+ import { chmodSync as chmodSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync2, renameSync, rmSync } from "node:fs";
31273
31470
  import { homedir as homedir2 } from "node:os";
31274
- import { basename as basename2, dirname, join as join6 } from "node:path";
31275
- var defaultModelsRoot = (home = homedir2()) => join6(home, ".securecoms", "models", "embeddings");
31276
- var filePath = (root3, modelId, f) => join6(root3, modelId, f.sha256, f.name);
31471
+ import { basename as basename3, dirname as dirname2, join as join7 } from "node:path";
31472
+ var defaultModelsRoot = (home = homedir2()) => join7(home, ".securecoms", "models", "embeddings");
31473
+ var filePath = (root3, modelId, f) => join7(root3, modelId, f.sha256, f.name);
31277
31474
  var LayoutRefused = class extends Error {
31278
31475
  constructor() {
31279
31476
  super("the model directory is not a plain directory");
@@ -31281,32 +31478,32 @@ var LayoutRefused = class extends Error {
31281
31478
  };
31282
31479
  function kind(p) {
31283
31480
  try {
31284
- return lstatSync(p).isDirectory() ? "dir" : "other";
31481
+ return lstatSync2(p).isDirectory() ? "dir" : "other";
31285
31482
  } catch (e) {
31286
31483
  if (e.code === "ENOENT") return "absent";
31287
31484
  throw e;
31288
31485
  }
31289
31486
  }
31290
31487
  function plainDirChain(root3, parts, opts) {
31291
- const modelsDir = dirname(root3);
31488
+ const modelsDir = dirname2(root3);
31292
31489
  for (const part of parts) if (!part || part === "." || part === ".." || part.includes("/") || part.includes("\\")) throw new LayoutRefused();
31293
31490
  const k = kind(modelsDir);
31294
31491
  if (k === "other") throw new LayoutRefused();
31295
31492
  if (k === "absent") {
31296
31493
  if (!opts.create) return null;
31297
- mkdirSync(modelsDir, { recursive: true, mode: 448 });
31494
+ mkdirSync2(modelsDir, { recursive: true, mode: 448 });
31298
31495
  if (kind(modelsDir) !== "dir") throw new LayoutRefused();
31299
31496
  }
31300
- const chain = opts.fromModelsDir ? parts : [basename2(root3), ...parts];
31497
+ const chain = opts.fromModelsDir ? parts : [basename3(root3), ...parts];
31301
31498
  let p = modelsDir;
31302
31499
  for (let i = 0; i < chain.length; i++) {
31303
- p = join6(p, chain[i]);
31500
+ p = join7(p, chain[i]);
31304
31501
  const c = kind(p);
31305
31502
  if (c === "other") throw new LayoutRefused();
31306
31503
  if (c === "absent") {
31307
31504
  if (!opts.create) return null;
31308
31505
  try {
31309
- mkdirSync(p, { mode: i === chain.length - 1 && opts.mode !== void 0 ? opts.mode : 493 });
31506
+ mkdirSync2(p, { mode: i === chain.length - 1 && opts.mode !== void 0 ? opts.mode : 493 });
31310
31507
  } catch (e) {
31311
31508
  if (e.code !== "EEXIST") throw e;
31312
31509
  }
@@ -31317,7 +31514,7 @@ function plainDirChain(root3, parts, opts) {
31317
31514
  }
31318
31515
  function presentWithSize(p, size) {
31319
31516
  try {
31320
- const s = lstatSync(p);
31517
+ const s = lstatSync2(p);
31321
31518
  return s.isFile() && s.size === size;
31322
31519
  } catch {
31323
31520
  return false;
@@ -31325,7 +31522,7 @@ function presentWithSize(p, size) {
31325
31522
  }
31326
31523
  function wrongTypeAt(p) {
31327
31524
  try {
31328
- const s = lstatSync(p);
31525
+ const s = lstatSync2(p);
31329
31526
  return !s.isFile() && !s.isSymbolicLink();
31330
31527
  } catch {
31331
31528
  return false;
@@ -31338,16 +31535,16 @@ function installVerified(root3, modelId, f, partial, opts) {
31338
31535
  dir = plainDirChain(root3, [modelId, f.sha256], { create: true });
31339
31536
  if (!dir) throw new LayoutRefused();
31340
31537
  if (!opts.replace && presentWithSize(dest, f.size)) return;
31341
- chmodSync(dir, 493);
31538
+ chmodSync2(dir, 493);
31342
31539
  try {
31343
31540
  renameSync(partial, dest);
31344
31541
  } catch (e) {
31345
31542
  if (!presentWithSize(dest, f.size)) throw e;
31346
31543
  return;
31347
31544
  }
31348
- chmodSync(dest, 292);
31545
+ chmodSync2(dest, 292);
31349
31546
  } finally {
31350
- if (dir) chmodSync(dir, 365);
31547
+ if (dir) chmodSync2(dir, 365);
31351
31548
  rmSync(partial, { force: true });
31352
31549
  }
31353
31550
  }
@@ -31367,8 +31564,8 @@ function removeModelFiles(root3, m) {
31367
31564
  for (const f of m.files) {
31368
31565
  const dir = plainDirChain(root3, [m.modelId, f.sha256], { create: false });
31369
31566
  if (!dir) continue;
31370
- chmodSync2(dir, 493);
31371
- rmSync2(join7(dir, f.name), { force: true });
31567
+ chmodSync3(dir, 493);
31568
+ rmSync2(join8(dir, f.name), { force: true });
31372
31569
  }
31373
31570
  }
31374
31571
  var VerifyFailed = class extends Error {
@@ -31415,7 +31612,7 @@ async function fetchSameOrigin(url, origin, fetchImpl, signal, stallMs, abort) {
31415
31612
  async function downloadOne(f, o) {
31416
31613
  if (wrongTypeAt(filePath(o.root, o.modelId, f))) throw new WrongType("wrong type at model path");
31417
31614
  const pdir = plainDirChain(o.root, [".partial"], { create: true, fromModelsDir: true, mode: 448 });
31418
- const partial = join7(pdir, randomBytes6(12).toString("hex"));
31615
+ const partial = join8(pdir, randomBytes6(12).toString("hex"));
31419
31616
  const fh = await open(partial, "wx", 384);
31420
31617
  const ac = new AbortController();
31421
31618
  const abort = () => ac.abort();
@@ -31491,17 +31688,17 @@ async function ensureModelFiles(o) {
31491
31688
  }
31492
31689
 
31493
31690
  // src/memory/embedder/gc.ts
31494
- import { chmodSync as chmodSync3, closeSync, constants, fstatSync, futimesSync, lstatSync as lstatSync2, openSync, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
31495
- import { join as join8 } from "node:path";
31691
+ import { chmodSync as chmodSync4, closeSync, constants as constants2, fstatSync, futimesSync, lstatSync as lstatSync3, openSync, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
31692
+ import { join as join9 } from "node:path";
31496
31693
  var THIRTY_DAYS = 30 * 864e5;
31497
31694
  var SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
31498
31695
  function touchUserMarker(root3, modelId, identityId) {
31499
31696
  if (!SAFE_ID.test(identityId)) throw new Error("invalid identity id for the model marker");
31500
31697
  const dir = plainDirChain(root3, [modelId, ".users"], { create: true });
31501
- const p = join8(dir, identityId);
31698
+ const p = join9(dir, identityId);
31502
31699
  let fd;
31503
31700
  try {
31504
- fd = openSync(p, constants.O_WRONLY | constants.O_CREAT | constants.O_NOFOLLOW | constants.O_NONBLOCK, 420);
31701
+ fd = openSync(p, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_NOFOLLOW | constants2.O_NONBLOCK, 420);
31505
31702
  } catch {
31506
31703
  throw new Error("the model marker is not a regular file");
31507
31704
  }
@@ -31515,22 +31712,22 @@ function touchUserMarker(root3, modelId, identityId) {
31515
31712
  }
31516
31713
  function isRealDir(p) {
31517
31714
  try {
31518
- return lstatSync2(p).isDirectory();
31715
+ return lstatSync3(p).isDirectory();
31519
31716
  } catch {
31520
31717
  return false;
31521
31718
  }
31522
31719
  }
31523
31720
  function makeWritable(p) {
31524
31721
  if (!isRealDir(p)) return;
31525
- chmodSync3(p, 493);
31526
- for (const e of readdirSync2(p)) makeWritable(join8(p, e));
31722
+ chmodSync4(p, 493);
31723
+ for (const e of readdirSync2(p)) makeWritable(join9(p, e));
31527
31724
  }
31528
31725
  function newestMarkerMs(users) {
31529
31726
  if (!isRealDir(users)) return void 0;
31530
31727
  let newest;
31531
31728
  for (const u of readdirSync2(users)) {
31532
31729
  try {
31533
- const s = lstatSync2(join8(users, u));
31730
+ const s = lstatSync3(join9(users, u));
31534
31731
  if (!s.isFile()) continue;
31535
31732
  newest = newest === void 0 ? s.mtimeMs : Math.max(newest, s.mtimeMs);
31536
31733
  } catch {
@@ -31543,9 +31740,9 @@ function gcModels(root3, keepModelIds, now, maxAgeMs = THIRTY_DAYS) {
31543
31740
  const removed = [];
31544
31741
  for (const modelId of readdirSync2(root3).sort()) {
31545
31742
  if (modelId.startsWith(".") || keepModelIds.includes(modelId)) continue;
31546
- const tree = join8(root3, modelId);
31743
+ const tree = join9(root3, modelId);
31547
31744
  if (!isRealDir(tree)) continue;
31548
- const lastUse = newestMarkerMs(join8(tree, ".users")) ?? lstatSync2(tree).mtimeMs;
31745
+ const lastUse = newestMarkerMs(join9(tree, ".users")) ?? lstatSync3(tree).mtimeMs;
31549
31746
  if (now - lastUse <= maxAgeMs) continue;
31550
31747
  makeWritable(tree);
31551
31748
  rmSync3(tree, { recursive: true, force: true });
@@ -31678,44 +31875,44 @@ var SealedChannel = class {
31678
31875
  // src/memory/embedder/embedder-paths.ts
31679
31876
  import { createHash as createHash3 } from "node:crypto";
31680
31877
  import { homedir as homedir3 } from "node:os";
31681
- import { join as join9 } from "node:path";
31878
+ import { join as join10 } from "node:path";
31682
31879
  function embedderKey(m) {
31683
31880
  const model = m.files.find((f) => f.name === "model.onnx");
31684
31881
  return createHash3("sha256").update(`${EMBEDDER_PROTOCOL}\0${m.modelId}\0${model.sha256}\0${m.wasmSha256}`).digest("hex").slice(0, 16);
31685
31882
  }
31686
- var embedderRunDir = (home = homedir3()) => join9(home, ".securecoms", "run", "embedder");
31883
+ var embedderRunDir = (home = homedir3()) => join10(home, ".securecoms", "run", "embedder");
31687
31884
  var embedderPaths = (dir, key) => ({
31688
- sock: join9(dir, `${key}.sock`),
31689
- token: join9(dir, `${key}.token`),
31690
- lock: join9(dir, `${key}.spawn.lock`),
31691
- status: join9(dir, `${key}.status`),
31692
- pid: join9(dir, `${key}.pid`)
31885
+ sock: join10(dir, `${key}.sock`),
31886
+ token: join10(dir, `${key}.token`),
31887
+ lock: join10(dir, `${key}.spawn.lock`),
31888
+ status: join10(dir, `${key}.status`),
31889
+ pid: join10(dir, `${key}.pid`)
31693
31890
  // E1 delta H1: the instance's lifetime lock
31694
31891
  });
31695
31892
  var SOCKET_PATH_MAX = 103;
31696
31893
  var windowsRunDir = (env = process.env) => {
31697
31894
  if (!env.LOCALAPPDATA) throw new Error("LOCALAPPDATA unset");
31698
- return join9(env.LOCALAPPDATA, "SecureComs", "run", "embedder");
31895
+ return join10(env.LOCALAPPDATA, "SecureComs", "run", "embedder");
31699
31896
  };
31700
31897
 
31701
31898
  // src/memory/embedder/spawn.ts
31702
31899
  import { spawn } from "node:child_process";
31703
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, realpathSync } from "node:fs";
31900
+ import { existsSync as existsSync2, mkdirSync as mkdirSync4, realpathSync as realpathSync2 } from "node:fs";
31704
31901
 
31705
31902
  // src/memory/embedder/run-dir.ts
31706
- import { existsSync, lstatSync as lstatSync3, mkdirSync as mkdirSync2 } from "node:fs";
31707
- import { dirname as dirname2 } from "node:path";
31903
+ import { existsSync, lstatSync as lstatSync4, mkdirSync as mkdirSync3 } from "node:fs";
31904
+ import { dirname as dirname3 } from "node:path";
31708
31905
  var RunDirRefused = class extends Error {
31709
31906
  };
31710
- function ensureRunDir(dir, uid = process.getuid?.() ?? -1, lstat = lstatSync3) {
31711
- const parent3 = dirname2(dir);
31712
- if (!existsSync(parent3)) mkdirSync2(parent3, { recursive: true, mode: 448 });
31907
+ function ensureRunDir(dir, uid = process.getuid?.() ?? -1, lstat = lstatSync4) {
31908
+ const parent3 = dirname3(dir);
31909
+ if (!existsSync(parent3)) mkdirSync3(parent3, { recursive: true, mode: 448 });
31713
31910
  const ps = lstat(parent3);
31714
31911
  if (ps.isSymbolicLink() || !ps.isDirectory()) throw new RunDirRefused("run directory parent is not a real directory");
31715
31912
  if (ps.uid !== uid) throw new RunDirRefused("run directory parent is not owned by this user");
31716
31913
  if ((ps.mode & 18) !== 0) throw new RunDirRefused("run directory parent is group or world writable");
31717
31914
  try {
31718
- mkdirSync2(dir, { mode: 448 });
31915
+ mkdirSync3(dir, { mode: 448 });
31719
31916
  } catch (e) {
31720
31917
  if (e.code !== "EEXIST") throw e;
31721
31918
  }
@@ -31726,10 +31923,10 @@ function ensureRunDir(dir, uid = process.getuid?.() ?? -1, lstat = lstatSync3) {
31726
31923
  }
31727
31924
 
31728
31925
  // src/memory/embedder/spawn.ts
31729
- import { basename as basename3, dirname as dirname3, join as join10 } from "node:path";
31926
+ import { basename as basename4, dirname as dirname4, join as join11 } from "node:path";
31730
31927
 
31731
31928
  // src/memory/embedder/locks.ts
31732
- import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, linkSync, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync4, writeSync } from "node:fs";
31929
+ import { closeSync as closeSync2, constants as constants3, fstatSync as fstatSync2, linkSync, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync4, writeSync } from "node:fs";
31733
31930
  import { randomBytes as randomBytes9 } from "node:crypto";
31734
31931
  import { uptime } from "node:os";
31735
31932
  var LOCK_STALE_MS = 1e4;
@@ -31751,7 +31948,7 @@ var PROCESS_LOCK_ENV = {
31751
31948
  function inspect(path) {
31752
31949
  let fd;
31753
31950
  try {
31754
- fd = openSync2(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
31951
+ fd = openSync2(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
31755
31952
  } catch {
31756
31953
  return null;
31757
31954
  }
@@ -31775,7 +31972,7 @@ function takeLock(path, now = Date.now, env = PROCESS_LOCK_ENV) {
31775
31972
  const id = `${env.pid} ${env.bootTime()} ${env.startTime()} ${randomBytes9(8).toString("hex")}`;
31776
31973
  for (let attempt = 0; attempt < 3; attempt++) {
31777
31974
  try {
31778
- const fd = openSync2(path, constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY, 384);
31975
+ const fd = openSync2(path, constants3.O_CREAT | constants3.O_EXCL | constants3.O_WRONLY, 384);
31779
31976
  try {
31780
31977
  writeSync(fd, id);
31781
31978
  } finally {
@@ -31836,7 +32033,7 @@ function embedderEnv(env = process.env, platform = process.platform) {
31836
32033
  }
31837
32034
  var withReal = (p) => {
31838
32035
  try {
31839
- const r = realpathSync(p);
32036
+ const r = realpathSync2(p);
31840
32037
  return r === p ? [p] : [p, r];
31841
32038
  } catch {
31842
32039
  return [p];
@@ -31844,10 +32041,10 @@ var withReal = (p) => {
31844
32041
  };
31845
32042
  var realish = (p) => {
31846
32043
  try {
31847
- return realpathSync(p);
32044
+ return realpathSync2(p);
31848
32045
  } catch {
31849
- const up = dirname3(p);
31850
- return up === p ? p : join10(realish(up), basename3(p));
32046
+ const up = dirname4(p);
32047
+ return up === p ? p : join11(realish(up), basename4(p));
31851
32048
  }
31852
32049
  };
31853
32050
  var both = (p) => {
@@ -31859,7 +32056,7 @@ function spawnEmbedder(o) {
31859
32056
  const run = o.runDir ?? embedderRunDir(o.home);
31860
32057
  const win = (o.platform ?? process.platform) === "win32";
31861
32058
  if (o.createRunDir !== false) {
31862
- if (win) mkdirSync3(run, { recursive: true, mode: 448 });
32059
+ if (win) mkdirSync4(run, { recursive: true, mode: 448 });
31863
32060
  else ensureRunDir(run);
31864
32061
  }
31865
32062
  const perm = permissionFlags();
@@ -31868,13 +32065,13 @@ function spawnEmbedder(o) {
31868
32065
  o.onNoPermissionModel?.();
31869
32066
  }
31870
32067
  const reads = perm.length === 0 ? [] : [
31871
- ...withReal(dirname3(o.distDir)),
32068
+ ...withReal(dirname4(o.distDir)),
31872
32069
  ...existsSync2(defaultModelsRoot(o.home)) ? withReal(defaultModelsRoot(o.home)) : [defaultModelsRoot(o.home)],
31873
- ...both(dirname3(run)),
31874
- ...win ? both(join10(o.home, ".securecoms", "run")) : []
32070
+ ...both(dirname4(run)),
32071
+ ...win ? both(join11(o.home, ".securecoms", "run")) : []
31875
32072
  // the voice-call heartbeats (the run dir lives elsewhere)
31876
32073
  ];
31877
- const script = withReal(join10(o.distDir, "embedder.js")).at(-1);
32074
+ const script = withReal(join11(o.distDir, "embedder.js")).at(-1);
31878
32075
  const net = (o.nodeFlags ?? process.allowedNodeEnvironmentFlags).has("--allow-net") ? ["--allow-net"] : [];
31879
32076
  const flags = perm.length === 0 ? [] : [...perm, ...reads.map((r) => `--allow-fs-read=${r}`), ...both(run).map((w) => `--allow-fs-write=${w}`), "--allow-worker", ...net];
31880
32077
  const child = (o.spawnFn ?? spawn)(
@@ -31883,7 +32080,7 @@ function spawnEmbedder(o) {
31883
32080
  // 11c review LOW 3, fixed by the Task 12 Mini gate: cwd is the run directory's PARENT, which the bridge created (0700)
31884
32081
  // before spawning; the run directory itself is created by the embedder, so on a first start it does not exist yet and
31885
32082
  // a cwd there fails the spawn with ENOENT. Never the bridge's working directory.
31886
- { detached: true, stdio: "ignore", windowsHide: true, cwd: dirname3(run), env: win ? { ...embedderEnv(process.env, "win32"), USERPROFILE: o.home, LOCALAPPDATA: dirname3(dirname3(dirname3(run))) } : { ...embedderEnv(), HOME: o.home } }
32083
+ { detached: true, stdio: "ignore", windowsHide: true, cwd: dirname4(run), env: win ? { ...embedderEnv(process.env, "win32"), USERPROFILE: o.home, LOCALAPPDATA: dirname4(dirname4(dirname4(run))) } : { ...embedderEnv(), HOME: o.home } }
31887
32084
  );
31888
32085
  child.unref();
31889
32086
  child.on("error", () => {
@@ -31895,8 +32092,8 @@ function spawnEmbedder(o) {
31895
32092
  import { execFile } from "node:child_process";
31896
32093
  import { promisify } from "node:util";
31897
32094
  import { randomBytes as randomBytes10 } from "node:crypto";
31898
- import { lstatSync as lstatSync4, readFileSync as readFileSync6, rmSync as rmSync5 } from "node:fs";
31899
- import { join as join11, win32 } from "node:path";
32095
+ import { lstatSync as lstatSync5, readFileSync as readFileSync6, rmSync as rmSync5 } from "node:fs";
32096
+ import { join as join12, win32 } from "node:path";
31900
32097
  var WINDOWS_SEMANTIC_ENABLED = false;
31901
32098
  var windowsSemanticOn = (platform, enabled = WINDOWS_SEMANTIC_ENABLED) => platform !== "win32" || enabled;
31902
32099
  function parseSaveSddl(saveFile) {
@@ -31924,7 +32121,7 @@ function aclAllows(sddl, ownerSid) {
31924
32121
  if (aces.length === 0 || rest !== "" && !rest.startsWith("S:")) return false;
31925
32122
  return aces.every((a) => a[0] === "A" && allowed.has(a[5] ?? "") || a[0] === "D");
31926
32123
  }
31927
- function hasReparsePoint(path, lstat = lstatSync4) {
32124
+ function hasReparsePoint(path, lstat = lstatSync5) {
31928
32125
  const root3 = win32.parse(path).root;
31929
32126
  let cur = root3;
31930
32127
  for (const part of path.slice(root3.length).split(/[\\/]/).filter(Boolean)) {
@@ -31943,7 +32140,7 @@ async function currentUserSid(run = defaultRunner) {
31943
32140
  }
31944
32141
  async function verifyRunDirAcl(path, deps = {}, sid) {
31945
32142
  const run = deps.run ?? defaultRunner;
31946
- const out = join11(deps.tmp ?? process.env.TEMP ?? process.env.TMP ?? ".", `sc-acl-${randomBytes10(8).toString("hex")}.txt`);
32143
+ const out = join12(deps.tmp ?? process.env.TEMP ?? process.env.TMP ?? ".", `sc-acl-${randomBytes10(8).toString("hex")}.txt`);
31947
32144
  try {
31948
32145
  if (hasReparsePoint(path, deps.lstat)) return false;
31949
32146
  const user = await (sid ? sid() : currentUserSid(run));
@@ -32024,12 +32221,12 @@ var EmbedderClient = class {
32024
32221
  ready(ms) {
32025
32222
  if (this.st.state === "ready") return Promise.resolve(true);
32026
32223
  if (this.st.state !== "preparing" || !(ms > 0)) return Promise.resolve(false);
32027
- return new Promise((resolve) => {
32224
+ return new Promise((resolve2) => {
32028
32225
  let t;
32029
32226
  const w = (ok) => {
32030
32227
  clearTimeout(t);
32031
32228
  this.readyWaiters.delete(w);
32032
- resolve(ok);
32229
+ resolve2(ok);
32033
32230
  };
32034
32231
  this.readyWaiters.add(w);
32035
32232
  t = setTimeout(() => w(false), ms);
@@ -32138,7 +32335,7 @@ var EmbedderClient = class {
32138
32335
  const p = this.paths();
32139
32336
  if (!this.tcp() && Buffer.byteLength(`${p.sock}.00000000`) > SOCKET_PATH_MAX) return this.fail("run_dir_refused");
32140
32337
  if (this.tcp()) {
32141
- mkdirSync4(this.runDir(), { recursive: true, mode: 448 });
32338
+ mkdirSync5(this.runDir(), { recursive: true, mode: 448 });
32142
32339
  if (!await this.aclOk(true)) return this.fail("acl_unverified");
32143
32340
  }
32144
32341
  if (!this.tcp()) {
@@ -32274,7 +32471,7 @@ var EmbedderClient = class {
32274
32471
  }
32275
32472
  if (this.closed) return false;
32276
32473
  }
32277
- return new Promise((resolve) => {
32474
+ return new Promise((resolve2) => {
32278
32475
  const s = this.tcp() ? connect({ host: "127.0.0.1", port: pub.port }) : connect(p.sock);
32279
32476
  const nc = nonce();
32280
32477
  let ns = "";
@@ -32287,7 +32484,7 @@ var EmbedderClient = class {
32287
32484
  if (!settled) {
32288
32485
  settled = true;
32289
32486
  clearTimeout(t);
32290
- resolve(ok);
32487
+ resolve2(ok);
32291
32488
  }
32292
32489
  }
32293
32490
  const refuse2 = (reason) => {
@@ -32852,12 +33049,12 @@ var ReembedQueue = class {
32852
33049
  };
32853
33050
 
32854
33051
  // src/memory/recall/call-heartbeat.ts
32855
- import { closeSync as closeSync3, constants as constants3, fstatSync as fstatSync3, futimesSync as futimesSync2, openSync as openSync3, rmSync as rmSync7 } from "node:fs";
32856
- import { join as join13 } from "node:path";
33052
+ import { closeSync as closeSync3, constants as constants4, fstatSync as fstatSync3, futimesSync as futimesSync2, openSync as openSync3, rmSync as rmSync7 } from "node:fs";
33053
+ import { join as join14 } from "node:path";
32857
33054
 
32858
33055
  // src/memory/embedder/heartbeats.ts
32859
- import { lstatSync as lstatSync5, readdirSync as readdirSync3 } from "node:fs";
32860
- import { join as join12 } from "node:path";
33056
+ import { lstatSync as lstatSync6, readdirSync as readdirSync3 } from "node:fs";
33057
+ import { join as join13 } from "node:path";
32861
33058
  var HEARTBEAT_FUTURE_SKEW_MS = 6e4;
32862
33059
  var HEARTBEAT_STALE_MS = 3e4;
32863
33060
  function heartbeatAgeFresh(ageMs, staleMs = HEARTBEAT_STALE_MS) {
@@ -32872,7 +33069,7 @@ function anyHeartbeatFresh(dir, now, staleMs = HEARTBEAT_STALE_MS) {
32872
33069
  }
32873
33070
  return names.some((n) => {
32874
33071
  try {
32875
- const st = lstatSync5(join12(dir, n));
33072
+ const st = lstatSync6(join13(dir, n));
32876
33073
  return st.isFile() && heartbeatAgeFresh(now - st.mtimeMs, staleMs);
32877
33074
  } catch {
32878
33075
  return false;
@@ -32938,7 +33135,7 @@ var CallHeartbeat = class {
32938
33135
  if (dir === null) return;
32939
33136
  let fd;
32940
33137
  try {
32941
- fd = openSync3(join13(dir, this.identityId), constants3.O_WRONLY | constants3.O_CREAT | constants3.O_NOFOLLOW | constants3.O_NONBLOCK, 384);
33138
+ fd = openSync3(join14(dir, this.identityId), constants4.O_WRONLY | constants4.O_CREAT | constants4.O_NOFOLLOW | constants4.O_NONBLOCK, 384);
32942
33139
  if (!fstatSync3(fd).isFile()) return;
32943
33140
  const t = new Date(this.now());
32944
33141
  futimesSync2(fd, t, t);
@@ -32953,7 +33150,7 @@ var CallHeartbeat = class {
32953
33150
  const dir = SAFE_ID2.test(this.identityId) ? this.dirOk() : null;
32954
33151
  if (dir === null) return;
32955
33152
  try {
32956
- rmSync7(join13(dir, this.identityId), { force: true });
33153
+ rmSync7(join14(dir, this.identityId), { force: true });
32957
33154
  this.wrote = false;
32958
33155
  } catch (e) {
32959
33156
  this.log("warn", "memory.call_heartbeat_failed", {}, e);
@@ -33016,9 +33213,9 @@ function queryTokenMacs(key, text, max = QUERY_MACS_MAX) {
33016
33213
 
33017
33214
  // src/memory/unified/ranking-client.ts
33018
33215
  import { Worker } from "node:worker_threads";
33019
- import { join as join14 } from "node:path";
33216
+ import { join as join15 } from "node:path";
33020
33217
  function workerTransport(init, distDir) {
33021
- const w = new Worker(join14(distDir, "memory-worker.js"), { workerData: init });
33218
+ const w = new Worker(join15(distDir, "memory-worker.js"), { workerData: init });
33022
33219
  return {
33023
33220
  post: (m) => w.postMessage(m),
33024
33221
  onMessage: (cb) => {
@@ -33078,15 +33275,15 @@ var RankingClient = class {
33078
33275
  ask(build, deadlineAt) {
33079
33276
  const id = ++this.seq;
33080
33277
  if (this.closed) return Promise.resolve("failed");
33081
- return new Promise((resolve) => {
33278
+ return new Promise((resolve2) => {
33082
33279
  const timer = setTimeout(() => {
33083
33280
  this.waiting.delete(id);
33084
33281
  this.misses++;
33085
- resolve("late");
33282
+ resolve2("late");
33086
33283
  }, Math.max(0, deadlineAt - Date.now()));
33087
33284
  this.waiting.set(id, { resolve: (m) => {
33088
33285
  clearTimeout(timer);
33089
- resolve(m);
33286
+ resolve2(m);
33090
33287
  } });
33091
33288
  try {
33092
33289
  this.transport().post(build(id));
@@ -33095,7 +33292,7 @@ var RankingClient = class {
33095
33292
  this.waiting.delete(id);
33096
33293
  this.misses++;
33097
33294
  (this.d.log ?? writeServiceLog2)("warn", "memory.ranking_worker_spawn_failed", void 0, e);
33098
- resolve("failed");
33295
+ resolve2("failed");
33099
33296
  }
33100
33297
  });
33101
33298
  }
@@ -34689,17 +34886,17 @@ async function runSummarize(opts) {
34689
34886
  // src/service/runtime-config-dir.ts
34690
34887
  import { createHash as createHash5, randomUUID } from "node:crypto";
34691
34888
  import {
34692
- chmodSync as chmodSync4,
34889
+ chmodSync as chmodSync5,
34693
34890
  existsSync as existsSync4,
34694
- lstatSync as lstatSync6,
34695
- mkdirSync as mkdirSync5,
34891
+ lstatSync as lstatSync7,
34892
+ mkdirSync as mkdirSync6,
34696
34893
  readFileSync as readFileSync8,
34697
34894
  renameSync as renameSync3,
34698
34895
  statSync as statSync3,
34699
34896
  unlinkSync,
34700
34897
  writeFileSync
34701
34898
  } from "node:fs";
34702
- import { basename as basename4, isAbsolute, join as join15 } from "node:path";
34899
+ import { basename as basename5, isAbsolute, join as join16 } from "node:path";
34703
34900
 
34704
34901
  // src/grants/search-providers.ts
34705
34902
  var SEARCH_PROVIDERS = /* @__PURE__ */ new Set(["brave", "tavily", "parallel", "exa", "firecrawl"]);
@@ -34711,18 +34908,18 @@ function isSearchProvider(value) {
34711
34908
  // src/service/runtime-config-dir.ts
34712
34909
  var MEMORY_FILE = "MEMORY.md";
34713
34910
  var REAL_FS = {
34714
- mkdir: (p, mode) => mkdirSync5(p, { recursive: true, mode }),
34715
- chmod: (p, mode) => chmodSync4(p, mode),
34911
+ mkdir: (p, mode) => mkdirSync6(p, { recursive: true, mode }),
34912
+ chmod: (p, mode) => chmodSync5(p, mode),
34716
34913
  writeFile: (p, data, mode) => writeFileSync(p, data, { mode }),
34717
34914
  rename: (a, b) => renameSync3(a, b),
34718
34915
  readFile: (p) => readFileSync8(p, "utf8"),
34719
34916
  unlink: (p) => unlinkSync(p)
34720
34917
  };
34721
34918
  function runtimeConfigDir(home, identityId) {
34722
- return join15(home, ".securecoms", "runtime-config", identityId);
34919
+ return join16(home, ".securecoms", "runtime-config", identityId);
34723
34920
  }
34724
34921
  function assertManagedName(name) {
34725
- if (!name || name.includes("/") || name.includes("\\") || name === "." || name === ".." || name !== basename4(name)) {
34922
+ if (!name || name.includes("/") || name.includes("\\") || name === "." || name === ".." || name !== basename5(name)) {
34726
34923
  throw new Error(`refusing managed config file name ${JSON.stringify(name)}: not a plain filename`);
34727
34924
  }
34728
34925
  if (isLocalOnlyName(name)) {
@@ -34736,19 +34933,19 @@ function isLocalOnlyName(name) {
34736
34933
  var MANIFEST_POLICY_FILE = "manifest-policy.json";
34737
34934
  function removeManagedLlmSource(home, identityId, deps = REAL_FS) {
34738
34935
  try {
34739
- deps.unlink(join15(runtimeConfigDir(home, identityId), "llm-source.json"));
34936
+ deps.unlink(join16(runtimeConfigDir(home, identityId), "llm-source.json"));
34740
34937
  } catch (e) {
34741
34938
  if (e?.code !== "ENOENT") throw e;
34742
34939
  }
34743
34940
  }
34744
34941
  function writeManifestPolicy(home, identityId, policy, deps = REAL_FS) {
34745
- const parent3 = join15(home, ".securecoms", "runtime-config");
34942
+ const parent3 = join16(home, ".securecoms", "runtime-config");
34746
34943
  const dir = runtimeConfigDir(home, identityId);
34747
- for (const d of [join15(home, ".securecoms"), parent3, dir]) {
34944
+ for (const d of [join16(home, ".securecoms"), parent3, dir]) {
34748
34945
  deps.mkdir(d, 448);
34749
34946
  deps.chmod(d, 448);
34750
34947
  }
34751
- atomicWrite(join15(dir, MANIFEST_POLICY_FILE), JSON.stringify(policy) + "\n", deps);
34948
+ atomicWrite(join16(dir, MANIFEST_POLICY_FILE), JSON.stringify(policy) + "\n", deps);
34752
34949
  }
34753
34950
  function atomicWrite(path, body, deps) {
34754
34951
  const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
@@ -34767,13 +34964,13 @@ function atomicWrite(path, body, deps) {
34767
34964
  function writeManagedFiles(home, identityId, files, deps = REAL_FS) {
34768
34965
  const entries = Object.entries(files);
34769
34966
  for (const [name] of entries) assertManagedName(name);
34770
- const parent3 = join15(home, ".securecoms", "runtime-config");
34967
+ const parent3 = join16(home, ".securecoms", "runtime-config");
34771
34968
  const dir = runtimeConfigDir(home, identityId);
34772
- for (const d of [join15(home, ".securecoms"), parent3, dir]) {
34969
+ for (const d of [join16(home, ".securecoms"), parent3, dir]) {
34773
34970
  deps.mkdir(d, 448);
34774
34971
  deps.chmod(d, 448);
34775
34972
  }
34776
- for (const [name, body] of entries) atomicWrite(join15(dir, name), body, deps);
34973
+ for (const [name, body] of entries) atomicWrite(join16(dir, name), body, deps);
34777
34974
  }
34778
34975
  function checkConfiguredPaths(skillsJson, statFn = (p) => {
34779
34976
  const s = statSync3(p);
@@ -34789,6 +34986,8 @@ function checkConfiguredPaths(skillsJson, statFn = (p) => {
34789
34986
  }
34790
34987
  const search = skillsJson.search;
34791
34988
  if (search?.keyFile) paths.push(search.keyFile);
34989
+ const weather = skillsJson.weather;
34990
+ if (weather?.keyFile) paths.push(weather.keyFile);
34792
34991
  return paths.map((path) => {
34793
34992
  let st;
34794
34993
  try {
@@ -34814,7 +35013,7 @@ function parseSecretPathsReport(raw) {
34814
35013
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
34815
35014
  const r = raw;
34816
35015
  const f = r.outside_by_field;
34817
- if (r.mode !== "report" && r.mode !== "enforce" || !isCount(r.inside) || !isCount(r.allowlisted) || !isCount(r.outside) || !f || typeof f !== "object" || Array.isArray(f) || !isCount(f.headers_file) || !isCount(f.postgres_url_file) || !isCount(f.search_key_file) || f.sqlite !== void 0 && !isCount(f.sqlite)) return void 0;
35016
+ if (r.mode !== "report" && r.mode !== "enforce" || !isCount(r.inside) || !isCount(r.allowlisted) || !isCount(r.outside) || !f || typeof f !== "object" || Array.isArray(f) || !isCount(f.headers_file) || !isCount(f.postgres_url_file) || !isCount(f.search_key_file) || f.sqlite !== void 0 && !isCount(f.sqlite) || f.weather_key_file !== void 0 && !isCount(f.weather_key_file)) return void 0;
34818
35017
  return {
34819
35018
  mode: r.mode,
34820
35019
  inside: r.inside,
@@ -34824,7 +35023,8 @@ function parseSecretPathsReport(raw) {
34824
35023
  headers_file: f.headers_file,
34825
35024
  postgres_url_file: f.postgres_url_file,
34826
35025
  search_key_file: f.search_key_file,
34827
- ...f.sqlite !== void 0 ? { sqlite: f.sqlite } : {}
35026
+ ...f.sqlite !== void 0 ? { sqlite: f.sqlite } : {},
35027
+ ...f.weather_key_file !== void 0 ? { weather_key_file: f.weather_key_file } : {}
34828
35028
  }
34829
35029
  };
34830
35030
  }
@@ -34849,7 +35049,7 @@ function localLegacySources(doc) {
34849
35049
  const channelId = source.channelId;
34850
35050
  if (kind2 !== "global-memory" && kind2 !== "conversation-history" && kind2 !== "captured-memory" || typeof path !== "string" || !isAbsolute(path) || channelId !== void 0 && typeof channelId !== "string") continue;
34851
35051
  try {
34852
- const st = lstatSync6(path);
35052
+ const st = lstatSync7(path);
34853
35053
  if (!st.isFile() || st.isSymbolicLink()) continue;
34854
35054
  if (st.size <= LEGACY_MEMORY_SOURCE_MAX_BYTES && st.size > contentBudget) continue;
34855
35055
  const oversize = st.size > LEGACY_MEMORY_SOURCE_MAX_BYTES;
@@ -34877,7 +35077,7 @@ function localLegacySources(doc) {
34877
35077
  return out;
34878
35078
  }
34879
35079
  function readLocalReport(home, identityId) {
34880
- const p = join15(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
35080
+ const p = join16(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
34881
35081
  if (!existsSync4(p)) return null;
34882
35082
  let doc;
34883
35083
  try {
@@ -34904,6 +35104,7 @@ function readLocalReport(home, identityId) {
34904
35104
  has_search_key: hasSearchKey,
34905
35105
  has_llm_key: hasLlmKey,
34906
35106
  has_extract_provider: doc.has_extract_provider === true,
35107
+ has_weather_key: doc.has_weather_key === true,
34907
35108
  ...ownerDmCap !== void 0 ? { owner_dm_search_cap_effective: ownerDmCap } : {},
34908
35109
  legacy_memory: localLegacySources(doc).map(({ path: _path, content: _content, ...source }) => source),
34909
35110
  secret_paths: parseSecretPathsReport(doc.secret_paths)
@@ -34911,7 +35112,7 @@ function readLocalReport(home, identityId) {
34911
35112
  }
34912
35113
  function deleteLegacyMemorySources(home, identityId, ids) {
34913
35114
  if (ids.length === 0) return 0;
34914
- const p = join15(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
35115
+ const p = join16(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
34915
35116
  if (!existsSync4(p)) return 0;
34916
35117
  let doc;
34917
35118
  try {
@@ -34954,7 +35155,7 @@ async function applyLegacyMemoryFactDecisions(home, identityId, rawDecisions, re
34954
35155
  group.set(decision.fact_hash, decision);
34955
35156
  bySource.set(decision.source_id, group);
34956
35157
  }
34957
- const reportPath = join15(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
35158
+ const reportPath = join16(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
34958
35159
  if (!existsSync4(reportPath)) return { sourcesDeleted: 0, factsAssigned: 0 };
34959
35160
  let doc;
34960
35161
  try {
@@ -34992,7 +35193,7 @@ async function applyLegacyMemoryFactDecisions(home, identityId, rawDecisions, re
34992
35193
  }
34993
35194
  function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
34994
35195
  const dir = runtimeConfigDir(home, identityId);
34995
- const path = join15(dir, MEMORY_FILE);
35196
+ const path = join16(dir, MEMORY_FILE);
34996
35197
  if (!existsSync4(path)) return 0;
34997
35198
  const drop = new Set(hashes);
34998
35199
  const before = deps.readFile(path).split("\n");
@@ -35005,6 +35206,19 @@ function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
35005
35206
  return removed;
35006
35207
  }
35007
35208
 
35209
+ // src/reply-credit.ts
35210
+ var REPLY_CREDIT_FEATURE = "reply_credit_v1";
35211
+ var WEATHER_CREDIT_LINE = "Weather data: WeatherAPI.com. For general information only.";
35212
+ var WEATHER_CREDIT_SUFFIX = `
35213
+
35214
+ ${WEATHER_CREDIT_LINE}`;
35215
+ function isReplyCredit(v) {
35216
+ return v === "weatherapi";
35217
+ }
35218
+ function stripCreditSuffix(text) {
35219
+ return text.endsWith(WEATHER_CREDIT_SUFFIX) ? text.slice(0, text.length - WEATHER_CREDIT_SUFFIX.length) : text;
35220
+ }
35221
+
35008
35222
  // src/grants/binding.ts
35009
35223
  import { sha256 as sha25611 } from "@noble/hashes/sha256";
35010
35224
 
@@ -35012,9 +35226,9 @@ import { sha256 as sha25611 } from "@noble/hashes/sha256";
35012
35226
  import { ed25519 as ed255195 } from "@noble/curves/ed25519";
35013
35227
 
35014
35228
  // src/grants/pin.ts
35015
- import { lstatSync as lstatSync7, readFileSync as readFileSync9 } from "node:fs";
35229
+ import { lstatSync as lstatSync8, readFileSync as readFileSync9 } from "node:fs";
35016
35230
  import { ed25519 as ed255194 } from "@noble/curves/ed25519";
35017
- import { dirname as dirname4, join as join16 } from "node:path";
35231
+ import { dirname as dirname5, join as join17 } from "node:path";
35018
35232
  var PIN_FILE_NAME = "owner-signing-key.json";
35019
35233
  var UUID_LOWER = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
35020
35234
  var PIN_KEYS = ["key_algo", "owner_identity_id", "public_key"];
@@ -35044,7 +35258,7 @@ function parsePinObject(v) {
35044
35258
  pin: Object.freeze({ ownerIdentityId: o.owner_identity_id, keyAlgo: "ed25519", publicKey, publicKeyB64: o.public_key })
35045
35259
  };
35046
35260
  }
35047
- var NODE_FS = { lstat: (p) => lstatSync7(p), readFile: (p) => readFileSync9(p, "utf8") };
35261
+ var NODE_FS = { lstat: (p) => lstatSync8(p), readFile: (p) => readFileSync9(p, "utf8") };
35048
35262
  function fromValue(v) {
35049
35263
  const r = parsePinObject(v);
35050
35264
  return r.ok ? { kind: "ok", pin: r.pin } : { kind: "bad", state: r.state };
@@ -35078,7 +35292,7 @@ function samePin(a, b) {
35078
35292
  }
35079
35293
  function loadOwnerGrantPin(opts) {
35080
35294
  const fs = opts.fs ?? NODE_FS;
35081
- const file2 = readPinFile(join16(dirname4(opts.credentialPath), PIN_FILE_NAME), fs);
35295
+ const file2 = readPinFile(join17(dirname5(opts.credentialPath), PIN_FILE_NAME), fs);
35082
35296
  const field = "owner_signing_key" in opts.credentialJson && opts.credentialJson.owner_signing_key !== void 0 ? fromValue(opts.credentialJson.owner_signing_key) : { kind: "absent" };
35083
35297
  if (file2.kind === "bad") return { state: file2.state, pin: null };
35084
35298
  if (field.kind === "bad") return { state: field.state, pin: null };
@@ -35093,9 +35307,10 @@ function loadOwnerGrantPin(opts) {
35093
35307
  // src/grants/manifest.ts
35094
35308
  var TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
35095
35309
  var MANIFEST_KNOWN_TOOLS = Object.freeze({
35096
- owner_dm: Object.freeze(["web_search", "web_extract", "fetch_url", "http_request", "sql_query"]),
35310
+ owner_dm: Object.freeze(["web_search", "web_extract", "fetch_url", "http_request", "sql_query", "weather_forecast", "currency_convert"]),
35097
35311
  // web_extract: owner DM only, never team_room (spec §5.4, D5)
35098
- team_room: Object.freeze(["web_search"])
35312
+ // Weather/currency spec §6.3 (U5): both utility tools in both audiences, each held behind utility_tools_v1 (TOOL_FEATURE).
35313
+ team_room: Object.freeze(["web_search", "weather_forecast", "currency_convert"])
35099
35314
  });
35100
35315
  function manifestSignedBytes(manifest) {
35101
35316
  const domain = new TextEncoder().encode(TOOL_GRANTS_DOMAIN);
@@ -35419,20 +35634,30 @@ function bindTargets(raw, signed, equals) {
35419
35634
  }
35420
35635
  return { written, mismatches };
35421
35636
  }
35422
- function signedManifestTools(manifest, audience, searchProvidersCapable) {
35423
- return MANIFEST_KNOWN_TOOLS[audience].filter((t) => (manifest.grants[audience] ?? []).includes(t) && (t !== "web_extract" || searchProvidersCapable));
35637
+ var NO_FEATURES = Object.freeze(/* @__PURE__ */ new Set());
35638
+ var TOOL_FEATURE = Object.freeze({
35639
+ web_extract: "search_providers_v2",
35640
+ weather_forecast: "utility_tools_v1",
35641
+ currency_convert: "utility_tools_v1"
35642
+ });
35643
+ function toolAllowedByFeatures(tool, features) {
35644
+ if (!Object.hasOwn(TOOL_FEATURE, tool)) return true;
35645
+ return features.has(TOOL_FEATURE[tool]);
35646
+ }
35647
+ function signedManifestTools(manifest, audience, features) {
35648
+ return MANIFEST_KNOWN_TOOLS[audience].filter((t) => (manifest.grants[audience] ?? []).includes(t) && toolAllowedByFeatures(t, features));
35424
35649
  }
35425
- function manifestPolicyFor(manifest, switches, searchProvidersCapable) {
35650
+ function manifestPolicyFor(manifest, switches, features) {
35426
35651
  return {
35427
35652
  v: 1,
35428
35653
  manifest_tools: [.../* @__PURE__ */ new Set([
35429
- ...switches.ownerDm ? signedManifestTools(manifest, "owner_dm", searchProvidersCapable) : [],
35430
- ...switches.room ? signedManifestTools(manifest, "team_room", searchProvidersCapable) : []
35654
+ ...switches.ownerDm ? signedManifestTools(manifest, "owner_dm", features) : [],
35655
+ ...switches.room ? signedManifestTools(manifest, "team_room", features) : []
35431
35656
  ])].sort(),
35432
35657
  secret_path_mode: "enforce"
35433
35658
  };
35434
35659
  }
35435
- function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true }, searchProvidersCapable = false) {
35660
+ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true }, features = NO_FEATURES) {
35436
35661
  const mismatches = /* @__PURE__ */ new Set();
35437
35662
  const cfg = manifest.config;
35438
35663
  const out = {};
@@ -35461,7 +35686,8 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
35461
35686
  for (const name of Object.keys(files)) {
35462
35687
  if (name !== LLM_SOURCE && !PERSONA_FILES.includes(name)) mismatches.add("files");
35463
35688
  }
35464
- const ownerDmTools = signedManifestTools(manifest, "owner_dm", searchProvidersCapable);
35689
+ const ownerDmTools = signedManifestTools(manifest, "owner_dm", features);
35690
+ const ownerDmSearchTools = ownerDmTools.filter((t) => t === "web_search" || t === "web_extract");
35465
35691
  const sj = isObj2(payload.skills_json) ? payload.skills_json : void 0;
35466
35692
  const eps = bindTargets(sj?.endpoints, manifest.endpoints, endpointEquals);
35467
35693
  const dbs = bindTargets(sj?.databases, manifest.databases, databaseEquals);
@@ -35482,17 +35708,20 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
35482
35708
  const usablePrior = cfg.search !== void 0 && prior.search?.provider === cfg.search.provider ? prior.search : void 0;
35483
35709
  searchBlock = cfg.search === void 0 ? void 0 : usablePrior ?? Object.freeze({ provider: cfg.search.provider });
35484
35710
  }
35711
+ const pw = isObj2(sj?.weather) ? sj.weather : void 0;
35712
+ const weatherBlock = typeof pw?.keyFile === "string" && pw.keyFile !== "" ? { keyFile: pw.keyFile } : void 0;
35485
35713
  const skillsDoc = {
35486
35714
  endpoints: eps.written,
35487
35715
  databases: dbs.written,
35488
35716
  flags: [...cfg.flags],
35489
35717
  ...deny !== void 0 || cfg.tools_allow !== void 0 ? { tools: { ...deny !== void 0 ? { deny } : {}, ...cfg.tools_allow !== void 0 ? { allow: [...cfg.tools_allow] } : {} } } : {},
35490
35718
  ...searchBlock ? { search: searchBlock } : {},
35719
+ ...weatherBlock ? { weather: weatherBlock } : {},
35491
35720
  // D31: the marker the runtime keys D24 (iii) on; written under every manifest, true when config.search is signed.
35492
35721
  search_signed: cfg.search !== void 0,
35493
35722
  // D36, D39: the signed owner-DM cap, only when the owner-DM switch is on and the manifest grants an owner-DM tool;
35494
35723
  // otherwise none, so the runtime clamps a signed raise to the default. Never read from the payload.
35495
- ...cfg.search?.owner_dm_daily_cap !== void 0 && switches.ownerDm && ownerDmTools.length > 0 ? { owner_dm_search_cap: cfg.search.owner_dm_daily_cap } : {}
35724
+ ...cfg.search?.owner_dm_daily_cap !== void 0 && switches.ownerDm && ownerDmSearchTools.length > 0 ? { owner_dm_search_cap: cfg.search.owner_dm_daily_cap } : {}
35496
35725
  };
35497
35726
  out["skills.json"] = JSON.stringify(skillsDoc, null, 2) + "\n";
35498
35727
  const signedMemory = { mode: cfg.memory.mode, ...cfg.memory.recall_days !== void 0 ? { recall_days: cfg.memory.recall_days } : {} };
@@ -35523,7 +35752,7 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
35523
35752
  mismatches.add("legacy_assign");
35524
35753
  legacyDecisions = decisions.filter((d) => isObj2(d) && d.action === "delete");
35525
35754
  }
35526
- const policy = manifestPolicyFor(manifest, switches, searchProvidersCapable);
35755
+ const policy = manifestPolicyFor(manifest, switches, features);
35527
35756
  return {
35528
35757
  mismatches: CONFIG_BINDING_FIELDS.filter((f) => mismatches.has(f)).sort(),
35529
35758
  targetMismatches: { endpoint: eps.mismatches, database: dbs.mismatches },
@@ -35666,7 +35895,11 @@ var TOOL_ROOM_CLASSES = Object.freeze({
35666
35895
  // D5: spends the owner's key and sends URLs out
35667
35896
  fetch_url: Object.freeze(["egress"]),
35668
35897
  sql_query: Object.freeze(["owner_credential"]),
35669
- http_request: Object.freeze(["owner_credential", "egress", "side_effect"])
35898
+ http_request: Object.freeze(["owner_credential", "egress", "side_effect"]),
35899
+ // Weather/currency (plan audit r1; supersedes spec §8's values): both send room words to an outside host, and weather
35900
+ // spends the owner's key.
35901
+ weather_forecast: Object.freeze(["owner_credential", "egress"]),
35902
+ currency_convert: Object.freeze(["egress"])
35670
35903
  });
35671
35904
  var LIVE_MEMBER_STATUSES = /* @__PURE__ */ new Set(["active", "add_pending"]);
35672
35905
  function isLiveRow(r) {
@@ -36083,8 +36316,8 @@ function voiceWindowEligible(t) {
36083
36316
  }
36084
36317
  function waitMs(ms) {
36085
36318
  if (ms <= 0) return Promise.resolve();
36086
- return new Promise((resolve) => {
36087
- const t = setTimeout(resolve, ms);
36319
+ return new Promise((resolve2) => {
36320
+ const t = setTimeout(resolve2, ms);
36088
36321
  t.unref?.();
36089
36322
  });
36090
36323
  }
@@ -36221,7 +36454,7 @@ var FileOwnerAuthorityStore = class {
36221
36454
  }
36222
36455
  async readAll() {
36223
36456
  try {
36224
- return JSON.parse(await fsp.readFile(this.path, "utf8"));
36457
+ return JSON.parse(await fsp2.readFile(this.path, "utf8"));
36225
36458
  } catch (e) {
36226
36459
  if (e.code === "ENOENT") return {};
36227
36460
  throw e;
@@ -36234,8 +36467,8 @@ var FileOwnerAuthorityStore = class {
36234
36467
  async save(enclaveId, pin) {
36235
36468
  const all = await this.readAll();
36236
36469
  all[enclaveId] = pin;
36237
- await fsp.mkdir(dirname5(this.path), { recursive: true });
36238
- await fsp.writeFile(this.path, JSON.stringify(all), "utf8");
36470
+ await fsp2.mkdir(dirname6(this.path), { recursive: true });
36471
+ await fsp2.writeFile(this.path, JSON.stringify(all), "utf8");
36239
36472
  }
36240
36473
  };
36241
36474
  var AgentPausedSendError = class extends Error {
@@ -36442,7 +36675,7 @@ var BridgeAgent = class _BridgeAgent {
36442
36675
  threshold: EMBEDDING_MANIFEST.similarityThreshold,
36443
36676
  warm: windowsSemanticOn(this.platform)
36444
36677
  };
36445
- return deps.rankingTransport ? deps.rankingTransport(init) : workerTransport(init, dirname5(fileURLToPath2(import.meta.url)));
36678
+ return deps.rankingTransport ? deps.rankingTransport(init) : workerTransport(init, dirname6(fileURLToPath2(import.meta.url)));
36446
36679
  }
36447
36680
  });
36448
36681
  this.embedder = deps.embedder ?? (deps.hostMachine === true && deps.home === void 0 ? new EmbedderClient({ identityId: cfg.identityId, home: homedir4(), source: cfg.modelSource, fetchImpl: this.fetchImpl, platform: this.platform }) : INERT_EMBEDDER);
@@ -36453,7 +36686,7 @@ var BridgeAgent = class _BridgeAgent {
36453
36686
  modelId: EMBEDDING_MANIFEST.modelId,
36454
36687
  threshold: EMBEDDING_MANIFEST.similarityThreshold
36455
36688
  });
36456
- this.heartbeat = new CallHeartbeat(this.machinePaths ? join17(this.home, ".securecoms", "run", "voice-call") : null, cfg.identityId, VOICE_LIVE_WINDOW_MS);
36689
+ this.heartbeat = new CallHeartbeat(this.machinePaths ? join18(this.home, ".securecoms", "run", "voice-call") : null, cfg.identityId, VOICE_LIVE_WINDOW_MS);
36457
36690
  this.reembed = new ReembedQueue({
36458
36691
  store: this.store,
36459
36692
  embedder: this.embedder,
@@ -36512,15 +36745,15 @@ var BridgeAgent = class _BridgeAgent {
36512
36745
  this.bootDrainWaitMs = deps.bootDrainWaitMs ?? BOOT_DRAIN_WAIT_MS_DEFAULT;
36513
36746
  this.searchCapabilityRefetchDebounceMs = deps.searchCapabilityRefetchDebounceMs ?? SEARCH_CAPABILITY_REFETCH_DEBOUNCE_MS;
36514
36747
  this.searchCapabilityRefetchMaxWaitMs = deps.searchCapabilityRefetchMaxWaitMs ?? SEARCH_CAPABILITY_REFETCH_MAX_WAIT_MS;
36515
- this.replySleep = deps.replySleep ?? ((ms) => new Promise((resolve) => {
36516
- const t = setTimeout(resolve, ms);
36748
+ this.replySleep = deps.replySleep ?? ((ms) => new Promise((resolve2) => {
36749
+ const t = setTimeout(resolve2, ms);
36517
36750
  t.unref?.();
36518
36751
  }));
36519
36752
  this.actionRateFloor = new TokenBucketFloor({ capacity: 1, refillPerMinute: 30, now: this.actionClock });
36520
36753
  this.globalActionRateFloor = new TokenBucketFloor({ capacity: 1, refillPerMinute: 30, now: this.actionClock });
36521
36754
  const self = this;
36522
36755
  this.downloadBlobImpl = deps.downloadBlob ?? async function(blobId) {
36523
- const url = `${cfg.apiBase}/api/v1/attachments/${encodeURIComponent(blobId)}?caller_identity_id=${encodeURIComponent(cfg.identityId)}`;
36756
+ const url = `${cfg.apiBase}/api/v1/attachments/${encodeURIComponent(assertBlobId(blobId))}?caller_identity_id=${encodeURIComponent(cfg.identityId)}`;
36524
36757
  const res = await fetchWithDeadline(self.fetchImpl, url, { headers: { authorization: `Bearer ${cfg.apiKey}` } }, {
36525
36758
  op: "attachment_download",
36526
36759
  timeoutMs: FETCH_DEADLINE_MS.blobBase,
@@ -36540,8 +36773,9 @@ var BridgeAgent = class _BridgeAgent {
36540
36773
  const j = await res.json();
36541
36774
  return String(j.attachment_id);
36542
36775
  };
36543
- this.attachmentsDirImpl = deps.attachmentsDir ?? join17(dirname5(cfg.storePath), "attachments");
36544
- this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join17(dirname5(cfg.storePath), "vault-owner-pins.json"));
36776
+ this.attachmentsDirImpl = deps.attachmentsDir ?? join18(dirname6(cfg.storePath), "attachments");
36777
+ this.vaultOutboxDirImpl = deps.vaultOutboxDir !== void 0 ? deps.vaultOutboxDir : cfg.storePath === ":memory:" ? null : join18(dirname6(cfg.storePath), VAULT_OUTBOX_DIRNAME);
36778
+ this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join18(dirname6(cfg.storePath), "vault-owner-pins.json"));
36545
36779
  this.action = {
36546
36780
  begin: (channelId) => this.actionBegin(channelId),
36547
36781
  start: (opts) => this.actionStart(opts),
@@ -37272,6 +37506,9 @@ var BridgeAgent = class _BridgeAgent {
37272
37506
  uploadBlobImpl;
37273
37507
  /** Track 5: directory where received attachments are saved. */
37274
37508
  attachmentsDirImpl;
37509
+ vaultOutboxDirImpl;
37510
+ /** Audit 2026-10-30 HIGH 2: the outbox pinned at start(); null = every upload refused. */
37511
+ outboxPin = null;
37275
37512
  /** B7: persistence for the vault kekset's owner-pin TOFU state. */
37276
37513
  vaultOwnerStoreImpl;
37277
37514
  /** B7: last-seen kekset generation per enclave, enforced monotonic across
@@ -37480,7 +37717,7 @@ var BridgeAgent = class _BridgeAgent {
37480
37717
  }
37481
37718
  let bound;
37482
37719
  try {
37483
- bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.searchProvidersCapable());
37720
+ bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.capabilityFeatures());
37484
37721
  } catch (e) {
37485
37722
  bound = null;
37486
37723
  return this.bindFailed(manifest, payload.memory, e);
@@ -37501,7 +37738,7 @@ var BridgeAgent = class _BridgeAgent {
37501
37738
  try {
37502
37739
  this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, bound.files);
37503
37740
  if (manifest.config.llm === void 0) this.grantWriters.removeManagedLlmSource(this.home, this.cfg.identityId);
37504
- this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.searchProvidersCapable()));
37741
+ this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.capabilityFeatures()));
37505
37742
  } catch (e) {
37506
37743
  this.manifestBinding = null;
37507
37744
  throw e;
@@ -37548,13 +37785,14 @@ var BridgeAgent = class _BridgeAgent {
37548
37785
  }
37549
37786
  /** No manifest in force: a manifest-policy.json left by an earlier one is replaced by an empty tool list. */
37550
37787
  overwriteStalePolicy() {
37551
- if (!existsSync5(join17(runtimeConfigDir(this.home, this.cfg.identityId), "manifest-policy.json"))) return;
37788
+ if (!existsSync5(join18(runtimeConfigDir(this.home, this.cfg.identityId), "manifest-policy.json"))) return;
37552
37789
  this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, { v: 1, manifest_tools: [], secret_path_mode: "enforce" });
37553
37790
  }
37554
37791
  /** The report half of refetchRuntimeConfigOnce (split out for the plan 4 Task 11 bind_failed path). */
37555
37792
  async postRuntimeConfigReport(base, version, skillsJson) {
37556
37793
  const local = readLocalReport(this.home, this.cfg.identityId);
37557
37794
  const reportedSearchProviders = this.searchProvidersCapable();
37795
+ const reportedUtilityTools = this.utilityToolsCapable();
37558
37796
  const report = {
37559
37797
  applied_version: version,
37560
37798
  paths: checkConfiguredPaths(skillsJson),
@@ -37572,6 +37810,9 @@ var BridgeAgent = class _BridgeAgent {
37572
37810
  // Final review I-2 (controller ruling): AND the current search_providers_v2, so the web never shows extract while
37573
37811
  // the bridge withholds web_extract from the runtime.
37574
37812
  has_extract_provider: (local?.has_extract_provider ?? false) && reportedSearchProviders,
37813
+ // Weather/currency L-1, §8 item 5: AND the current utility_tools_v1, so the web never shows a usable weather key
37814
+ // while the bridge withholds weather_forecast from the runtime.
37815
+ has_weather_key: (local?.has_weather_key ?? false) && reportedUtilityTools,
37575
37816
  // D39: the owner-DM daily cap the runtime enforces, passed through when it reported a valid one; never the signed value.
37576
37817
  ...local?.owner_dm_search_cap_effective !== void 0 ? { owner_dm_search_cap_effective: local.owner_dm_search_cap_effective } : {},
37577
37818
  // Same idea, for the BYOK LLM key file: only the runtime can see
@@ -37619,7 +37860,9 @@ var BridgeAgent = class _BridgeAgent {
37619
37860
  // D28 (§11.1 step 3): a separate key, never a value in the capability enum. False with no runtime attached.
37620
37861
  search_providers_v2: reportedSearchProviders,
37621
37862
  // D19 item 4: content-free; the signed provider is one the attached runtime(s) cannot honour.
37622
- search_provider_unsupported: this.searchProviderUnsupported(reportedSearchProviders)
37863
+ search_provider_unsupported: this.searchProviderUnsupported(reportedSearchProviders),
37864
+ // Weather/currency §8 item 5: a separate key, false with no runtime attached.
37865
+ utility_tools_v1: reportedUtilityTools
37623
37866
  }
37624
37867
  };
37625
37868
  const reportedVerifyFailures = this.ownerVerifyFailures;
@@ -37641,6 +37884,7 @@ var BridgeAgent = class _BridgeAgent {
37641
37884
  }
37642
37885
  this.ownerVerifyFailures = Math.max(0, this.ownerVerifyFailures - reportedVerifyFailures);
37643
37886
  this.lastReportedSearchProviders = reportedSearchProviders;
37887
+ this.lastReportedUtilityTools = reportedUtilityTools;
37644
37888
  if (version) writeServiceLog2("info", "runtime_config.applied", { version });
37645
37889
  else writeServiceLog2("info", "runtime_config.legacy_memory_reported");
37646
37890
  }
@@ -37708,9 +37952,16 @@ var BridgeAgent = class _BridgeAgent {
37708
37952
  }
37709
37953
  async startBody() {
37710
37954
  this.startedAt = Date.now();
37955
+ if (this.vaultOutboxDirImpl) {
37956
+ try {
37957
+ this.outboxPin = ensureOutbox(this.vaultOutboxDirImpl);
37958
+ } catch (e) {
37959
+ writeServiceLog2("warn", "vault_outbox.unavailable", {}, e);
37960
+ }
37961
+ }
37711
37962
  await this.restoreRuntimeInbox();
37712
37963
  await this.loadToolGrantState();
37713
- this.recomputeManifestPolicy(false);
37964
+ this.recomputeManifestPolicy(NO_FEATURES);
37714
37965
  try {
37715
37966
  this.store.pruneRoomSearchCap(utcDayOf(Date.now()));
37716
37967
  } catch (e) {
@@ -37733,8 +37984,8 @@ var BridgeAgent = class _BridgeAgent {
37733
37984
  const drain = this.ingestOnce().then(() => true, () => true);
37734
37985
  const drainedInTime = await Promise.race([
37735
37986
  drain,
37736
- new Promise((resolve) => {
37737
- const t = setTimeout(() => resolve(false), this.bootDrainWaitMs);
37987
+ new Promise((resolve2) => {
37988
+ const t = setTimeout(() => resolve2(false), this.bootDrainWaitMs);
37738
37989
  t.unref?.();
37739
37990
  void drain.then(() => clearTimeout(t));
37740
37991
  })
@@ -37744,7 +37995,7 @@ var BridgeAgent = class _BridgeAgent {
37744
37995
  await this.bootReconcileKeyPackage();
37745
37996
  } else {
37746
37997
  writeServiceLog2("warn", "boot.drain_slow", { wait_ms: this.bootDrainWaitMs });
37747
- const settled = new Promise((resolve) => this.ingestSettledWaiters.push(resolve));
37998
+ const settled = new Promise((resolve2) => this.ingestSettledWaiters.push(resolve2));
37748
37999
  this.deferredBootPublish = settled.then(async () => {
37749
38000
  this.bootDrainPending = false;
37750
38001
  if (this.closingDown) return;
@@ -38693,13 +38944,13 @@ var BridgeAgent = class _BridgeAgent {
38693
38944
  * absent, because the id is derived.
38694
38945
  */
38695
38946
  async send(channelId, text, messageId, opts = {}) {
38696
- const body = { kind: "text", text };
38947
+ const body = { kind: "text", text: opts.credit !== void 0 ? text + WEATHER_CREDIT_SUFFIX : text };
38697
38948
  if (this.isIdempotentSend(messageId, opts)) return this.trackReplyWork(this.sendIdempotent(channelId, body, opts));
38698
38949
  const id = resolveMessageId(messageId);
38699
38950
  const purgeEpochBeforePublish = this.store.conversationPurgeEpoch(channelId);
38700
38951
  const replyCapture = this.unifiedReplyCapture(channelId, opts.inReplyTo);
38701
38952
  await this.publishBody(channelId, body, id);
38702
- this.afterPublish(channelId, body, id, opts, purgeEpochBeforePublish, replyCapture);
38953
+ this.afterPublish(channelId, body, id, opts, purgeEpochBeforePublish, replyCapture, text);
38703
38954
  return { message_id: id, status: "sent" };
38704
38955
  }
38705
38956
  /** PR #716 review MEDIUM-5: register reply work so close() can wait for it. */
@@ -38718,7 +38969,7 @@ var BridgeAgent = class _BridgeAgent {
38718
38969
  const purgeEpochBeforePublish = this.store.conversationPurgeEpoch(channelId);
38719
38970
  const replyCapture = this.unifiedReplyCapture(channelId, opts.inReplyTo);
38720
38971
  await this.publishBody(channelId, body, id);
38721
- this.afterPublish(channelId, body, id, opts, purgeEpochBeforePublish, replyCapture);
38972
+ this.afterPublish(channelId, body, id, opts, purgeEpochBeforePublish, replyCapture, null);
38722
38973
  return { message_id: id, status: "sent" };
38723
38974
  }
38724
38975
  /**
@@ -38851,18 +39102,24 @@ var BridgeAgent = class _BridgeAgent {
38851
39102
  const meta = this.freshMeta(capChannel);
38852
39103
  if (meta && !meta.is_dm) this.enforceRateFloor("room", capChannel);
38853
39104
  }
38854
- /** Bookkeeping after a CONFIRMED publish of `body` under `id`. */
38855
- afterPublish(channelId, body, id, opts, purgeEpoch, replyCapture) {
39105
+ /**
39106
+ * Bookkeeping after a CONFIRMED publish of `body` under `id`. `recordText` (weather/currency Task 17) is what a text
39107
+ * reply is RECORDED as: the published text minus any credit line. Every record site below reads `recorded`, never
39108
+ * `body.text`. Every production caller passes it (pinned by reply-credit.test.ts: text, or null for an a2a body);
39109
+ * undefined is left only for direct test calls of an uncredited body, where the published and record texts are equal.
39110
+ */
39111
+ afterPublish(channelId, body, id, opts, purgeEpoch, replyCapture, recordText) {
39112
+ const recorded = recordText ?? (recordText === void 0 && body.kind === "text" ? body.text : "");
38856
39113
  if (opts.agentTurn !== false) this.agentHops.noteOwnSend(channelId);
38857
39114
  const modality = this.spokenReplyIds.has(id) ? "voice" : "text";
38858
- const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(body.text);
39115
+ const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(recorded);
38859
39116
  if (opts.inReplyTo !== void 0 && this.isOwnerVerifyFailed(opts.inReplyTo)) return;
38860
- if (body.kind === "text" && opts.callBufferReply === true && opts.inReplyTo !== void 0 && !this.spokenReplyIds.has(id) && !isBridgeMemoryLine(body.text) && this.callBuffer.isVoiceScopedId(opts.inReplyTo)) {
38861
- this.callBuffer.appendAgent(channelId, body.text, "reply");
39117
+ if (body.kind === "text" && opts.callBufferReply === true && opts.inReplyTo !== void 0 && !this.spokenReplyIds.has(id) && !isBridgeMemoryLine(recorded) && this.callBuffer.isVoiceScopedId(opts.inReplyTo)) {
39118
+ this.callBuffer.appendAgent(channelId, recorded, "reply");
38862
39119
  }
38863
39120
  const origin = this.spokenReplyIds.has(id) ? "fast_path" : !this.taintScope(channelId) ? "reply" : opts.inReplyTo !== void 0 && this.cleanTurns.has(opts.inReplyTo) && !this.taintedTurns.has(opts.inReplyTo) ? "reply" : "untrusted";
38864
- if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId, body.text, id, modality, purgeEpoch, origin);
38865
- if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId, body.text, id, replyCapture ?? null, origin);
39121
+ if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId, recorded, id, modality, purgeEpoch, origin);
39122
+ if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId, recorded, id, replyCapture ?? null, origin);
38866
39123
  }
38867
39124
  /**
38868
39125
  * Final review bridge I-1 (ledger T25): the agent reply's U11 capture, taken BEFORE the publish await. The clear generation
@@ -38940,14 +39197,14 @@ var BridgeAgent = class _BridgeAgent {
38940
39197
  const k = replyIntentKey({ inReplyTo: opts.inReplyTo, idempotencyKey: opts.idempotencyKey, ordinal: () => 0 });
38941
39198
  if (!k) throw new Error("idempotent send without a key");
38942
39199
  const id = replyMessageId(this.replyIdKey, this.cfg.identityId, channelId, k);
38943
- const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes);
38944
- const { intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes });
39200
+ const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, opts.credit);
39201
+ const { intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit: opts.credit ?? null });
38945
39202
  if (intent.bodyMac !== mac2 || intent.channelId !== channelId || intent.state === "skipped") {
38946
39203
  throw new IdempotencyConflictError();
38947
39204
  }
38948
39205
  return k;
38949
39206
  }
38950
- return this.claimUnkeyedPart(opts.inReplyTo, channelId, bytes, true);
39207
+ return this.claimUnkeyedPart(opts.inReplyTo, channelId, bytes, true, opts.credit ?? null);
38951
39208
  });
38952
39209
  return this.publishIntent(key, opts);
38953
39210
  }
@@ -38956,22 +39213,23 @@ var BridgeAgent = class _BridgeAgent {
38956
39213
  * the reply to `inReplyTo`. Caller holds the scope lock. `matchRetry` false
38957
39214
  * after a slot collision: the body must go to a NEW part.
38958
39215
  */
38959
- async claimUnkeyedPart(inReplyTo, channelId, bytes, matchRetry) {
39216
+ async claimUnkeyedPart(inReplyTo, channelId, bytes, matchRetry, replyCredit) {
39217
+ const credit = replyCredit ?? void 0;
38960
39218
  const scope = `reply:${inReplyTo}`;
38961
39219
  let latest = null;
38962
39220
  for (const intent of await this.store.listReplyIntents(inReplyTo)) {
38963
39221
  const n = intent.scope === scope ? partOrdinal(intent.part) : null;
38964
39222
  if (n !== null && (latest === null || n > latest.ordinal)) latest = { ordinal: n, intent };
38965
39223
  }
38966
- if (matchRetry && latest && latest.intent.state !== "skipped" && latest.intent.channelId === channelId && latest.intent.bodyMac === replyBodyMac(this.replyIdKey, latest.intent.replyMessageId, channelId, bytes)) {
39224
+ if (matchRetry && latest && latest.intent.state !== "skipped" && latest.intent.channelId === channelId && latest.intent.bodyMac === replyBodyMac(this.replyIdKey, latest.intent.replyMessageId, channelId, bytes, credit)) {
38967
39225
  return { scope, part: latest.intent.part, inReplyTo };
38968
39226
  }
38969
39227
  let ordinal = latest ? latest.ordinal + 1 : 0;
38970
39228
  for (let i = 0; i < REPLY_MAX_PART_ADVANCES; i++, ordinal++) {
38971
39229
  const k = { scope, part: `p:${ordinal}`, inReplyTo };
38972
39230
  const id = replyMessageId(this.replyIdKey, this.cfg.identityId, channelId, k);
38973
- const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes);
38974
- const { created, intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes });
39231
+ const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, credit);
39232
+ const { created, intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit });
38975
39233
  if (created) return k;
38976
39234
  if (matchRetry && intent.state !== "skipped" && intent.bodyMac === mac2 && intent.channelId === channelId) return k;
38977
39235
  writeServiceLog2("info", "reply.part_slot_taken", { attempt: i + 1 });
@@ -39016,6 +39274,12 @@ var BridgeAgent = class _BridgeAgent {
39016
39274
  }
39017
39275
  const bytes = intent.body;
39018
39276
  const body = decodeReplyBody(bytes);
39277
+ const recordText = this.recordTextOf(intent.replyCredit, body);
39278
+ if (recordText === void 0) {
39279
+ writeServiceLog2("error", "reply.intent_integrity_refused");
39280
+ this.store.deletePendingReplyIntent(key.scope, key.part);
39281
+ throw new ReplyIntentIntegrityError();
39282
+ }
39019
39283
  const purgeEpochBeforePublish = this.store.conversationPurgeEpoch(intent.channelId);
39020
39284
  const replyCapture = this.unifiedReplyCapture(intent.channelId, intent.inReplyTo ?? void 0);
39021
39285
  try {
@@ -39044,14 +39308,17 @@ var BridgeAgent = class _BridgeAgent {
39044
39308
  const inReplyTo = intent.inReplyTo;
39045
39309
  const next = await this.withReplyLock(
39046
39310
  JSON.stringify(["scope", key.scope]),
39047
- () => this.claimUnkeyedPart(inReplyTo, intent.channelId, bytes, false)
39311
+ () => (
39312
+ // r2 BLOCKER 1: the colliding row's OWN credit (verified above), never `opts`.
39313
+ this.claimUnkeyedPart(inReplyTo, intent.channelId, bytes, false, isReplyCredit(intent.replyCredit) ? intent.replyCredit : null)
39314
+ )
39048
39315
  );
39049
39316
  return this.publishIntent(next, opts, advances + 1);
39050
39317
  }
39051
39318
  throw e;
39052
39319
  }
39053
39320
  this.store.markReplyIntentSent(key.scope, key.part);
39054
- this.afterPublish(intent.channelId, body, intent.replyMessageId, opts, purgeEpochBeforePublish, replyCapture);
39321
+ this.afterPublish(intent.channelId, body, intent.replyMessageId, opts, purgeEpochBeforePublish, replyCapture, recordText);
39055
39322
  return { message_id: intent.replyMessageId, status: "sent" };
39056
39323
  });
39057
39324
  }
@@ -39061,9 +39328,21 @@ var BridgeAgent = class _BridgeAgent {
39061
39328
  */
39062
39329
  intentVerifies(intent) {
39063
39330
  if (!intent.body) return false;
39331
+ const credit = intent.replyCredit ?? null;
39332
+ if (credit !== null && !isReplyCredit(credit)) return false;
39064
39333
  const id = replyMessageId(this.replyIdKey, this.cfg.identityId, intent.channelId, intent);
39065
39334
  if (id !== intent.replyMessageId) return false;
39066
- return replyBodyMac(this.replyIdKey, intent.replyMessageId, intent.channelId, intent.body) === intent.bodyMac;
39335
+ return replyBodyMac(this.replyIdKey, intent.replyMessageId, intent.channelId, intent.body, credit ?? void 0) === intent.bodyMac;
39336
+ }
39337
+ /**
39338
+ * Weather/currency Task 17: what a verified row's reply is RECORDED as. Null credit: the decoded text unchanged (null
39339
+ * for an a2a body). "weatherapi": the text minus the exact credit suffix. Undefined: the row is refused (an unknown
39340
+ * credit, a credited a2a body, or a credited text without the exact suffix).
39341
+ */
39342
+ recordTextOf(credit, body) {
39343
+ if (credit === null) return body.kind === "text" ? body.text : null;
39344
+ if (!isReplyCredit(credit) || body.kind !== "text" || !body.text.endsWith(WEATHER_CREDIT_SUFFIX)) return void 0;
39345
+ return body.text.slice(0, body.text.length - WEATHER_CREDIT_SUFFIX.length);
39067
39346
  }
39068
39347
  /**
39069
39348
  * SEC-06 #18 item 3: a rate-limited reply retries the PUBLISH in-process
@@ -39223,7 +39502,8 @@ var BridgeAgent = class _BridgeAgent {
39223
39502
  uploadBlob: (ciphertext, channelId) => this.uploadBlobImpl(ciphertext, channelId),
39224
39503
  encryptFile,
39225
39504
  bytesToBase64: bytesToBase642,
39226
- readFile: (p) => fsp.readFile(p).then((b) => new Uint8Array(b))
39505
+ // Security fix 2026-10-30: attachments upload only from the outbox.
39506
+ readFile: (p) => readOutboxFile(this.outboxPin, p).then((r) => r.bytes)
39227
39507
  };
39228
39508
  }
39229
39509
  receiveDeps() {
@@ -39233,8 +39513,8 @@ var BridgeAgent = class _BridgeAgent {
39233
39513
  decryptFile,
39234
39514
  computeFileDigest,
39235
39515
  base64ToBytes: base64ToBytes2,
39236
- mkdirp: (d) => fsp.mkdir(d, { recursive: true }).then(() => void 0),
39237
- writeFile: (p, data) => fsp.writeFile(p, data)
39516
+ mkdirp: (d) => fsp2.mkdir(d, { recursive: true }).then(() => void 0),
39517
+ writeFile: (p, data) => fsp2.writeFile(p, data)
39238
39518
  };
39239
39519
  }
39240
39520
  async sendAttachment(channelId, filePath2, caption) {
@@ -39242,8 +39522,11 @@ var BridgeAgent = class _BridgeAgent {
39242
39522
  const joined = await this.reconcileJoin(channelId);
39243
39523
  if (!joined) throw new ChannelPendingError(channelId);
39244
39524
  let body;
39525
+ let read;
39245
39526
  try {
39246
- body = await prepareAttachment(filePath2, caption, channelId, this.sendDeps());
39527
+ read = await readOutboxFile(this.outboxPin, filePath2);
39528
+ const bytes = read.bytes;
39529
+ body = await prepareAttachment(filePath2, caption, channelId, { ...this.sendDeps(), readFile: async () => bytes });
39247
39530
  } catch (e) {
39248
39531
  if (e.code === "ENOENT" || e.code === "EACCES" || e.code === "ENOTDIR" || e.code === "EISDIR") {
39249
39532
  throw new InvalidPathError(filePath2, e);
@@ -39284,6 +39567,7 @@ var BridgeAgent = class _BridgeAgent {
39284
39567
  );
39285
39568
  this.latency.record("send.total", performance.now() - sendStart);
39286
39569
  this.agentHops.noteOwnSend(channelId);
39570
+ await this.consumeUploaded(read);
39287
39571
  return { message_id: id, status: "sent" };
39288
39572
  }
39289
39573
  // ── B7: FileSafe vault commands (loopback boundary) ──────────────────────
@@ -39809,25 +40093,26 @@ var BridgeAgent = class _BridgeAgent {
39809
40093
  }
39810
40094
  };
39811
40095
  }
39812
- /** Read `path` off local disk, encrypt + wrap its DEK under the vault's
40096
+ /** Read `path` from the vault outbox (security fix 2026-10-30; see
40097
+ * ./outbox.ts), encrypt + wrap its DEK under the vault's
39813
40098
  * current KEK generation, and create+upload+commit it as a new vault
39814
40099
  * file node named `name` (which may differ from `path`'s basename). */
39815
40100
  async vaultWrite(enclaveId, vaultChannelId, path, name) {
39816
- const session = await this.getVaultSession(enclaveId, await this.resolveVaultChannelId(enclaveId, vaultChannelId));
39817
- let bytes;
40101
+ let read;
39818
40102
  try {
39819
- bytes = new Uint8Array(await fsp.readFile(path));
40103
+ read = await readOutboxFile(this.outboxPin, path);
39820
40104
  } catch (e) {
39821
40105
  const code = e.code;
39822
40106
  if (code === "ENOENT" || code === "EACCES" || code === "ENOTDIR" || code === "EISDIR") throw new InvalidPathError(path, e);
39823
40107
  throw e;
39824
40108
  }
40109
+ const session = await this.getVaultSession(enclaveId, await this.resolveVaultChannelId(enclaveId, vaultChannelId));
39825
40110
  const nodeId = crypto.randomUUID();
39826
- return vaultWriteImpl({
40111
+ const written = await vaultWriteImpl({
39827
40112
  enclaveId,
39828
40113
  nodeId,
39829
40114
  name,
39830
- bytes,
40115
+ bytes: read.bytes,
39831
40116
  deps: {
39832
40117
  exporterKekForGeneration: session.kekForGeneration,
39833
40118
  currentGeneration: session.currentGeneration,
@@ -39836,6 +40121,16 @@ var BridgeAgent = class _BridgeAgent {
39836
40121
  commitNode: (committedNodeId) => this.vaultCommitNode(enclaveId, committedNodeId)
39837
40122
  }
39838
40123
  });
40124
+ await this.consumeUploaded(read);
40125
+ return written;
40126
+ }
40127
+ /** Audit 2026-10-30 MED 5: an uploaded file leaves the outbox (no plaintext lingers). Content-free log. */
40128
+ async consumeUploaded(read) {
40129
+ if (!this.outboxPin || !await consumeOutboxFile(this.outboxPin, read)) writeServiceLog2("warn", "vault_outbox.consume_failed");
40130
+ }
40131
+ /** Audit 2026-10-30 MED 3: the pinned outbox path, served only on the authenticated GET /v1/vault-outbox. */
40132
+ vaultOutboxPath() {
40133
+ return this.outboxPin?.dir ?? null;
39839
40134
  }
39840
40135
  /** Download + verify + decrypt a vault file node. Returns the plaintext
39841
40136
  * base64-encoded (the loopback boundary is JSON-only, mirroring how
@@ -42268,13 +42563,13 @@ var BridgeAgent = class _BridgeAgent {
42268
42563
  throw new Error("summarize_no_runtime");
42269
42564
  }
42270
42565
  const messages = [{ role: "user", content: prompt }];
42271
- return new Promise((resolve, reject) => {
42566
+ return new Promise((resolve2, reject) => {
42272
42567
  const onAbort = () => reject(new Error("aborted"));
42273
42568
  signal.addEventListener("abort", onAbort);
42274
42569
  this.fastLlm.complete(messages).then(
42275
42570
  (text) => {
42276
42571
  signal.removeEventListener("abort", onAbort);
42277
- resolve(text);
42572
+ resolve2(text);
42278
42573
  },
42279
42574
  (err) => {
42280
42575
  signal.removeEventListener("abort", onAbort);
@@ -43187,7 +43482,7 @@ var BridgeAgent = class _BridgeAgent {
43187
43482
  this.memory,
43188
43483
  m.channelId,
43189
43484
  m.envelope.sender_id,
43190
- parsed.text,
43485
+ m.envelope.sender_id === this.cfg.identityId ? stripCreditSuffix(parsed.text) : parsed.text,
43191
43486
  m.messageId,
43192
43487
  m.voiceMode ? "voice" : "text",
43193
43488
  purgeEpochAtTap
@@ -43222,7 +43517,7 @@ var BridgeAgent = class _BridgeAgent {
43222
43517
  return got;
43223
43518
  }
43224
43519
  const cap = Math.min(waitSeconds, 50) * 1e3;
43225
- await new Promise((resolve) => {
43520
+ await new Promise((resolve2) => {
43226
43521
  let fired = false;
43227
43522
  let timer;
43228
43523
  const wake = () => {
@@ -43231,7 +43526,7 @@ var BridgeAgent = class _BridgeAgent {
43231
43526
  clearTimeout(timer);
43232
43527
  const i = this.waiters.indexOf(wake);
43233
43528
  if (i >= 0) this.waiters.splice(i, 1);
43234
- resolve();
43529
+ resolve2();
43235
43530
  };
43236
43531
  timer = setTimeout(wake, cap);
43237
43532
  this.waiters.push(wake);
@@ -43274,13 +43569,13 @@ var BridgeAgent = class _BridgeAgent {
43274
43569
  const bound = b !== null && b.state === "bound" && b.counter === this.toolGrantCounter;
43275
43570
  const switchOn = audience === "owner_dm" ? this.ownerDmManifestGrants : audience === "team_room" ? this.roomManifestGrants : false;
43276
43571
  const signedNow = new Set(audience && manifest ? manifest.grants[audience] ?? [] : []);
43277
- const constants4 = new Set(audience === "owner_dm" ? OWNER_DM_DEFAULT_READ_GRANTS.map((g) => g.tool) : audience === "team_room" ? ROOM_DEFAULT_READ_GRANTS.map((g) => g.tool) : []);
43572
+ const constants5 = new Set(audience === "owner_dm" ? OWNER_DM_DEFAULT_READ_GRANTS.map((g) => g.tool) : audience === "team_room" ? ROOM_DEFAULT_READ_GRANTS.map((g) => g.tool) : []);
43278
43573
  let changed = false;
43279
43574
  const out = a.tool_grants.flatMap((g) => {
43280
43575
  if (!(g.mode === "read" && g.source === "manifest")) return [g];
43281
43576
  if (switchOn && bound && signedNow.has(g.tool)) return [g];
43282
43577
  changed = true;
43283
- return constants4.has(g.tool) && signedNow.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
43578
+ return constants5.has(g.tool) && signedNow.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
43284
43579
  });
43285
43580
  if (!changed) return m;
43286
43581
  return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(out) }) };
@@ -43985,21 +44280,49 @@ ${channelId}`;
43985
44280
  */
43986
44281
  searchProvidersSockets = /* @__PURE__ */ new Map();
43987
44282
  /**
43988
- * The D28 value the files were last recomputed for; null until the first socket observation, so that observation
43989
- * always recomputes (final review I-1), even when it is non-capable.
44283
+ * Weather/currency U6 (H-1): the same AND-over-sockets state for utility_tools_v1, kept separately per socket, so a
44284
+ * runtime that declares search_providers_v2 but not utility_tools_v1 (0.14.0, 0.14.1) keeps web_extract and never
44285
+ * sees a utility name.
43990
44286
  */
43991
- searchProvidersState = null;
44287
+ utilityToolsSockets = /* @__PURE__ */ new Map();
44288
+ /**
44289
+ * The (search, utility) tuple the files were last recomputed for; null until the first socket observation, so that
44290
+ * observation always recomputes (final review I-1), even when it is non-capable. One trigger keyed on BOTH values.
44291
+ */
44292
+ capabilityState = null;
43992
44293
  searchCapabilityRefetchTimer = null;
43993
44294
  /** When the current burst's first change happened (valid while the timer is set). */
43994
44295
  searchCapabilityBurstStart = 0;
43995
44296
  /** The D28 value in the last report posted (null until one is); a change needs a refetch to reach the web. */
43996
44297
  lastReportedSearchProviders = null;
44298
+ /** U6: the utility_tools_v1 value in the last report posted (null until one is). */
44299
+ lastReportedUtilityTools = null;
43997
44300
  noteSearchProvidersSocket(token, declared) {
43998
44301
  this.searchProvidersSockets.set(token, declared);
43999
- this.onSearchProvidersMaybeChanged();
44302
+ this.onCapabilityMaybeChanged();
44000
44303
  }
44001
44304
  clearSearchProvidersSocket(token) {
44002
- if (this.searchProvidersSockets.delete(token)) this.onSearchProvidersMaybeChanged();
44305
+ if (this.searchProvidersSockets.delete(token)) this.onCapabilityMaybeChanged();
44306
+ }
44307
+ noteUtilityToolsSocket(token, declared) {
44308
+ this.utilityToolsSockets.set(token, declared);
44309
+ this.onCapabilityMaybeChanged();
44310
+ }
44311
+ clearUtilityToolsSocket(token) {
44312
+ if (this.utilityToolsSockets.delete(token)) this.onCapabilityMaybeChanged();
44313
+ }
44314
+ /** U6: every attached runtime socket declared utility_tools_v1; no socket attached means absent (false). */
44315
+ utilityToolsCapable() {
44316
+ if (this.utilityToolsSockets.size === 0) return false;
44317
+ for (const v of this.utilityToolsSockets.values()) if (!v) return false;
44318
+ return true;
44319
+ }
44320
+ /** U6: the features every attached runtime socket declared, for the policy functions. */
44321
+ capabilityFeatures() {
44322
+ const out = /* @__PURE__ */ new Set();
44323
+ if (this.searchProvidersCapable()) out.add("search_providers_v2");
44324
+ if (this.utilityToolsCapable()) out.add("utility_tools_v1");
44325
+ return out;
44003
44326
  }
44004
44327
  /** D28: every attached runtime socket declared search_providers_v2; no socket attached means absent (false). */
44005
44328
  searchProvidersCapable() {
@@ -44026,14 +44349,14 @@ ${channelId}`;
44026
44349
  * D35 (P-4): on a capability change, rewrite manifest-policy.json at once from the stored manifest, the kill
44027
44350
  * switches and the new capability, then a debounced refetch (one per burst) only when needed.
44028
44351
  */
44029
- onSearchProvidersMaybeChanged() {
44030
- const capable = this.searchProvidersCapable();
44031
- const prev = this.searchProvidersState;
44032
- if (capable === prev) return;
44033
- this.searchProvidersState = capable;
44034
- this.recomputeManifestPolicy(capable);
44352
+ onCapabilityMaybeChanged() {
44353
+ const next = { search: this.searchProvidersCapable(), utility: this.utilityToolsCapable() };
44354
+ const prev = this.capabilityState;
44355
+ if (prev !== null && prev.search === next.search && prev.utility === next.utility) return;
44356
+ this.capabilityState = next;
44357
+ this.recomputeManifestPolicy(this.capabilityFeatures());
44035
44358
  if (this.closingDown) return;
44036
- if (prev === null && !capable) return;
44359
+ if (prev === null && !next.search && !next.utility) return;
44037
44360
  const now = Date.now();
44038
44361
  if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
44039
44362
  else this.searchCapabilityBurstStart = now;
@@ -44051,7 +44374,7 @@ ${channelId}`;
44051
44374
  searchCapabilityRefetchNeeded() {
44052
44375
  if (!this.bootConfigSettled || this.closingDown) return false;
44053
44376
  const inForce = (this.cfg.ownerGrantPin?.pin ?? null) !== null && this.toolGrantManifest !== null;
44054
- return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable();
44377
+ return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable() || this.lastReportedUtilityTools !== this.utilityToolsCapable();
44055
44378
  }
44056
44379
  /**
44057
44380
  * D35: the local recompute. Widening (web_extract back in) only while the binding for the in-force manifest stands,
@@ -44059,16 +44382,17 @@ ${channelId}`;
44059
44382
  * lists web_extract loses it at once, so an older runtime never reads it as unreadable (which drops every grant).
44060
44383
  * Content-free; a write failure is logged and the debounced refetch tries again.
44061
44384
  */
44062
- recomputeManifestPolicy(capable) {
44385
+ recomputeManifestPolicy(features) {
44063
44386
  const pin = this.cfg.ownerGrantPin?.pin ?? null;
44064
44387
  const manifest = pin !== null ? this.toolGrantManifest : null;
44065
44388
  try {
44066
44389
  if (manifest !== null && this.manifestBinding !== null && this.manifestBinding.counter === manifest.counter) {
44067
- this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, capable));
44390
+ this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, features));
44068
44391
  return;
44069
44392
  }
44070
- if (capable) return;
44071
- const path = join17(runtimeConfigDir(this.home, this.cfg.identityId), MANIFEST_POLICY_FILE);
44393
+ const withheld = new Set(Object.keys(TOOL_FEATURE).filter((t) => !toolAllowedByFeatures(t, features)));
44394
+ if (withheld.size === 0) return;
44395
+ const path = join18(runtimeConfigDir(this.home, this.cfg.identityId), MANIFEST_POLICY_FILE);
44072
44396
  if (!existsSync5(path)) return;
44073
44397
  let doc;
44074
44398
  try {
@@ -44077,10 +44401,10 @@ ${channelId}`;
44077
44401
  return;
44078
44402
  }
44079
44403
  const tools = doc?.manifest_tools;
44080
- if (!Array.isArray(tools) || !tools.includes("web_extract")) return;
44404
+ if (!Array.isArray(tools) || !tools.some((t) => typeof t === "string" && withheld.has(t))) return;
44081
44405
  this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, {
44082
44406
  v: 1,
44083
- manifest_tools: tools.filter((t) => typeof t === "string" && t !== "web_extract"),
44407
+ manifest_tools: tools.filter((t) => typeof t === "string" && !withheld.has(t)),
44084
44408
  secret_path_mode: "enforce"
44085
44409
  });
44086
44410
  } catch (e) {
@@ -44293,8 +44617,8 @@ ${channelId}`;
44293
44617
  let timer;
44294
44618
  await Promise.race([
44295
44619
  ingest.released,
44296
- new Promise((resolve) => {
44297
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
44620
+ new Promise((resolve2) => {
44621
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44298
44622
  })
44299
44623
  ]);
44300
44624
  if (timer) clearTimeout(timer);
@@ -44304,8 +44628,8 @@ ${channelId}`;
44304
44628
  let timer;
44305
44629
  await Promise.race([
44306
44630
  starting,
44307
- new Promise((resolve) => {
44308
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
44631
+ new Promise((resolve2) => {
44632
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44309
44633
  })
44310
44634
  ]);
44311
44635
  if (timer) clearTimeout(timer);
@@ -44314,8 +44638,8 @@ ${channelId}`;
44314
44638
  let timer;
44315
44639
  await Promise.race([
44316
44640
  this.membershipSweepWork,
44317
- new Promise((resolve) => {
44318
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
44641
+ new Promise((resolve2) => {
44642
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44319
44643
  })
44320
44644
  ]);
44321
44645
  if (timer) clearTimeout(timer);
@@ -44325,8 +44649,8 @@ ${channelId}`;
44325
44649
  let timer;
44326
44650
  await Promise.race([
44327
44651
  this.recallPruneWork,
44328
- new Promise((resolve) => {
44329
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
44652
+ new Promise((resolve2) => {
44653
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44330
44654
  })
44331
44655
  ]);
44332
44656
  if (timer) clearTimeout(timer);
@@ -44336,8 +44660,8 @@ ${channelId}`;
44336
44660
  let timer;
44337
44661
  await Promise.race([
44338
44662
  this.recallMaintenanceWork,
44339
- new Promise((resolve) => {
44340
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
44663
+ new Promise((resolve2) => {
44664
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44341
44665
  })
44342
44666
  ]);
44343
44667
  if (timer) clearTimeout(timer);
@@ -44349,8 +44673,8 @@ ${channelId}`;
44349
44673
  let timer;
44350
44674
  await Promise.race([
44351
44675
  Promise.allSettled([...this.recallTapWork]),
44352
- new Promise((resolve) => {
44353
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
44676
+ new Promise((resolve2) => {
44677
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44354
44678
  })
44355
44679
  ]);
44356
44680
  if (timer) clearTimeout(timer);
@@ -44359,8 +44683,8 @@ ${channelId}`;
44359
44683
  let timer;
44360
44684
  await Promise.race([
44361
44685
  Promise.allSettled([...this.unifiedWork]),
44362
- new Promise((resolve) => {
44363
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
44686
+ new Promise((resolve2) => {
44687
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44364
44688
  })
44365
44689
  ]);
44366
44690
  if (timer) clearTimeout(timer);
@@ -44369,8 +44693,8 @@ ${channelId}`;
44369
44693
  let timer;
44370
44694
  await Promise.race([
44371
44695
  this.recallPrewarmWork,
44372
- new Promise((resolve) => {
44373
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
44696
+ new Promise((resolve2) => {
44697
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44374
44698
  })
44375
44699
  ]);
44376
44700
  if (timer) clearTimeout(timer);
@@ -44380,8 +44704,8 @@ ${channelId}`;
44380
44704
  let timer;
44381
44705
  await Promise.race([
44382
44706
  Promise.allSettled([...this.replyWork]),
44383
- new Promise((resolve) => {
44384
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
44707
+ new Promise((resolve2) => {
44708
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44385
44709
  })
44386
44710
  ]);
44387
44711
  if (timer) clearTimeout(timer);
@@ -44394,7 +44718,7 @@ ${channelId}`;
44394
44718
 
44395
44719
  // src/config.ts
44396
44720
  import { readFileSync as readFileSync11, statSync as statSync5 } from "node:fs";
44397
- import { dirname as dirname6, join as join18 } from "node:path";
44721
+ import { dirname as dirname7, join as join19 } from "node:path";
44398
44722
  var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
44399
44723
  var CONFIG_ERROR_MESSAGE = {
44400
44724
  credential_not_found: "credential file not found",
@@ -44432,7 +44756,7 @@ function loadConfig(opts) {
44432
44756
  throw new ConfigError("credential_not_json");
44433
44757
  }
44434
44758
  for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new ConfigError("credential_missing_field", k);
44435
- const storePath = opts.storePath ?? join18(dirname6(opts.credentialPath), "bridge-state.db");
44759
+ const storePath = opts.storePath ?? join19(dirname7(opts.credentialPath), "bridge-state.db");
44436
44760
  const ownerGrantPin = loadOwnerGrantPin({ credentialPath: opts.credentialPath, credentialJson: json2 });
44437
44761
  writeServiceLog2(ownerGrantPin.state === "ok" || ownerGrantPin.state === "no_pin" ? "info" : "warn", "authz.pin", {
44438
44762
  state: ServiceLogEnum.of(ownerGrantPin.state, ["ok", "no_pin", "pin_conflict", "malformed", "unsupported_algo", "symlink_refused", "unreadable"])
@@ -44617,6 +44941,13 @@ var BRIDGE_ERRORS = {
44617
44941
  // C9 (spec §4.1.6 step 1): another copy of this agent took its SecureComs
44618
44942
  // session (WS 4409), so this copy stopped. Every local route but /healthz.
44619
44943
  session_superseded: { http: 503, message: "another copy of this agent took its session; this copy stopped" },
44944
+ // Weather/currency Task 17 (OQ-P1): a send frame's credit is not the closed value, or rides an a2a frame. TERMINAL:
44945
+ // the inbound is retired, so the model (and the weather call) never runs again for it.
44946
+ reply_credit_unknown: { http: 400, message: "reply credit not recognised; not sent" },
44947
+ // Security fix 2026-10-30: an upload route was given a path that is not a
44948
+ // regular file directly inside the bridge's vault outbox (`vault_outbox` on
44949
+ // /healthz). Fixed and content-free: the path is never echoed or logged.
44950
+ path_outside_outbox: { http: 400, message: "uploads must come from the bridge vault outbox; copy the file there first" },
44620
44951
  internal: { http: 500, message: "internal error" }
44621
44952
  };
44622
44953
  function errorBody(code, detail) {
@@ -44646,6 +44977,7 @@ function withConsumerGrants(m, perToolConsumer) {
44646
44977
  }
44647
44978
  var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
44648
44979
  var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
44980
+ var UTILITY_TOOLS_FEATURE = "utility_tools_v1";
44649
44981
  var CONSTANT_TOOL_NAMES = Object.freeze({
44650
44982
  owner_dm: Object.freeze(["current_time", "web_search"]),
44651
44983
  team_room: Object.freeze(["current_time"])
@@ -44655,10 +44987,10 @@ function withManifestCapability(m, manifestConsumer) {
44655
44987
  if (manifestConsumer) return m;
44656
44988
  const a = m.authorization;
44657
44989
  if (!a || !a.tool_grants.some(isManifestGrant)) return m;
44658
- const constants4 = new Set(a.audience === "owner_dm" || a.audience === "team_room" ? CONSTANT_TOOL_NAMES[a.audience] : []);
44990
+ const constants5 = new Set(a.audience === "owner_dm" || a.audience === "team_room" ? CONSTANT_TOOL_NAMES[a.audience] : []);
44659
44991
  const narrowed = a.tool_grants.flatMap((g) => {
44660
44992
  if (!isManifestGrant(g)) return [g];
44661
- return constants4.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
44993
+ return constants5.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
44662
44994
  });
44663
44995
  return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(narrowed) }) };
44664
44996
  }
@@ -44677,6 +45009,15 @@ function withSearchCapability(m, consumerHasFeature, signedProvider) {
44677
45009
  if (kept.length === a.tool_grants.length) return m;
44678
45010
  return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
44679
45011
  }
45012
+ var UTILITY_TOOL_NAMES = /* @__PURE__ */ new Set(["weather_forecast", "currency_convert"]);
45013
+ function withUtilityCapability(m, consumerHasFeature) {
45014
+ if (consumerHasFeature) return m;
45015
+ const a = m.authorization;
45016
+ if (!a) return m;
45017
+ const kept = a.tool_grants.filter((g) => !UTILITY_TOOL_NAMES.has(g.tool));
45018
+ if (kept.length === a.tool_grants.length) return m;
45019
+ return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
45020
+ }
44680
45021
 
44681
45022
  // src/http-server.ts
44682
45023
  function writeSse(res, msgs, onWriteError) {
@@ -44700,7 +45041,7 @@ data: ${JSON.stringify(msgs[i])}
44700
45041
  function deliverSseBatch(res, batch, agent, onWriteError) {
44701
45042
  const withheld = agent.harnessGrantsWithheld();
44702
45043
  const provider = agent.signedSearchProvider();
44703
- const left3 = writeSse(res, batch.map((m) => withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider)), onWriteError);
45044
+ const left3 = writeSse(res, batch.map((m) => withUtilityCapability(withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider), false)), onWriteError);
44704
45045
  const leftIds = new Set(left3.map((message) => message.message_id));
44705
45046
  agent.ackRuntimeMessages(batch.filter((message) => !leftIds.has(message.message_id)).map((message) => message.message_id));
44706
45047
  if (left3.length) agent.requeueFront(batch.filter((message) => leftIds.has(message.message_id)));
@@ -44732,7 +45073,7 @@ var OTHER_CAP = 8 * 1024;
44732
45073
  var a2aSchema = z3.object({
44733
45074
  a2a_version: z3.string().min(1),
44734
45075
  kind: z3.enum(["message", "task", "artifact"]),
44735
- object: z3.record(z3.unknown())
45076
+ object: z3.record(z3.string(), z3.unknown())
44736
45077
  });
44737
45078
  var sendAttachmentSchema = z3.object({
44738
45079
  channel_id: z3.string().min(1),
@@ -44807,7 +45148,7 @@ function jsonError(res, code, detail) {
44807
45148
  json(res, httpStatus(code), errorBody(code, detail));
44808
45149
  }
44809
45150
  function readBody(req, cap) {
44810
- return new Promise((resolve, reject) => {
45151
+ return new Promise((resolve2, reject) => {
44811
45152
  let size = 0;
44812
45153
  let done = false;
44813
45154
  const chunks = [];
@@ -44827,7 +45168,7 @@ function readBody(req, cap) {
44827
45168
  req.on("end", () => {
44828
45169
  if (!done) {
44829
45170
  done = true;
44830
- resolve(Buffer.concat(chunks).toString("utf8"));
45171
+ resolve2(Buffer.concat(chunks).toString("utf8"));
44831
45172
  }
44832
45173
  });
44833
45174
  req.on("error", (e) => {
@@ -44905,6 +45246,9 @@ function createHttpServer(agent, opts) {
44905
45246
  }
44906
45247
  return json(res, 200, r);
44907
45248
  }
45249
+ if (method === "GET" && path === "/v1/vault-outbox") {
45250
+ return json(res, 200, { path: agent.vaultOutboxPath() });
45251
+ }
44908
45252
  if (method === "POST" && path === "/v1/send-attachment") {
44909
45253
  const parsed = sendAttachmentSchema.safeParse(JSON.parse(await readBody(req, OTHER_CAP)));
44910
45254
  if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
@@ -44974,7 +45318,7 @@ function createHttpServer(agent, opts) {
44974
45318
  const drained = await agent.drain(channel, wait);
44975
45319
  const withheld = agent.harnessGrantsWithheld();
44976
45320
  const provider = agent.signedSearchProvider();
44977
- return json(res, 200, drained.map((m) => withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider)));
45321
+ return json(res, 200, drained.map((m) => withUtilityCapability(withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider), false)));
44978
45322
  }
44979
45323
  if (method === "GET" && path === "/v1/events") {
44980
45324
  const channel = url.searchParams.get("channel_id") ?? void 0;
@@ -45016,6 +45360,10 @@ function createHttpServer(agent, opts) {
45016
45360
  if (e instanceof SendForbiddenError) return jsonError(res, "send_forbidden");
45017
45361
  if (e instanceof GroupSupersededSendError) return jsonError(res, "group_superseded");
45018
45362
  if (e instanceof IdempotencyConflictError) return jsonError(res, "invalid_request", e.message);
45363
+ if (e instanceof PathOutsideOutboxError) {
45364
+ writeServiceLog2("warn", "vault_outbox.refused");
45365
+ return jsonError(res, "path_outside_outbox");
45366
+ }
45019
45367
  if (e instanceof InvalidPathError) return jsonError(res, "invalid_request", e.message);
45020
45368
  if (e instanceof VaultPinMismatchError) return jsonError(res, "vault_ref_changed", { pin: e.pin });
45021
45369
  if (e instanceof VaultIntegrityError) return jsonError(res, "vault_integrity_failed", e.message);
@@ -45149,6 +45497,7 @@ function attachAgentWebSocket(server, agent, opts) {
45149
45497
  perToolGrants: false,
45150
45498
  manifestGrants: false,
45151
45499
  searchProviders: false,
45500
+ utilityTools: false,
45152
45501
  // helloWaitMs 0 keeps today's synchronous initial flush exactly (the opt-out the existing tests use).
45153
45502
  deliveryOpen: helloWaitMs <= 0,
45154
45503
  deliveryToken: /* @__PURE__ */ new Map(),
@@ -45202,14 +45551,14 @@ function attachAgentWebSocket(server, agent, opts) {
45202
45551
  continue;
45203
45552
  }
45204
45553
  const out = withConsumerNotes(
45205
- withSearchCapability(
45554
+ withUtilityCapability(withSearchCapability(
45206
45555
  withManifestCapability(
45207
45556
  withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
45208
45557
  session.manifestGrants
45209
45558
  ),
45210
45559
  session.searchProviders,
45211
45560
  agent.signedSearchProvider()
45212
- ),
45561
+ ), session.utilityTools),
45213
45562
  batch[i],
45214
45563
  session,
45215
45564
  agent
@@ -45266,6 +45615,7 @@ function attachAgentWebSocket(server, agent, opts) {
45266
45615
  if (session.perToolGrants) agent.clearPerToolConsumer(session);
45267
45616
  if (session.manifestGrants) agent.clearManifestConsumer(session);
45268
45617
  agent.clearSearchProvidersSocket(session);
45618
+ agent.clearUtilityToolsSocket(session);
45269
45619
  };
45270
45620
  current = { sock, cleanup, sweepStale };
45271
45621
  sock.on("close", cleanup);
@@ -45306,7 +45656,10 @@ function sendJson(sock, body) {
45306
45656
  } catch {
45307
45657
  }
45308
45658
  }
45659
+ var ReplyCreditUnknownError = class extends Error {
45660
+ };
45309
45661
  function sendErrorCode(e) {
45662
+ if (e instanceof ReplyCreditUnknownError) return "reply_credit_unknown";
45310
45663
  if (e instanceof ChannelPendingError) return "channel_pending";
45311
45664
  if (e instanceof RateLimitedError) return "rate_limited";
45312
45665
  if (e instanceof AgentPausedSendError) return "agent_paused";
@@ -45367,6 +45720,8 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
45367
45720
  agent.noteConsumerCapability(manifest ? "ok" : session.harnessClaimed ? "harness_consumer" : "missing");
45368
45721
  session.searchProviders = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(SEARCH_PROVIDERS_FEATURE);
45369
45722
  agent.noteSearchProvidersSocket(session, session.searchProviders);
45723
+ session.utilityTools = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(UTILITY_TOOLS_FEATURE);
45724
+ agent.noteUtilityToolsSocket(session, session.utilityTools);
45370
45725
  if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
45371
45726
  agent.noteNotesConsumer();
45372
45727
  }
@@ -45374,7 +45729,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
45374
45729
  if (session.recallConsumer) agent.noteRecallConsumer();
45375
45730
  session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
45376
45731
  if (session.unifiedConsumer) agent.noteUnifiedConsumer();
45377
- sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE, SEARCH_PROVIDERS_FEATURE] });
45732
+ sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE, SEARCH_PROVIDERS_FEATURE, REPLY_CREDIT_FEATURE] });
45378
45733
  if (!session.deliveryOpen) {
45379
45734
  session.deliveryOpen = true;
45380
45735
  flush();
@@ -45403,7 +45758,8 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
45403
45758
  }
45404
45759
  const replyFailed = (e, inReplyTo) => {
45405
45760
  const code = sendErrorCode(e);
45406
- if ((code === "agent_paused" || code === "send_forbidden") && typeof inReplyTo === "string") {
45761
+ const terminal = code === "agent_paused" || code === "send_forbidden" || code === "reply_credit_unknown";
45762
+ if (terminal && typeof inReplyTo === "string") {
45407
45763
  inflight.delete(inReplyTo);
45408
45764
  session.deliveryToken.delete(inReplyTo);
45409
45765
  session.shownRecall.delete(inReplyTo);
@@ -45411,7 +45767,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
45411
45767
  agent.ackRuntimeMessages([inReplyTo]);
45412
45768
  }
45413
45769
  sendJson(sock, { ...wsErrorFrame(code), ...typeof inReplyTo === "string" ? { in_reply_to: inReplyTo } : {} });
45414
- if (code === "agent_paused" || code === "send_forbidden") flush();
45770
+ if (terminal) flush();
45415
45771
  };
45416
45772
  if (m.type === "send") {
45417
45773
  if (typeof m.channel_id !== "string" || !m.channel_id) {
@@ -45428,8 +45784,14 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
45428
45784
  }
45429
45785
  const record = typeof m.in_reply_to === "string" ? inflight.get(m.in_reply_to) : void 0;
45430
45786
  const callBufferReply = session.consumer === "runtime" && session.perToolGrants === true && session.harnessClaimed !== true && typeof m.in_reply_to === "string" && record !== void 0 && session.deliveryToken.get(m.in_reply_to) !== void 0 && record.channel_id === m.channel_id && !agent.isRuntimeMessageRetired(m.in_reply_to);
45787
+ if (m.credit !== void 0 && (m.a2a !== void 0 || !isReplyCredit(m.credit))) {
45788
+ writeServiceLog2("error", "reply.credit_unknown");
45789
+ replyFailed(new ReplyCreditUnknownError(), m.in_reply_to);
45790
+ return;
45791
+ }
45431
45792
  const sendOpts = {
45432
45793
  agentTurn: isAgentTurn(agent, m),
45794
+ ...isReplyCredit(m.credit) ? { credit: m.credit } : {},
45433
45795
  ...typeof m.in_reply_to === "string" ? { inReplyTo: m.in_reply_to } : {},
45434
45796
  ...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {},
45435
45797
  ...callBufferReply ? { callBufferReply: true } : {}