@securecoms/agent-bridge 0.34.0 → 0.36.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
@@ -3083,7 +3083,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
3083
3083
  if (outputLen !== expectedLen)
3084
3084
  throw new Error(`hash.outputLen must be ${expectedLen}, got ${outputLen}`);
3085
3085
  }
3086
- const randomBytes12 = opts.randomBytes === void 0 ? randomBytes4 : opts.randomBytes;
3086
+ const randomBytes13 = opts.randomBytes === void 0 ? randomBytes4 : opts.randomBytes;
3087
3087
  const adjustScalarBytes3 = opts.adjustScalarBytes === void 0 ? (bytes) => bytes : opts.adjustScalarBytes;
3088
3088
  const domain = opts.domain === void 0 ? (data, ctx, phflag) => {
3089
3089
  abool2(phflag, "phflag");
@@ -3169,7 +3169,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
3169
3169
  seed: _size
3170
3170
  };
3171
3171
  function randomSecretKey(seed) {
3172
- seed = seed === void 0 ? randomBytes12(lengths.seed) : seed;
3172
+ seed = seed === void 0 ? randomBytes13(lengths.seed) : seed;
3173
3173
  return abytes3(seed, lengths.seed, "seed");
3174
3174
  }
3175
3175
  function isValidSecretKey(key) {
@@ -6418,7 +6418,7 @@ function ecdsa(Point, hash, ecdsaOpts = {}) {
6418
6418
  bits2int_modN: "function"
6419
6419
  });
6420
6420
  ecdsaOpts = Object.assign({}, ecdsaOpts);
6421
- const randomBytes12 = ecdsaOpts.randomBytes === void 0 ? randomBytes4 : ecdsaOpts.randomBytes;
6421
+ const randomBytes13 = ecdsaOpts.randomBytes === void 0 ? randomBytes4 : ecdsaOpts.randomBytes;
6422
6422
  const hmac5 = ecdsaOpts.hmac === void 0 ? (key, msg) => hmac(hash_, key, msg) : ecdsaOpts.hmac;
6423
6423
  const { Fp: Fp3, Fn: Fn3 } = Point;
6424
6424
  const { ORDER: CURVE_ORDER, BITS: fnBits } = Fn3;
@@ -6564,7 +6564,7 @@ function ecdsa(Point, hash, ecdsaOpts = {}) {
6564
6564
  throw new Error("invalid private key");
6565
6565
  const seedArgs = [int2octets(d), int2octets(h1int)];
6566
6566
  if (extraEntropy != null && extraEntropy !== false) {
6567
- const e = extraEntropy === true ? randomBytes12(lengths.secretKey) : extraEntropy;
6567
+ const e = extraEntropy === true ? randomBytes13(lengths.secretKey) : extraEntropy;
6568
6568
  seedArgs.push(abytes3(e, void 0, "extraEntropy"));
6569
6569
  }
6570
6570
  const seed = concatBytes4(...seedArgs);
@@ -18415,8 +18415,8 @@ function sameRef(a, b) {
18415
18415
  // ../connect-sdk-ts/dist/groupkey/own-removal.js
18416
18416
  async function probeOwnRemoval(mgr, commitBytes) {
18417
18417
  try {
18418
- const own = mgr.state?.privatePath?.leafIndex;
18419
- if (typeof own !== "number")
18418
+ const own2 = mgr.state?.privatePath?.leafIndex;
18419
+ if (typeof own2 !== "number")
18420
18420
  return null;
18421
18421
  if (!hoistedTsMlsMatchesCrypto(mgr))
18422
18422
  return null;
@@ -18441,7 +18441,7 @@ async function probeOwnRemoval(mgr, commitBytes) {
18441
18441
  committer = typeof incoming.senderLeafIndex === "number" ? Number(incoming.senderLeafIndex) : null;
18442
18442
  for (const p of incoming.proposals) {
18443
18443
  const prop = p.proposal;
18444
- if (prop.proposalType === "remove" && Number(prop.remove?.removed) === own)
18444
+ if (prop.proposalType === "remove" && Number(prop.remove?.removed) === own2)
18445
18445
  removesOwn = true;
18446
18446
  }
18447
18447
  }
@@ -18449,7 +18449,7 @@ async function probeOwnRemoval(mgr, commitBytes) {
18449
18449
  }, cs);
18450
18450
  if (!sawCommit || !removesOwn)
18451
18451
  return null;
18452
- return { ownLeafIndex: own, committerLeafIndex: committer };
18452
+ return { ownLeafIndex: own2, committerLeafIndex: committer };
18453
18453
  } catch {
18454
18454
  return null;
18455
18455
  }
@@ -19311,6 +19311,33 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
19311
19311
  }
19312
19312
  return { ownLeafIndex: this.ownLeafIndex(mgr), leaves };
19313
19313
  }
19314
+ /**
19315
+ * MCP client (bridge Task 16; audit H-2, round 2 H-D): a SYNCHRONOUS, read-only snapshot of the WARM in-memory
19316
+ * manager for `channelId`: its group id, its epoch and EVERY non-blank leaf, reported exactly as
19317
+ * {@link leafTreeSummary} reports them (non-basic credentials included, identity null, #771 HIGH-B). All three are
19318
+ * read from ONE `state` object (MLSGroupManager replaces `state` whole on every commit), with no await, so they belong
19319
+ * to the same epoch and no commit can land between them.
19320
+ *
19321
+ * `null` when no manager is warm: it never loads from the store, never takes the channel lock, never touches the
19322
+ * network and never mutates. A caller treating `null` as "cannot tell" fails closed.
19323
+ */
19324
+ leafSnapshotCached(channelId) {
19325
+ const state = this.managers.get(channelId)?.state;
19326
+ const tree = state?.ratchetTree;
19327
+ if (!state || !tree)
19328
+ return null;
19329
+ const leaves = [];
19330
+ const numLeaves = Math.floor((tree.length + 1) / 2);
19331
+ for (let i = 0; i < numLeaves; i++) {
19332
+ const node = tree[i * 2];
19333
+ if (!node || node.nodeType !== "leaf")
19334
+ continue;
19335
+ const cred = getCredentialFromLeafIndex(tree, toLeafIndex2(i));
19336
+ const basic = cred.credentialType === "basic";
19337
+ leaves.push({ leafIndex: i, basic, identity: basic ? new TextDecoder().decode(cred.identity) : null });
19338
+ }
19339
+ return { groupId: new TextDecoder().decode(state.groupContext.groupId), epoch: Number(state.groupContext.epoch), leaves };
19340
+ }
19314
19341
  async fetchDeviceLeafCredential(deviceId) {
19315
19342
  const kps = await this.o.backend.fetchKeyPackages([deviceId]);
19316
19343
  const hex = kps[deviceId];
@@ -24728,8 +24755,8 @@ function openDeliveryPings(o) {
24728
24755
  return Promise.resolve({ value: void 0, done: true });
24729
24756
  if (queue.length)
24730
24757
  return Promise.resolve({ value: queue.shift(), done: false });
24731
- return new Promise((resolve) => {
24732
- waiter = resolve;
24758
+ return new Promise((resolve2) => {
24759
+ waiter = resolve2;
24733
24760
  });
24734
24761
  }
24735
24762
  };
@@ -25049,8 +25076,8 @@ var PingSubscriber = class {
25049
25076
  return Promise.resolve({ value: void 0, done: true });
25050
25077
  if (this.queue.length)
25051
25078
  return Promise.resolve({ value: this.queue.shift(), done: false });
25052
- return new Promise((resolve) => {
25053
- this.waiter = resolve;
25079
+ return new Promise((resolve2) => {
25080
+ this.waiter = resolve2;
25054
25081
  });
25055
25082
  }
25056
25083
  };
@@ -26644,10 +26671,12 @@ function openSchema(db) {
26644
26671
  ensureRuntimeReplyIntent(db);
26645
26672
  ensureRuntimeReplyIntentEnvelopeGroup(db);
26646
26673
  ensureRuntimeReplyIntentCredit(db);
26674
+ ensureRuntimeReplyIntentMcpTurn(db);
26647
26675
  ensureGroupLink(db);
26648
26676
  ensureMlsCustody(db);
26649
26677
  ensureToolGrantTables(db);
26650
26678
  ensureToolGrantAppliedTable(db);
26679
+ ensureToolGrantMcpSeenTable(db);
26651
26680
  ensureRoomSearchCapTable(db);
26652
26681
  ensureVoiceChannelParentTable(db);
26653
26682
  db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
@@ -26737,6 +26766,19 @@ function ensureRuntimeReplyIntentCredit(db) {
26737
26766
  throw e;
26738
26767
  }
26739
26768
  }
26769
+ function ensureRuntimeReplyIntentMcpTurn(db) {
26770
+ const hasColumn = () => db.prepare("PRAGMA table_info(runtime_reply_intent)").all().some((c) => c.name === "mcp_turn");
26771
+ const cols = db.prepare("PRAGMA table_info(runtime_reply_intent)").all();
26772
+ if (cols.length === 0 || cols.some((c) => c.name === "mcp_turn"))
26773
+ return;
26774
+ try {
26775
+ db.exec("ALTER TABLE runtime_reply_intent ADD COLUMN mcp_turn INTEGER NOT NULL DEFAULT 0");
26776
+ } catch (e) {
26777
+ if (hasColumn())
26778
+ return;
26779
+ throw e;
26780
+ }
26781
+ }
26740
26782
  var RECALL_STATE_COLUMNS = [
26741
26783
  ["recall_enabled", "INTEGER NOT NULL DEFAULT 0"],
26742
26784
  ["recall_mode_epoch", "INTEGER NOT NULL DEFAULT 0"],
@@ -26848,6 +26890,15 @@ function ensureToolGrantAppliedTable(db) {
26848
26890
  );
26849
26891
  `);
26850
26892
  }
26893
+ function ensureToolGrantMcpSeenTable(db) {
26894
+ db.exec(`
26895
+ CREATE TABLE IF NOT EXISTS tool_grant_mcp_seen (
26896
+ identity_id TEXT PRIMARY KEY,
26897
+ sealed_state BLOB NOT NULL,
26898
+ updated_at INTEGER NOT NULL
26899
+ );
26900
+ `);
26901
+ }
26851
26902
  function ensureVoiceChannelParentTable(db) {
26852
26903
  db.exec(`
26853
26904
  CREATE TABLE IF NOT EXISTS voice_channel_parent (
@@ -27788,8 +27839,8 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
27788
27839
  this.db.prepare("DELETE FROM runtime_reply_intent WHERE identity_id=? AND updated_at < ?").run(this.id, now - RUNTIME_REPLY_INTENT_RETENTION_MS);
27789
27840
  const info = this.db.prepare(`INSERT OR IGNORE INTO runtime_reply_intent
27790
27841
  (identity_id, scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
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);
27842
+ sealed_body, sealed_envelope, state, created_at, updated_at, reply_credit, mcp_turn)
27843
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, 'pending', ?, ?, ?, ?)`).run(this.id, input.scope, input.part, input.inReplyTo, input.channelId, input.replyMessageId, input.bodyMac, sealed, now, now, input.replyCredit ?? null, input.mcpTurn === true ? 1 : 0);
27793
27844
  const count = this.db.prepare("SELECT COUNT(*) AS n FROM runtime_reply_intent WHERE identity_id=?").get(this.id);
27794
27845
  if (count.n > RUNTIME_REPLY_INTENT_MAX) {
27795
27846
  this.db.prepare(`DELETE FROM runtime_reply_intent
@@ -27799,7 +27850,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
27799
27850
  )`).run(this.id, this.id, RUNTIME_REPLY_INTENT_MAX);
27800
27851
  }
27801
27852
  const row = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
27802
- sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
27853
+ sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit, mcp_turn
27803
27854
  FROM runtime_reply_intent WHERE identity_id=? AND scope=? AND part=?`).get(this.id, input.scope, input.part);
27804
27855
  if (!row)
27805
27856
  throw new Error("reply intent vanished after claim");
@@ -27808,7 +27859,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
27808
27859
  /** SEC-06 #18: every intent answering `inReplyTo`, in claim order. */
27809
27860
  async listReplyIntents(inReplyTo) {
27810
27861
  const rows = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
27811
- sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
27862
+ sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit, mcp_turn
27812
27863
  FROM runtime_reply_intent WHERE identity_id=? AND in_reply_to=?
27813
27864
  ORDER BY created_at ASC, rowid ASC`).all(this.id, inReplyTo);
27814
27865
  const out = [];
@@ -27819,7 +27870,7 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
27819
27870
  /** SEC-06 #18: re-read one intent by its key (null when absent). */
27820
27871
  async getReplyIntent(scope, part) {
27821
27872
  const row = this.db.prepare(`SELECT scope, part, in_reply_to, channel_id, reply_message_id, body_mac,
27822
- sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit
27873
+ sealed_body, sealed_envelope, sealed_envelope_group, state, created_at, reply_credit, mcp_turn
27823
27874
  FROM runtime_reply_intent WHERE identity_id=? AND scope=? AND part=?`).get(this.id, scope, part);
27824
27875
  return row ? this.openReplyIntent(row) : null;
27825
27876
  }
@@ -27894,7 +27945,9 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
27894
27945
  envelopeGroupId: envelopeGroupFor(envelope, await openOrNull(row.sealed_envelope_group)),
27895
27946
  state: row.state === "sent" ? "sent" : row.state === "skipped" ? "skipped" : "pending",
27896
27947
  createdAt: Number(row.created_at),
27897
- replyCredit: typeof row.reply_credit === "string" ? row.reply_credit : null
27948
+ replyCredit: typeof row.reply_credit === "string" ? row.reply_credit : null,
27949
+ // Any value other than exactly 1 (0, NULL, a missing column) is false; the bridge's MAC decides what a 1 means.
27950
+ mcpTurn: Number(row.mcp_turn ?? 0) === 1
27898
27951
  };
27899
27952
  }
27900
27953
  async recordMemory(input) {
@@ -28466,10 +28519,10 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
28466
28519
  const r = this.db.prepare("SELECT owner_size_bytes AS b, owner_item_count AS n FROM agent_memory_state WHERE identity_id=?").get(this.id);
28467
28520
  if (r && Number(r.b) >= 0 && Number(r.n) >= 0)
28468
28521
  return { items: Number(r.n), bytes: Number(r.b) };
28469
- const own = this.scopeLoad("i.scope_kind='owner_dm'");
28522
+ const own2 = this.scopeLoad("i.scope_kind='owner_dm'");
28470
28523
  this.ensureStateRow();
28471
- this.db.prepare("UPDATE agent_memory_state SET owner_size_bytes=?, owner_item_count=? WHERE identity_id=?").run(own.bytes, own.items, this.id);
28472
- return own;
28524
+ this.db.prepare("UPDATE agent_memory_state SET owner_size_bytes=?, owner_item_count=? WHERE identity_id=?").run(own2.bytes, own2.items, this.id);
28525
+ return own2;
28473
28526
  }
28474
28527
  /** Inside a transaction: delete one origin group (the row, or the parent and its chunks) and its token rows; returns the ids and the accounts it held. */
28475
28528
  deleteItemGroup(firstItemId) {
@@ -28643,8 +28696,8 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
28643
28696
  const o2 = this.overCap();
28644
28697
  if (!o2.overItems && !o2.overBytes)
28645
28698
  break;
28646
- const own = this.ownerLoad();
28647
- const protectOwner = (!o2.overItems || own.items < this.itemCap * OWNER_DM_FLOOR_SHARE) && (!o2.overBytes || own.bytes < this.byteCap * OWNER_DM_FLOOR_SHARE);
28699
+ const own2 = this.ownerLoad();
28700
+ const protectOwner = (!o2.overItems || own2.items < this.itemCap * OWNER_DM_FLOOR_SHARE) && (!o2.overBytes || own2.bytes < this.byteCap * OWNER_DM_FLOOR_SHARE);
28648
28701
  const v = (protectOwner ? this.oldestAutoGroup({ notOwnerDm: true }) : void 0) ?? this.oldestAutoGroup({});
28649
28702
  if (v === void 0 || !drop(v))
28650
28703
  break;
@@ -28671,8 +28724,8 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
28671
28724
  FROM agent_memory_item i WHERE identity_id=?`).get(this.id);
28672
28725
  const tk = this.db.prepare("SELECT COUNT(*) AS n FROM agent_memory_token WHERE identity_id=?").get(this.id);
28673
28726
  const size = Number(it.b) + Number(it.rows) * ITEM_ROW_OVERHEAD_BYTES + Number(tk.n) * TOKEN_ROW_OVERHEAD_BYTES;
28674
- const own = this.scopeLoad("i.scope_kind='owner_dm'");
28675
- this.db.prepare("UPDATE agent_memory_state SET size_bytes=?, item_count=?, owner_size_bytes=?, owner_item_count=? WHERE identity_id=?").run(size, Number(it.n), own.bytes, own.items, this.id);
28727
+ const own2 = this.scopeLoad("i.scope_kind='owner_dm'");
28728
+ this.db.prepare("UPDATE agent_memory_state SET size_bytes=?, item_count=?, owner_size_bytes=?, owner_item_count=? WHERE identity_id=?").run(size, Number(it.n), own2.bytes, own2.items, this.id);
28676
28729
  }
28677
28730
  /** Inside a transaction: delete every unified row of the identity and move each channel's content epoch. */
28678
28731
  deleteAllItems() {
@@ -29488,9 +29541,40 @@ var SqliteMlsStateStore = class _SqliteMlsStateStore {
29488
29541
  dropToolGrantAppliedExcept(pinKeyB64) {
29489
29542
  return Number(this.db.prepare("DELETE FROM tool_grant_applied WHERE identity_id=? AND pin_key_b64<>?").run(this.id, pinKeyB64).changes);
29490
29543
  }
29544
+ // MCP client Phase 1, Task 14 (OQ-3 ruling item 3): the sticky R-3 flag, per agent identity.
29545
+ /**
29546
+ * False when never written. A row that does not open, or opens to anything but this identity's own record, THROWS:
29547
+ * the bridge then holds R-3 (fail closed). Never returns false for an unreadable row.
29548
+ */
29549
+ async loadMcpSeen() {
29550
+ const row = this.db.prepare("SELECT sealed_state FROM tool_grant_mcp_seen WHERE identity_id=?").get(this.id);
29551
+ if (!row)
29552
+ return false;
29553
+ const v = await this.unsealJson(row.sealed_state);
29554
+ if (v === null || typeof v !== "object" || v.identity_id !== this.id || typeof v.seen !== "boolean") {
29555
+ throw new MlsStoreSealError("tool_grant_mcp_seen: unexpected record");
29556
+ }
29557
+ return v.seen;
29558
+ }
29559
+ /** The sealed value names its identity, so a blob copied to another agent's row does not read as that agent's. */
29560
+ async saveMcpSeen(seen) {
29561
+ const sealed = await this.sealJson({ identity_id: this.id, seen });
29562
+ this.db.prepare(`INSERT INTO tool_grant_mcp_seen (identity_id, sealed_state, updated_at) VALUES (?, ?, ?)
29563
+ ON CONFLICT(identity_id) DO UPDATE SET sealed_state = excluded.sealed_state, updated_at = excluded.updated_at`).run(this.id, sealed, Date.now());
29564
+ }
29491
29565
  hasOwnerMessage(channelId, messageId) {
29492
29566
  return this.db.prepare("SELECT 1 FROM owner_replay WHERE identity_id=? AND channel_id=? AND message_id=?").get(this.id, channelId, messageId) !== void 0;
29493
29567
  }
29568
+ /**
29569
+ * MCP client Phase 1, Task 18 (audit M-3): the owner replay row for a verified owner message that is NEVER staged for
29570
+ * the runtime (an `mcp_catalog_request`), so a redelivery of it is classed `replay`, after a restart too. The same row
29571
+ * stageRuntimeInbox writes in its staging transaction; insert-if-absent; pruned by pruneOwnerReplay like any other.
29572
+ */
29573
+ recordOwnerMessage(channelId, messageId, groupId, epoch, processedAt = Date.now()) {
29574
+ this.db.prepare(`INSERT OR IGNORE INTO owner_replay
29575
+ (identity_id, channel_id, message_id, group_id, epoch, processed_at)
29576
+ VALUES (?, ?, ?, ?, ?, ?)`).run(this.id, channelId, messageId, groupId, epoch, processedAt);
29577
+ }
29494
29578
  isOwnerEpochClosed(channelId, groupId, epoch) {
29495
29579
  return this.db.prepare("SELECT 1 FROM owner_replay_closed_epoch WHERE identity_id=? AND channel_id=? AND group_id=? AND epoch=?").get(this.id, channelId, groupId, epoch) !== void 0;
29496
29580
  }
@@ -29664,8 +29748,9 @@ function decodeReplyBody(bytes) {
29664
29748
  if (b?.kind === "a2a" && b.a2a && typeof b.a2a === "object") return b;
29665
29749
  throw new Error("reply intent body is malformed");
29666
29750
  }
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");
29751
+ function replyBodyMac(key, replyId, channelId, body, credit, mcpTurn) {
29752
+ const label = mcpTurn === true ? ["reply-body", replyId, channelId, credit ?? null, "mcp_turn"] : credit === void 0 ? ["reply-body", replyId, channelId] : ["reply-body", replyId, channelId, credit];
29753
+ return createHmac("sha256", key).update(JSON.stringify(label)).update("\0").update(body).digest("hex");
29669
29754
  }
29670
29755
  function partOrdinal(part) {
29671
29756
  const m = /^p:(\d+)$/.exec(part);
@@ -29706,8 +29791,96 @@ function loadPackageMeta(importMetaUrl) {
29706
29791
  }
29707
29792
 
29708
29793
  // src/bridge-agent.ts
29709
- import { existsSync as existsSync5, promises as fsp, readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
29710
- import { join as join17, dirname as dirname5 } from "node:path";
29794
+ import { existsSync as existsSync5, promises as fsp2, readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
29795
+
29796
+ // src/outbox.ts
29797
+ import { constants, mkdirSync, lstatSync, chmodSync, realpathSync, promises as fsp } from "node:fs";
29798
+ import { basename, dirname, join, resolve } from "node:path";
29799
+ var VAULT_OUTBOX_DIRNAME = "vault-outbox";
29800
+ var PathOutsideOutboxError = class extends Error {
29801
+ constructor() {
29802
+ super("path_outside_outbox");
29803
+ this.name = "PathOutsideOutboxError";
29804
+ }
29805
+ };
29806
+ var POSIX = process.platform !== "win32";
29807
+ function ownedPrivateDir(st) {
29808
+ if (st.isSymbolicLink() || !st.isDirectory()) return false;
29809
+ if (!POSIX) return true;
29810
+ const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
29811
+ if (uid !== void 0 && st.uid !== uid) return false;
29812
+ return (st.mode & 511) === 448;
29813
+ }
29814
+ function ensureOutbox(dir) {
29815
+ mkdirSync(dir, { recursive: true, mode: 448 });
29816
+ let st = lstatSync(dir);
29817
+ if (st.isSymbolicLink() || !st.isDirectory()) throw new PathOutsideOutboxError();
29818
+ const uid = POSIX && typeof process.getuid === "function" ? process.getuid() : void 0;
29819
+ if (uid !== void 0 && st.uid !== uid) throw new PathOutsideOutboxError();
29820
+ if (POSIX && (st.mode & 511) !== 448) {
29821
+ chmodSync(dir, 448);
29822
+ st = lstatSync(dir);
29823
+ }
29824
+ if (!ownedPrivateDir(st)) throw new PathOutsideOutboxError();
29825
+ return { dir, real: realpathSync(dir), dev: st.dev, ino: st.ino };
29826
+ }
29827
+ var NOFOLLOW = constants.O_NOFOLLOW ?? 0;
29828
+ var NONBLOCK = constants.O_NONBLOCK ?? 0;
29829
+ async function readOutboxFile(pin, callerPath) {
29830
+ if (!pin) throw new PathOutsideOutboxError();
29831
+ try {
29832
+ const st = await fsp.lstat(pin.dir);
29833
+ if (!ownedPrivateDir(st) || st.dev !== pin.dev || st.ino !== pin.ino) throw new PathOutsideOutboxError();
29834
+ } catch {
29835
+ throw new PathOutsideOutboxError();
29836
+ }
29837
+ const requested = resolve(pin.dir, callerPath);
29838
+ const name = basename(requested);
29839
+ let realParent;
29840
+ try {
29841
+ realParent = await fsp.realpath(dirname(requested));
29842
+ } catch {
29843
+ throw new PathOutsideOutboxError();
29844
+ }
29845
+ if (realParent !== pin.real) throw new PathOutsideOutboxError();
29846
+ const target = join(pin.real, name);
29847
+ const pre = await fsp.lstat(target);
29848
+ if (pre.isSymbolicLink() || !pre.isFile()) throw new PathOutsideOutboxError();
29849
+ let handle;
29850
+ try {
29851
+ handle = await fsp.open(target, constants.O_RDONLY | NOFOLLOW | NONBLOCK);
29852
+ } catch (e) {
29853
+ if (e.code === "ENOENT") throw e;
29854
+ throw new PathOutsideOutboxError();
29855
+ }
29856
+ try {
29857
+ const st = await handle.stat();
29858
+ if (!st.isFile() || st.dev !== pre.dev || st.ino !== pre.ino || st.nlink !== 1) {
29859
+ throw new PathOutsideOutboxError();
29860
+ }
29861
+ return { bytes: new Uint8Array(await handle.readFile()), path: target, dev: st.dev, ino: st.ino };
29862
+ } finally {
29863
+ await handle.close().catch(() => {
29864
+ });
29865
+ }
29866
+ }
29867
+ async function consumeOutboxFile(pin, read) {
29868
+ try {
29869
+ const dir = await fsp.lstat(pin.dir);
29870
+ if (!ownedPrivateDir(dir) || dir.dev !== pin.dev || dir.ino !== pin.ino) return false;
29871
+ if (await fsp.realpath(pin.dir) !== pin.real) return false;
29872
+ const target = join(pin.real, basename(read.path));
29873
+ const st = await fsp.lstat(target);
29874
+ if (st.isSymbolicLink() || st.dev !== read.dev || st.ino !== read.ino) return false;
29875
+ await fsp.unlink(target);
29876
+ return true;
29877
+ } catch {
29878
+ return false;
29879
+ }
29880
+ }
29881
+
29882
+ // src/bridge-agent.ts
29883
+ import { join as join18, dirname as dirname6 } from "node:path";
29711
29884
  import { fileURLToPath as fileURLToPath2 } from "node:url";
29712
29885
 
29713
29886
  // src/latency.ts
@@ -30205,11 +30378,21 @@ function pinCoverage(pinStripped, ownerMessage, coverTexts) {
30205
30378
  }
30206
30379
 
30207
30380
  // src/attachments.ts
30208
- import { basename } from "node:path";
30381
+ import { basename as basename2 } from "node:path";
30209
30382
  var VAULT_REF_MAX_SIZE_HINT = 100 * 1024 * 1024 - 16;
30210
30383
  var VAULT_REF_MAX_NAME = 255;
30211
30384
  var VAULT_REF_MAX_MIME = 127;
30212
30385
  var CANONICAL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
30386
+ var InvalidBlobIdError = class extends Error {
30387
+ constructor() {
30388
+ super("invalid_blob_id");
30389
+ this.name = "InvalidBlobIdError";
30390
+ }
30391
+ };
30392
+ function assertBlobId(blobId) {
30393
+ if (typeof blobId !== "string" || !CANONICAL_UUID.test(blobId)) throw new InvalidBlobIdError();
30394
+ return blobId;
30395
+ }
30213
30396
  var VAULT_PIN = /^blake2b:[0-9a-f]{64}$/;
30214
30397
  var HINT_FORBIDDEN = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/;
30215
30398
  var MIME_TYPE = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$/;
@@ -30248,7 +30431,7 @@ function toVaultRefEntry(body) {
30248
30431
  return entry;
30249
30432
  }
30250
30433
  function sanitizeFilename(name, fallback) {
30251
- const base = basename(name ?? "");
30434
+ const base = basename2(name ?? "");
30252
30435
  const cleaned = base.replace(/[/\\]/g, "").replace(/^\.+$/, "");
30253
30436
  return cleaned.length > 0 ? cleaned : fallback;
30254
30437
  }
@@ -30278,7 +30461,7 @@ function mimeForName(name) {
30278
30461
  async function prepareAttachment(filePath2, caption, channelId, deps) {
30279
30462
  await deps.ready();
30280
30463
  const bytes = await deps.readFile(filePath2);
30281
- const name = basename(filePath2);
30464
+ const name = basename2(filePath2);
30282
30465
  const { encryptedData, fileKey, fileNonce, digest } = await deps.encryptFile(bytes);
30283
30466
  const blobId = await deps.uploadBlob(encryptedData, channelId);
30284
30467
  const body = {
@@ -30296,8 +30479,9 @@ async function prepareAttachment(filePath2, caption, channelId, deps) {
30296
30479
  return body;
30297
30480
  }
30298
30481
  async function materializeAttachment(body, ctx, deps) {
30482
+ const blobId = assertBlobId(body.blobId);
30299
30483
  await deps.ready();
30300
- const ciphertext = await deps.downloadBlob(body.blobId);
30484
+ const ciphertext = await deps.downloadBlob(blobId);
30301
30485
  const sha25612 = await deps.computeFileDigest(ciphertext);
30302
30486
  if (sha25612 !== body.digest) {
30303
30487
  throw new Error(`attachment digest mismatch for blob ${body.blobId}`);
@@ -30323,13 +30507,13 @@ function ensureSodiumReady2() {
30323
30507
  // src/memory/hermes-session-map.ts
30324
30508
  import { readFileSync } from "node:fs";
30325
30509
  import { homedir } from "node:os";
30326
- import { join } from "node:path";
30510
+ import { join as join2 } from "node:path";
30327
30511
  function expandHome(p) {
30328
- return p === "~" || p.startsWith("~/") ? join(homedir(), p.slice(1)) : p;
30512
+ return p === "~" || p.startsWith("~/") ? join2(homedir(), p.slice(1)) : p;
30329
30513
  }
30330
30514
  function resolveHermesSession(baseDir, conversationId) {
30331
30515
  try {
30332
- const path = join(expandHome(baseDir), "sessions", "sessions.json");
30516
+ const path = join2(expandHome(baseDir), "sessions", "sessions.json");
30333
30517
  const map = JSON.parse(readFileSync(path, "utf8"));
30334
30518
  const entry = map[`agent:main:securecoms:dm:${conversationId}`];
30335
30519
  const sid = entry?.session_id;
@@ -30341,11 +30525,11 @@ function resolveHermesSession(baseDir, conversationId) {
30341
30525
 
30342
30526
  // src/memory/hermes-store.ts
30343
30527
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
30344
- import { join as join2 } from "node:path";
30528
+ import { join as join3 } from "node:path";
30345
30529
  var HermesStore = class {
30346
30530
  dbPath;
30347
30531
  constructor(baseDir) {
30348
- this.dbPath = join2(expandHome(baseDir), "state.db");
30532
+ this.dbPath = join3(expandHome(baseDir), "state.db");
30349
30533
  }
30350
30534
  /** Recent-K conversational turns for a session, chronological. Fail-safe → []. */
30351
30535
  readRecent(sessionId, limit) {
@@ -30487,10 +30671,10 @@ var HermesMemoryProvider = class {
30487
30671
 
30488
30672
  // src/memory/openclaw-session-map.ts
30489
30673
  import { readFileSync as readFileSync2 } from "node:fs";
30490
- import { join as join3 } from "node:path";
30674
+ import { join as join4 } from "node:path";
30491
30675
  function resolveOwnerDmSession(baseDir, agent) {
30492
30676
  try {
30493
- const path = join3(expandHome(baseDir), "agents", agent, "sessions", "sessions.json");
30677
+ const path = join4(expandHome(baseDir), "agents", agent, "sessions", "sessions.json");
30494
30678
  const map = JSON.parse(readFileSync2(path, "utf8"));
30495
30679
  let best = null;
30496
30680
  for (const [key, entry] of Object.entries(map)) {
@@ -30508,11 +30692,11 @@ function resolveOwnerDmSession(baseDir, agent) {
30508
30692
 
30509
30693
  // src/memory/openclaw-session-log.ts
30510
30694
  import { readFileSync as readFileSync3 } from "node:fs";
30511
- import { join as join4 } from "node:path";
30695
+ import { join as join5 } from "node:path";
30512
30696
  function readRecentTurns(baseDir, agent, sessionId, k) {
30513
30697
  let raw;
30514
30698
  try {
30515
- raw = readFileSync3(join4(expandHome(baseDir), "agents", agent, "sessions", `${sessionId}.jsonl`), "utf8");
30699
+ raw = readFileSync3(join5(expandHome(baseDir), "agents", agent, "sessions", `${sessionId}.jsonl`), "utf8");
30516
30700
  } catch {
30517
30701
  return [];
30518
30702
  }
@@ -30543,7 +30727,7 @@ function readRecentTurns(baseDir, agent, sessionId, k) {
30543
30727
 
30544
30728
  // src/memory/openclaw-workspace-facts.ts
30545
30729
  import { readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
30546
- import { join as join5 } from "node:path";
30730
+ import { join as join6 } from "node:path";
30547
30731
  var DEFAULT_FACTS_BUDGET = 8e3;
30548
30732
  var MEMORY_MD_CAP = 6e3;
30549
30733
  var MIN_FRAGMENT = 200;
@@ -30554,7 +30738,7 @@ function resolveWorkspaceDir(baseDir, agent, override) {
30554
30738
  const base = expandHome(baseDir);
30555
30739
  const isMain = agent === "" || agent === "main";
30556
30740
  try {
30557
- const cfg = JSON.parse(readFileSync4(join5(base, "openclaw.json"), "utf8"));
30741
+ const cfg = JSON.parse(readFileSync4(join6(base, "openclaw.json"), "utf8"));
30558
30742
  const agents = cfg?.agents;
30559
30743
  if (agents && typeof agents === "object") {
30560
30744
  const list = Array.isArray(agents.list) ? agents.list : [];
@@ -30567,7 +30751,7 @@ function resolveWorkspaceDir(baseDir, agent, override) {
30567
30751
  }
30568
30752
  } catch {
30569
30753
  }
30570
- return isMain ? join5(base, "workspace") : join5(base, `workspace-${agent}`);
30754
+ return isMain ? join6(base, "workspace") : join6(base, `workspace-${agent}`);
30571
30755
  }
30572
30756
  var TRUNC = "\u2026[truncated]";
30573
30757
  function headSlice(s, n) {
@@ -30597,7 +30781,7 @@ function readWorkspaceFacts(baseDir, agent, opts) {
30597
30781
  let truncated = false;
30598
30782
  let remaining = budget;
30599
30783
  try {
30600
- const r = readBounded(join5(dir, "MEMORY.md"));
30784
+ const r = readBounded(join6(dir, "MEMORY.md"));
30601
30785
  if (r.oversize) truncated = true;
30602
30786
  else if (r.text && r.text.trim()) {
30603
30787
  const { text, clipped } = headSlice(r.text, Math.min(MEMORY_MD_CAP, budget));
@@ -30610,7 +30794,7 @@ ${TRUNC}` : ""}`);
30610
30794
  } catch {
30611
30795
  }
30612
30796
  try {
30613
- const dailies = readdirSync(join5(dir, "memory")).filter((f) => DAILY_RE.test(f)).sort().reverse().slice(0, 2);
30797
+ const dailies = readdirSync(join6(dir, "memory")).filter((f) => DAILY_RE.test(f)).sort().reverse().slice(0, 2);
30614
30798
  for (const f of dailies) {
30615
30799
  if (remaining < MIN_FRAGMENT) {
30616
30800
  truncated = true;
@@ -30618,7 +30802,7 @@ ${TRUNC}` : ""}`);
30618
30802
  }
30619
30803
  let r;
30620
30804
  try {
30621
- r = readBounded(join5(dir, "memory", f));
30805
+ r = readBounded(join6(dir, "memory", f));
30622
30806
  } catch {
30623
30807
  continue;
30624
30808
  }
@@ -31281,8 +31465,8 @@ var MembershipPurgeSweeper = class {
31281
31465
  const dms = /* @__PURE__ */ new Set();
31282
31466
  for (const c of this.d.conversations()) {
31283
31467
  if (stopping()) return { purged: [], held: 0, absent: 0 };
31284
- const own = this.d.enclaveOf(c);
31285
- const channels = own === void 0 ? void 0 : listed.get(own);
31468
+ const own2 = this.d.enclaveOf(c);
31469
+ const channels = own2 === void 0 ? void 0 : listed.get(own2);
31286
31470
  if (channels === void 0) continue;
31287
31471
  if (channels.has(c)) {
31288
31472
  this.d.clearAbsence(c);
@@ -31357,21 +31541,21 @@ function parsePayloadMemory(raw) {
31357
31541
 
31358
31542
  // src/memory/embedder/client.ts
31359
31543
  import { connect } from "node:net";
31360
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync7, rmSync as rmSync6, statSync as statSync2 } from "node:fs";
31544
+ import { existsSync as existsSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync7, rmSync as rmSync6, statSync as statSync2 } from "node:fs";
31361
31545
  import { fileURLToPath } from "node:url";
31362
31546
 
31363
31547
  // src/memory/embedder/download.ts
31364
31548
  import { createHash as createHash2, randomBytes as randomBytes6 } from "node:crypto";
31365
- import { chmodSync as chmodSync2, rmSync as rmSync2 } from "node:fs";
31549
+ import { chmodSync as chmodSync3, rmSync as rmSync2 } from "node:fs";
31366
31550
  import { open } from "node:fs/promises";
31367
- import { join as join7 } from "node:path";
31551
+ import { join as join8 } from "node:path";
31368
31552
 
31369
31553
  // src/memory/embedder/layout.ts
31370
- import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync } from "node:fs";
31554
+ import { chmodSync as chmodSync2, lstatSync as lstatSync2, mkdirSync as mkdirSync2, renameSync, rmSync } from "node:fs";
31371
31555
  import { homedir as homedir2 } from "node:os";
31372
- import { basename as basename2, dirname, join as join6 } from "node:path";
31373
- var defaultModelsRoot = (home = homedir2()) => join6(home, ".securecoms", "models", "embeddings");
31374
- var filePath = (root3, modelId, f) => join6(root3, modelId, f.sha256, f.name);
31556
+ import { basename as basename3, dirname as dirname2, join as join7 } from "node:path";
31557
+ var defaultModelsRoot = (home = homedir2()) => join7(home, ".securecoms", "models", "embeddings");
31558
+ var filePath = (root3, modelId, f) => join7(root3, modelId, f.sha256, f.name);
31375
31559
  var LayoutRefused = class extends Error {
31376
31560
  constructor() {
31377
31561
  super("the model directory is not a plain directory");
@@ -31379,32 +31563,32 @@ var LayoutRefused = class extends Error {
31379
31563
  };
31380
31564
  function kind(p) {
31381
31565
  try {
31382
- return lstatSync(p).isDirectory() ? "dir" : "other";
31566
+ return lstatSync2(p).isDirectory() ? "dir" : "other";
31383
31567
  } catch (e) {
31384
31568
  if (e.code === "ENOENT") return "absent";
31385
31569
  throw e;
31386
31570
  }
31387
31571
  }
31388
31572
  function plainDirChain(root3, parts, opts) {
31389
- const modelsDir = dirname(root3);
31573
+ const modelsDir = dirname2(root3);
31390
31574
  for (const part of parts) if (!part || part === "." || part === ".." || part.includes("/") || part.includes("\\")) throw new LayoutRefused();
31391
31575
  const k = kind(modelsDir);
31392
31576
  if (k === "other") throw new LayoutRefused();
31393
31577
  if (k === "absent") {
31394
31578
  if (!opts.create) return null;
31395
- mkdirSync(modelsDir, { recursive: true, mode: 448 });
31579
+ mkdirSync2(modelsDir, { recursive: true, mode: 448 });
31396
31580
  if (kind(modelsDir) !== "dir") throw new LayoutRefused();
31397
31581
  }
31398
- const chain = opts.fromModelsDir ? parts : [basename2(root3), ...parts];
31582
+ const chain = opts.fromModelsDir ? parts : [basename3(root3), ...parts];
31399
31583
  let p = modelsDir;
31400
31584
  for (let i = 0; i < chain.length; i++) {
31401
- p = join6(p, chain[i]);
31585
+ p = join7(p, chain[i]);
31402
31586
  const c = kind(p);
31403
31587
  if (c === "other") throw new LayoutRefused();
31404
31588
  if (c === "absent") {
31405
31589
  if (!opts.create) return null;
31406
31590
  try {
31407
- mkdirSync(p, { mode: i === chain.length - 1 && opts.mode !== void 0 ? opts.mode : 493 });
31591
+ mkdirSync2(p, { mode: i === chain.length - 1 && opts.mode !== void 0 ? opts.mode : 493 });
31408
31592
  } catch (e) {
31409
31593
  if (e.code !== "EEXIST") throw e;
31410
31594
  }
@@ -31415,7 +31599,7 @@ function plainDirChain(root3, parts, opts) {
31415
31599
  }
31416
31600
  function presentWithSize(p, size) {
31417
31601
  try {
31418
- const s = lstatSync(p);
31602
+ const s = lstatSync2(p);
31419
31603
  return s.isFile() && s.size === size;
31420
31604
  } catch {
31421
31605
  return false;
@@ -31423,7 +31607,7 @@ function presentWithSize(p, size) {
31423
31607
  }
31424
31608
  function wrongTypeAt(p) {
31425
31609
  try {
31426
- const s = lstatSync(p);
31610
+ const s = lstatSync2(p);
31427
31611
  return !s.isFile() && !s.isSymbolicLink();
31428
31612
  } catch {
31429
31613
  return false;
@@ -31436,16 +31620,16 @@ function installVerified(root3, modelId, f, partial, opts) {
31436
31620
  dir = plainDirChain(root3, [modelId, f.sha256], { create: true });
31437
31621
  if (!dir) throw new LayoutRefused();
31438
31622
  if (!opts.replace && presentWithSize(dest, f.size)) return;
31439
- chmodSync(dir, 493);
31623
+ chmodSync2(dir, 493);
31440
31624
  try {
31441
31625
  renameSync(partial, dest);
31442
31626
  } catch (e) {
31443
31627
  if (!presentWithSize(dest, f.size)) throw e;
31444
31628
  return;
31445
31629
  }
31446
- chmodSync(dest, 292);
31630
+ chmodSync2(dest, 292);
31447
31631
  } finally {
31448
- if (dir) chmodSync(dir, 365);
31632
+ if (dir) chmodSync2(dir, 365);
31449
31633
  rmSync(partial, { force: true });
31450
31634
  }
31451
31635
  }
@@ -31465,8 +31649,8 @@ function removeModelFiles(root3, m) {
31465
31649
  for (const f of m.files) {
31466
31650
  const dir = plainDirChain(root3, [m.modelId, f.sha256], { create: false });
31467
31651
  if (!dir) continue;
31468
- chmodSync2(dir, 493);
31469
- rmSync2(join7(dir, f.name), { force: true });
31652
+ chmodSync3(dir, 493);
31653
+ rmSync2(join8(dir, f.name), { force: true });
31470
31654
  }
31471
31655
  }
31472
31656
  var VerifyFailed = class extends Error {
@@ -31513,7 +31697,7 @@ async function fetchSameOrigin(url, origin, fetchImpl, signal, stallMs, abort) {
31513
31697
  async function downloadOne(f, o) {
31514
31698
  if (wrongTypeAt(filePath(o.root, o.modelId, f))) throw new WrongType("wrong type at model path");
31515
31699
  const pdir = plainDirChain(o.root, [".partial"], { create: true, fromModelsDir: true, mode: 448 });
31516
- const partial = join7(pdir, randomBytes6(12).toString("hex"));
31700
+ const partial = join8(pdir, randomBytes6(12).toString("hex"));
31517
31701
  const fh = await open(partial, "wx", 384);
31518
31702
  const ac = new AbortController();
31519
31703
  const abort = () => ac.abort();
@@ -31589,17 +31773,17 @@ async function ensureModelFiles(o) {
31589
31773
  }
31590
31774
 
31591
31775
  // src/memory/embedder/gc.ts
31592
- import { chmodSync as chmodSync3, closeSync, constants, fstatSync, futimesSync, lstatSync as lstatSync2, openSync, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
31593
- import { join as join8 } from "node:path";
31776
+ import { chmodSync as chmodSync4, closeSync, constants as constants2, fstatSync, futimesSync, lstatSync as lstatSync3, openSync, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
31777
+ import { join as join9 } from "node:path";
31594
31778
  var THIRTY_DAYS = 30 * 864e5;
31595
31779
  var SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
31596
31780
  function touchUserMarker(root3, modelId, identityId) {
31597
31781
  if (!SAFE_ID.test(identityId)) throw new Error("invalid identity id for the model marker");
31598
31782
  const dir = plainDirChain(root3, [modelId, ".users"], { create: true });
31599
- const p = join8(dir, identityId);
31783
+ const p = join9(dir, identityId);
31600
31784
  let fd;
31601
31785
  try {
31602
- fd = openSync(p, constants.O_WRONLY | constants.O_CREAT | constants.O_NOFOLLOW | constants.O_NONBLOCK, 420);
31786
+ fd = openSync(p, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_NOFOLLOW | constants2.O_NONBLOCK, 420);
31603
31787
  } catch {
31604
31788
  throw new Error("the model marker is not a regular file");
31605
31789
  }
@@ -31613,22 +31797,22 @@ function touchUserMarker(root3, modelId, identityId) {
31613
31797
  }
31614
31798
  function isRealDir(p) {
31615
31799
  try {
31616
- return lstatSync2(p).isDirectory();
31800
+ return lstatSync3(p).isDirectory();
31617
31801
  } catch {
31618
31802
  return false;
31619
31803
  }
31620
31804
  }
31621
31805
  function makeWritable(p) {
31622
31806
  if (!isRealDir(p)) return;
31623
- chmodSync3(p, 493);
31624
- for (const e of readdirSync2(p)) makeWritable(join8(p, e));
31807
+ chmodSync4(p, 493);
31808
+ for (const e of readdirSync2(p)) makeWritable(join9(p, e));
31625
31809
  }
31626
31810
  function newestMarkerMs(users) {
31627
31811
  if (!isRealDir(users)) return void 0;
31628
31812
  let newest;
31629
31813
  for (const u of readdirSync2(users)) {
31630
31814
  try {
31631
- const s = lstatSync2(join8(users, u));
31815
+ const s = lstatSync3(join9(users, u));
31632
31816
  if (!s.isFile()) continue;
31633
31817
  newest = newest === void 0 ? s.mtimeMs : Math.max(newest, s.mtimeMs);
31634
31818
  } catch {
@@ -31641,9 +31825,9 @@ function gcModels(root3, keepModelIds, now, maxAgeMs = THIRTY_DAYS) {
31641
31825
  const removed = [];
31642
31826
  for (const modelId of readdirSync2(root3).sort()) {
31643
31827
  if (modelId.startsWith(".") || keepModelIds.includes(modelId)) continue;
31644
- const tree = join8(root3, modelId);
31828
+ const tree = join9(root3, modelId);
31645
31829
  if (!isRealDir(tree)) continue;
31646
- const lastUse = newestMarkerMs(join8(tree, ".users")) ?? lstatSync2(tree).mtimeMs;
31830
+ const lastUse = newestMarkerMs(join9(tree, ".users")) ?? lstatSync3(tree).mtimeMs;
31647
31831
  if (now - lastUse <= maxAgeMs) continue;
31648
31832
  makeWritable(tree);
31649
31833
  rmSync3(tree, { recursive: true, force: true });
@@ -31776,44 +31960,44 @@ var SealedChannel = class {
31776
31960
  // src/memory/embedder/embedder-paths.ts
31777
31961
  import { createHash as createHash3 } from "node:crypto";
31778
31962
  import { homedir as homedir3 } from "node:os";
31779
- import { join as join9 } from "node:path";
31963
+ import { join as join10 } from "node:path";
31780
31964
  function embedderKey(m) {
31781
31965
  const model = m.files.find((f) => f.name === "model.onnx");
31782
31966
  return createHash3("sha256").update(`${EMBEDDER_PROTOCOL}\0${m.modelId}\0${model.sha256}\0${m.wasmSha256}`).digest("hex").slice(0, 16);
31783
31967
  }
31784
- var embedderRunDir = (home = homedir3()) => join9(home, ".securecoms", "run", "embedder");
31968
+ var embedderRunDir = (home = homedir3()) => join10(home, ".securecoms", "run", "embedder");
31785
31969
  var embedderPaths = (dir, key) => ({
31786
- sock: join9(dir, `${key}.sock`),
31787
- token: join9(dir, `${key}.token`),
31788
- lock: join9(dir, `${key}.spawn.lock`),
31789
- status: join9(dir, `${key}.status`),
31790
- pid: join9(dir, `${key}.pid`)
31970
+ sock: join10(dir, `${key}.sock`),
31971
+ token: join10(dir, `${key}.token`),
31972
+ lock: join10(dir, `${key}.spawn.lock`),
31973
+ status: join10(dir, `${key}.status`),
31974
+ pid: join10(dir, `${key}.pid`)
31791
31975
  // E1 delta H1: the instance's lifetime lock
31792
31976
  });
31793
31977
  var SOCKET_PATH_MAX = 103;
31794
31978
  var windowsRunDir = (env = process.env) => {
31795
31979
  if (!env.LOCALAPPDATA) throw new Error("LOCALAPPDATA unset");
31796
- return join9(env.LOCALAPPDATA, "SecureComs", "run", "embedder");
31980
+ return join10(env.LOCALAPPDATA, "SecureComs", "run", "embedder");
31797
31981
  };
31798
31982
 
31799
31983
  // src/memory/embedder/spawn.ts
31800
31984
  import { spawn } from "node:child_process";
31801
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, realpathSync } from "node:fs";
31985
+ import { existsSync as existsSync2, mkdirSync as mkdirSync4, realpathSync as realpathSync2 } from "node:fs";
31802
31986
 
31803
31987
  // src/memory/embedder/run-dir.ts
31804
- import { existsSync, lstatSync as lstatSync3, mkdirSync as mkdirSync2 } from "node:fs";
31805
- import { dirname as dirname2 } from "node:path";
31988
+ import { existsSync, lstatSync as lstatSync4, mkdirSync as mkdirSync3 } from "node:fs";
31989
+ import { dirname as dirname3 } from "node:path";
31806
31990
  var RunDirRefused = class extends Error {
31807
31991
  };
31808
- function ensureRunDir(dir, uid = process.getuid?.() ?? -1, lstat = lstatSync3) {
31809
- const parent3 = dirname2(dir);
31810
- if (!existsSync(parent3)) mkdirSync2(parent3, { recursive: true, mode: 448 });
31992
+ function ensureRunDir(dir, uid = process.getuid?.() ?? -1, lstat = lstatSync4) {
31993
+ const parent3 = dirname3(dir);
31994
+ if (!existsSync(parent3)) mkdirSync3(parent3, { recursive: true, mode: 448 });
31811
31995
  const ps = lstat(parent3);
31812
31996
  if (ps.isSymbolicLink() || !ps.isDirectory()) throw new RunDirRefused("run directory parent is not a real directory");
31813
31997
  if (ps.uid !== uid) throw new RunDirRefused("run directory parent is not owned by this user");
31814
31998
  if ((ps.mode & 18) !== 0) throw new RunDirRefused("run directory parent is group or world writable");
31815
31999
  try {
31816
- mkdirSync2(dir, { mode: 448 });
32000
+ mkdirSync3(dir, { mode: 448 });
31817
32001
  } catch (e) {
31818
32002
  if (e.code !== "EEXIST") throw e;
31819
32003
  }
@@ -31824,10 +32008,10 @@ function ensureRunDir(dir, uid = process.getuid?.() ?? -1, lstat = lstatSync3) {
31824
32008
  }
31825
32009
 
31826
32010
  // src/memory/embedder/spawn.ts
31827
- import { basename as basename3, dirname as dirname3, join as join10 } from "node:path";
32011
+ import { basename as basename4, dirname as dirname4, join as join11 } from "node:path";
31828
32012
 
31829
32013
  // src/memory/embedder/locks.ts
31830
- 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";
32014
+ 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";
31831
32015
  import { randomBytes as randomBytes9 } from "node:crypto";
31832
32016
  import { uptime } from "node:os";
31833
32017
  var LOCK_STALE_MS = 1e4;
@@ -31849,7 +32033,7 @@ var PROCESS_LOCK_ENV = {
31849
32033
  function inspect(path) {
31850
32034
  let fd;
31851
32035
  try {
31852
- fd = openSync2(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
32036
+ fd = openSync2(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
31853
32037
  } catch {
31854
32038
  return null;
31855
32039
  }
@@ -31873,7 +32057,7 @@ function takeLock(path, now = Date.now, env = PROCESS_LOCK_ENV) {
31873
32057
  const id = `${env.pid} ${env.bootTime()} ${env.startTime()} ${randomBytes9(8).toString("hex")}`;
31874
32058
  for (let attempt = 0; attempt < 3; attempt++) {
31875
32059
  try {
31876
- const fd = openSync2(path, constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY, 384);
32060
+ const fd = openSync2(path, constants3.O_CREAT | constants3.O_EXCL | constants3.O_WRONLY, 384);
31877
32061
  try {
31878
32062
  writeSync(fd, id);
31879
32063
  } finally {
@@ -31934,7 +32118,7 @@ function embedderEnv(env = process.env, platform = process.platform) {
31934
32118
  }
31935
32119
  var withReal = (p) => {
31936
32120
  try {
31937
- const r = realpathSync(p);
32121
+ const r = realpathSync2(p);
31938
32122
  return r === p ? [p] : [p, r];
31939
32123
  } catch {
31940
32124
  return [p];
@@ -31942,10 +32126,10 @@ var withReal = (p) => {
31942
32126
  };
31943
32127
  var realish = (p) => {
31944
32128
  try {
31945
- return realpathSync(p);
32129
+ return realpathSync2(p);
31946
32130
  } catch {
31947
- const up = dirname3(p);
31948
- return up === p ? p : join10(realish(up), basename3(p));
32131
+ const up = dirname4(p);
32132
+ return up === p ? p : join11(realish(up), basename4(p));
31949
32133
  }
31950
32134
  };
31951
32135
  var both = (p) => {
@@ -31957,7 +32141,7 @@ function spawnEmbedder(o) {
31957
32141
  const run = o.runDir ?? embedderRunDir(o.home);
31958
32142
  const win = (o.platform ?? process.platform) === "win32";
31959
32143
  if (o.createRunDir !== false) {
31960
- if (win) mkdirSync3(run, { recursive: true, mode: 448 });
32144
+ if (win) mkdirSync4(run, { recursive: true, mode: 448 });
31961
32145
  else ensureRunDir(run);
31962
32146
  }
31963
32147
  const perm = permissionFlags();
@@ -31966,13 +32150,13 @@ function spawnEmbedder(o) {
31966
32150
  o.onNoPermissionModel?.();
31967
32151
  }
31968
32152
  const reads = perm.length === 0 ? [] : [
31969
- ...withReal(dirname3(o.distDir)),
32153
+ ...withReal(dirname4(o.distDir)),
31970
32154
  ...existsSync2(defaultModelsRoot(o.home)) ? withReal(defaultModelsRoot(o.home)) : [defaultModelsRoot(o.home)],
31971
- ...both(dirname3(run)),
31972
- ...win ? both(join10(o.home, ".securecoms", "run")) : []
32155
+ ...both(dirname4(run)),
32156
+ ...win ? both(join11(o.home, ".securecoms", "run")) : []
31973
32157
  // the voice-call heartbeats (the run dir lives elsewhere)
31974
32158
  ];
31975
- const script = withReal(join10(o.distDir, "embedder.js")).at(-1);
32159
+ const script = withReal(join11(o.distDir, "embedder.js")).at(-1);
31976
32160
  const net = (o.nodeFlags ?? process.allowedNodeEnvironmentFlags).has("--allow-net") ? ["--allow-net"] : [];
31977
32161
  const flags = perm.length === 0 ? [] : [...perm, ...reads.map((r) => `--allow-fs-read=${r}`), ...both(run).map((w) => `--allow-fs-write=${w}`), "--allow-worker", ...net];
31978
32162
  const child = (o.spawnFn ?? spawn)(
@@ -31981,7 +32165,7 @@ function spawnEmbedder(o) {
31981
32165
  // 11c review LOW 3, fixed by the Task 12 Mini gate: cwd is the run directory's PARENT, which the bridge created (0700)
31982
32166
  // before spawning; the run directory itself is created by the embedder, so on a first start it does not exist yet and
31983
32167
  // a cwd there fails the spawn with ENOENT. Never the bridge's working directory.
31984
- { 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 } }
32168
+ { 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 } }
31985
32169
  );
31986
32170
  child.unref();
31987
32171
  child.on("error", () => {
@@ -31993,8 +32177,8 @@ function spawnEmbedder(o) {
31993
32177
  import { execFile } from "node:child_process";
31994
32178
  import { promisify } from "node:util";
31995
32179
  import { randomBytes as randomBytes10 } from "node:crypto";
31996
- import { lstatSync as lstatSync4, readFileSync as readFileSync6, rmSync as rmSync5 } from "node:fs";
31997
- import { join as join11, win32 } from "node:path";
32180
+ import { lstatSync as lstatSync5, readFileSync as readFileSync6, rmSync as rmSync5 } from "node:fs";
32181
+ import { join as join12, win32 } from "node:path";
31998
32182
  var WINDOWS_SEMANTIC_ENABLED = false;
31999
32183
  var windowsSemanticOn = (platform, enabled = WINDOWS_SEMANTIC_ENABLED) => platform !== "win32" || enabled;
32000
32184
  function parseSaveSddl(saveFile) {
@@ -32022,7 +32206,7 @@ function aclAllows(sddl, ownerSid) {
32022
32206
  if (aces.length === 0 || rest !== "" && !rest.startsWith("S:")) return false;
32023
32207
  return aces.every((a) => a[0] === "A" && allowed.has(a[5] ?? "") || a[0] === "D");
32024
32208
  }
32025
- function hasReparsePoint(path, lstat = lstatSync4) {
32209
+ function hasReparsePoint(path, lstat = lstatSync5) {
32026
32210
  const root3 = win32.parse(path).root;
32027
32211
  let cur = root3;
32028
32212
  for (const part of path.slice(root3.length).split(/[\\/]/).filter(Boolean)) {
@@ -32041,7 +32225,7 @@ async function currentUserSid(run = defaultRunner) {
32041
32225
  }
32042
32226
  async function verifyRunDirAcl(path, deps = {}, sid) {
32043
32227
  const run = deps.run ?? defaultRunner;
32044
- const out = join11(deps.tmp ?? process.env.TEMP ?? process.env.TMP ?? ".", `sc-acl-${randomBytes10(8).toString("hex")}.txt`);
32228
+ const out = join12(deps.tmp ?? process.env.TEMP ?? process.env.TMP ?? ".", `sc-acl-${randomBytes10(8).toString("hex")}.txt`);
32045
32229
  try {
32046
32230
  if (hasReparsePoint(path, deps.lstat)) return false;
32047
32231
  const user = await (sid ? sid() : currentUserSid(run));
@@ -32122,12 +32306,12 @@ var EmbedderClient = class {
32122
32306
  ready(ms) {
32123
32307
  if (this.st.state === "ready") return Promise.resolve(true);
32124
32308
  if (this.st.state !== "preparing" || !(ms > 0)) return Promise.resolve(false);
32125
- return new Promise((resolve) => {
32309
+ return new Promise((resolve2) => {
32126
32310
  let t;
32127
32311
  const w = (ok) => {
32128
32312
  clearTimeout(t);
32129
32313
  this.readyWaiters.delete(w);
32130
- resolve(ok);
32314
+ resolve2(ok);
32131
32315
  };
32132
32316
  this.readyWaiters.add(w);
32133
32317
  t = setTimeout(() => w(false), ms);
@@ -32236,7 +32420,7 @@ var EmbedderClient = class {
32236
32420
  const p = this.paths();
32237
32421
  if (!this.tcp() && Buffer.byteLength(`${p.sock}.00000000`) > SOCKET_PATH_MAX) return this.fail("run_dir_refused");
32238
32422
  if (this.tcp()) {
32239
- mkdirSync4(this.runDir(), { recursive: true, mode: 448 });
32423
+ mkdirSync5(this.runDir(), { recursive: true, mode: 448 });
32240
32424
  if (!await this.aclOk(true)) return this.fail("acl_unverified");
32241
32425
  }
32242
32426
  if (!this.tcp()) {
@@ -32372,7 +32556,7 @@ var EmbedderClient = class {
32372
32556
  }
32373
32557
  if (this.closed) return false;
32374
32558
  }
32375
- return new Promise((resolve) => {
32559
+ return new Promise((resolve2) => {
32376
32560
  const s = this.tcp() ? connect({ host: "127.0.0.1", port: pub.port }) : connect(p.sock);
32377
32561
  const nc = nonce();
32378
32562
  let ns = "";
@@ -32385,7 +32569,7 @@ var EmbedderClient = class {
32385
32569
  if (!settled) {
32386
32570
  settled = true;
32387
32571
  clearTimeout(t);
32388
- resolve(ok);
32572
+ resolve2(ok);
32389
32573
  }
32390
32574
  }
32391
32575
  const refuse2 = (reason) => {
@@ -32950,12 +33134,12 @@ var ReembedQueue = class {
32950
33134
  };
32951
33135
 
32952
33136
  // src/memory/recall/call-heartbeat.ts
32953
- import { closeSync as closeSync3, constants as constants3, fstatSync as fstatSync3, futimesSync as futimesSync2, openSync as openSync3, rmSync as rmSync7 } from "node:fs";
32954
- import { join as join13 } from "node:path";
33137
+ import { closeSync as closeSync3, constants as constants4, fstatSync as fstatSync3, futimesSync as futimesSync2, openSync as openSync3, rmSync as rmSync7 } from "node:fs";
33138
+ import { join as join14 } from "node:path";
32955
33139
 
32956
33140
  // src/memory/embedder/heartbeats.ts
32957
- import { lstatSync as lstatSync5, readdirSync as readdirSync3 } from "node:fs";
32958
- import { join as join12 } from "node:path";
33141
+ import { lstatSync as lstatSync6, readdirSync as readdirSync3 } from "node:fs";
33142
+ import { join as join13 } from "node:path";
32959
33143
  var HEARTBEAT_FUTURE_SKEW_MS = 6e4;
32960
33144
  var HEARTBEAT_STALE_MS = 3e4;
32961
33145
  function heartbeatAgeFresh(ageMs, staleMs = HEARTBEAT_STALE_MS) {
@@ -32970,7 +33154,7 @@ function anyHeartbeatFresh(dir, now, staleMs = HEARTBEAT_STALE_MS) {
32970
33154
  }
32971
33155
  return names.some((n) => {
32972
33156
  try {
32973
- const st = lstatSync5(join12(dir, n));
33157
+ const st = lstatSync6(join13(dir, n));
32974
33158
  return st.isFile() && heartbeatAgeFresh(now - st.mtimeMs, staleMs);
32975
33159
  } catch {
32976
33160
  return false;
@@ -33036,7 +33220,7 @@ var CallHeartbeat = class {
33036
33220
  if (dir === null) return;
33037
33221
  let fd;
33038
33222
  try {
33039
- fd = openSync3(join13(dir, this.identityId), constants3.O_WRONLY | constants3.O_CREAT | constants3.O_NOFOLLOW | constants3.O_NONBLOCK, 384);
33223
+ fd = openSync3(join14(dir, this.identityId), constants4.O_WRONLY | constants4.O_CREAT | constants4.O_NOFOLLOW | constants4.O_NONBLOCK, 384);
33040
33224
  if (!fstatSync3(fd).isFile()) return;
33041
33225
  const t = new Date(this.now());
33042
33226
  futimesSync2(fd, t, t);
@@ -33051,7 +33235,7 @@ var CallHeartbeat = class {
33051
33235
  const dir = SAFE_ID2.test(this.identityId) ? this.dirOk() : null;
33052
33236
  if (dir === null) return;
33053
33237
  try {
33054
- rmSync7(join13(dir, this.identityId), { force: true });
33238
+ rmSync7(join14(dir, this.identityId), { force: true });
33055
33239
  this.wrote = false;
33056
33240
  } catch (e) {
33057
33241
  this.log("warn", "memory.call_heartbeat_failed", {}, e);
@@ -33114,9 +33298,9 @@ function queryTokenMacs(key, text, max = QUERY_MACS_MAX) {
33114
33298
 
33115
33299
  // src/memory/unified/ranking-client.ts
33116
33300
  import { Worker } from "node:worker_threads";
33117
- import { join as join14 } from "node:path";
33301
+ import { join as join15 } from "node:path";
33118
33302
  function workerTransport(init, distDir) {
33119
- const w = new Worker(join14(distDir, "memory-worker.js"), { workerData: init });
33303
+ const w = new Worker(join15(distDir, "memory-worker.js"), { workerData: init });
33120
33304
  return {
33121
33305
  post: (m) => w.postMessage(m),
33122
33306
  onMessage: (cb) => {
@@ -33176,15 +33360,15 @@ var RankingClient = class {
33176
33360
  ask(build, deadlineAt) {
33177
33361
  const id = ++this.seq;
33178
33362
  if (this.closed) return Promise.resolve("failed");
33179
- return new Promise((resolve) => {
33363
+ return new Promise((resolve2) => {
33180
33364
  const timer = setTimeout(() => {
33181
33365
  this.waiting.delete(id);
33182
33366
  this.misses++;
33183
- resolve("late");
33367
+ resolve2("late");
33184
33368
  }, Math.max(0, deadlineAt - Date.now()));
33185
33369
  this.waiting.set(id, { resolve: (m) => {
33186
33370
  clearTimeout(timer);
33187
- resolve(m);
33371
+ resolve2(m);
33188
33372
  } });
33189
33373
  try {
33190
33374
  this.transport().post(build(id));
@@ -33193,7 +33377,7 @@ var RankingClient = class {
33193
33377
  this.waiting.delete(id);
33194
33378
  this.misses++;
33195
33379
  (this.d.log ?? writeServiceLog2)("warn", "memory.ranking_worker_spawn_failed", void 0, e);
33196
- resolve("failed");
33380
+ resolve2("failed");
33197
33381
  }
33198
33382
  });
33199
33383
  }
@@ -34787,17 +34971,17 @@ async function runSummarize(opts) {
34787
34971
  // src/service/runtime-config-dir.ts
34788
34972
  import { createHash as createHash5, randomUUID } from "node:crypto";
34789
34973
  import {
34790
- chmodSync as chmodSync4,
34974
+ chmodSync as chmodSync5,
34791
34975
  existsSync as existsSync4,
34792
- lstatSync as lstatSync6,
34793
- mkdirSync as mkdirSync5,
34976
+ lstatSync as lstatSync7,
34977
+ mkdirSync as mkdirSync6,
34794
34978
  readFileSync as readFileSync8,
34795
34979
  renameSync as renameSync3,
34796
34980
  statSync as statSync3,
34797
34981
  unlinkSync,
34798
34982
  writeFileSync
34799
34983
  } from "node:fs";
34800
- import { basename as basename4, isAbsolute, join as join15 } from "node:path";
34984
+ import { basename as basename5, isAbsolute, join as join16 } from "node:path";
34801
34985
 
34802
34986
  // src/grants/search-providers.ts
34803
34987
  var SEARCH_PROVIDERS = /* @__PURE__ */ new Set(["brave", "tavily", "parallel", "exa", "firecrawl"]);
@@ -34809,18 +34993,18 @@ function isSearchProvider(value) {
34809
34993
  // src/service/runtime-config-dir.ts
34810
34994
  var MEMORY_FILE = "MEMORY.md";
34811
34995
  var REAL_FS = {
34812
- mkdir: (p, mode) => mkdirSync5(p, { recursive: true, mode }),
34813
- chmod: (p, mode) => chmodSync4(p, mode),
34996
+ mkdir: (p, mode) => mkdirSync6(p, { recursive: true, mode }),
34997
+ chmod: (p, mode) => chmodSync5(p, mode),
34814
34998
  writeFile: (p, data, mode) => writeFileSync(p, data, { mode }),
34815
34999
  rename: (a, b) => renameSync3(a, b),
34816
35000
  readFile: (p) => readFileSync8(p, "utf8"),
34817
35001
  unlink: (p) => unlinkSync(p)
34818
35002
  };
34819
35003
  function runtimeConfigDir(home, identityId) {
34820
- return join15(home, ".securecoms", "runtime-config", identityId);
35004
+ return join16(home, ".securecoms", "runtime-config", identityId);
34821
35005
  }
34822
35006
  function assertManagedName(name) {
34823
- if (!name || name.includes("/") || name.includes("\\") || name === "." || name === ".." || name !== basename4(name)) {
35007
+ if (!name || name.includes("/") || name.includes("\\") || name === "." || name === ".." || name !== basename5(name)) {
34824
35008
  throw new Error(`refusing managed config file name ${JSON.stringify(name)}: not a plain filename`);
34825
35009
  }
34826
35010
  if (isLocalOnlyName(name)) {
@@ -34829,24 +35013,46 @@ function assertManagedName(name) {
34829
35013
  }
34830
35014
  function isLocalOnlyName(name) {
34831
35015
  const n = name.toLowerCase().replace(/[. ]+$/, "");
34832
- return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json" || n === "secrets" || n === MANIFEST_POLICY_FILE;
35016
+ return n === "llm.key" || n.startsWith("llm.key.") || n === "local-config.json" || n === "secrets" || n === MANIFEST_POLICY_FILE || n === MCP_POLICY_FILE;
34833
35017
  }
34834
35018
  var MANIFEST_POLICY_FILE = "manifest-policy.json";
34835
35019
  function removeManagedLlmSource(home, identityId, deps = REAL_FS) {
34836
35020
  try {
34837
- deps.unlink(join15(runtimeConfigDir(home, identityId), "llm-source.json"));
35021
+ deps.unlink(join16(runtimeConfigDir(home, identityId), "llm-source.json"));
34838
35022
  } catch (e) {
34839
35023
  if (e?.code !== "ENOENT") throw e;
34840
35024
  }
34841
35025
  }
34842
35026
  function writeManifestPolicy(home, identityId, policy, deps = REAL_FS) {
34843
- const parent3 = join15(home, ".securecoms", "runtime-config");
35027
+ const parent3 = join16(home, ".securecoms", "runtime-config");
35028
+ const dir = runtimeConfigDir(home, identityId);
35029
+ for (const d of [join16(home, ".securecoms"), parent3, dir]) {
35030
+ deps.mkdir(d, 448);
35031
+ deps.chmod(d, 448);
35032
+ }
35033
+ atomicWrite(join16(dir, MANIFEST_POLICY_FILE), JSON.stringify(policy) + "\n", deps);
35034
+ }
35035
+ var MCP_POLICY_FILE = "mcp-policy.json";
35036
+ var MCP_POLICY_TOP_STATES = Object.freeze([
35037
+ "ok",
35038
+ "not_entitled",
35039
+ "kill_switch",
35040
+ "suspended",
35041
+ "no_config",
35042
+ "switch_off",
35043
+ "no_manifest",
35044
+ "malformed",
35045
+ "no_capability",
35046
+ "section_absent"
35047
+ ]);
35048
+ function writeMcpPolicy(home, identityId, doc, deps = REAL_FS) {
35049
+ const parent3 = join16(home, ".securecoms", "runtime-config");
34844
35050
  const dir = runtimeConfigDir(home, identityId);
34845
- for (const d of [join15(home, ".securecoms"), parent3, dir]) {
35051
+ for (const d of [join16(home, ".securecoms"), parent3, dir]) {
34846
35052
  deps.mkdir(d, 448);
34847
35053
  deps.chmod(d, 448);
34848
35054
  }
34849
- atomicWrite(join15(dir, MANIFEST_POLICY_FILE), JSON.stringify(policy) + "\n", deps);
35055
+ atomicWrite(join16(dir, MCP_POLICY_FILE), JSON.stringify(doc) + "\n", deps);
34850
35056
  }
34851
35057
  function atomicWrite(path, body, deps) {
34852
35058
  const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
@@ -34865,13 +35071,13 @@ function atomicWrite(path, body, deps) {
34865
35071
  function writeManagedFiles(home, identityId, files, deps = REAL_FS) {
34866
35072
  const entries = Object.entries(files);
34867
35073
  for (const [name] of entries) assertManagedName(name);
34868
- const parent3 = join15(home, ".securecoms", "runtime-config");
35074
+ const parent3 = join16(home, ".securecoms", "runtime-config");
34869
35075
  const dir = runtimeConfigDir(home, identityId);
34870
- for (const d of [join15(home, ".securecoms"), parent3, dir]) {
35076
+ for (const d of [join16(home, ".securecoms"), parent3, dir]) {
34871
35077
  deps.mkdir(d, 448);
34872
35078
  deps.chmod(d, 448);
34873
35079
  }
34874
- for (const [name, body] of entries) atomicWrite(join15(dir, name), body, deps);
35080
+ for (const [name, body] of entries) atomicWrite(join16(dir, name), body, deps);
34875
35081
  }
34876
35082
  function checkConfiguredPaths(skillsJson, statFn = (p) => {
34877
35083
  const s = statSync3(p);
@@ -34950,7 +35156,7 @@ function localLegacySources(doc) {
34950
35156
  const channelId = source.channelId;
34951
35157
  if (kind2 !== "global-memory" && kind2 !== "conversation-history" && kind2 !== "captured-memory" || typeof path !== "string" || !isAbsolute(path) || channelId !== void 0 && typeof channelId !== "string") continue;
34952
35158
  try {
34953
- const st = lstatSync6(path);
35159
+ const st = lstatSync7(path);
34954
35160
  if (!st.isFile() || st.isSymbolicLink()) continue;
34955
35161
  if (st.size <= LEGACY_MEMORY_SOURCE_MAX_BYTES && st.size > contentBudget) continue;
34956
35162
  const oversize = st.size > LEGACY_MEMORY_SOURCE_MAX_BYTES;
@@ -34978,7 +35184,7 @@ function localLegacySources(doc) {
34978
35184
  return out;
34979
35185
  }
34980
35186
  function readLocalReport(home, identityId) {
34981
- const p = join15(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
35187
+ const p = join16(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
34982
35188
  if (!existsSync4(p)) return null;
34983
35189
  let doc;
34984
35190
  try {
@@ -35013,7 +35219,7 @@ function readLocalReport(home, identityId) {
35013
35219
  }
35014
35220
  function deleteLegacyMemorySources(home, identityId, ids) {
35015
35221
  if (ids.length === 0) return 0;
35016
- const p = join15(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
35222
+ const p = join16(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
35017
35223
  if (!existsSync4(p)) return 0;
35018
35224
  let doc;
35019
35225
  try {
@@ -35056,7 +35262,7 @@ async function applyLegacyMemoryFactDecisions(home, identityId, rawDecisions, re
35056
35262
  group.set(decision.fact_hash, decision);
35057
35263
  bySource.set(decision.source_id, group);
35058
35264
  }
35059
- const reportPath = join15(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
35265
+ const reportPath = join16(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
35060
35266
  if (!existsSync4(reportPath)) return { sourcesDeleted: 0, factsAssigned: 0 };
35061
35267
  let doc;
35062
35268
  try {
@@ -35094,7 +35300,7 @@ async function applyLegacyMemoryFactDecisions(home, identityId, rawDecisions, re
35094
35300
  }
35095
35301
  function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
35096
35302
  const dir = runtimeConfigDir(home, identityId);
35097
- const path = join15(dir, MEMORY_FILE);
35303
+ const path = join16(dir, MEMORY_FILE);
35098
35304
  if (!existsSync4(path)) return 0;
35099
35305
  const drop = new Set(hashes);
35100
35306
  const before = deps.readFile(path).split("\n");
@@ -35127,9 +35333,9 @@ import { sha256 as sha25611 } from "@noble/hashes/sha256";
35127
35333
  import { ed25519 as ed255195 } from "@noble/curves/ed25519";
35128
35334
 
35129
35335
  // src/grants/pin.ts
35130
- import { lstatSync as lstatSync7, readFileSync as readFileSync9 } from "node:fs";
35336
+ import { lstatSync as lstatSync8, readFileSync as readFileSync9 } from "node:fs";
35131
35337
  import { ed25519 as ed255194 } from "@noble/curves/ed25519";
35132
- import { dirname as dirname4, join as join16 } from "node:path";
35338
+ import { dirname as dirname5, join as join17 } from "node:path";
35133
35339
  var PIN_FILE_NAME = "owner-signing-key.json";
35134
35340
  var UUID_LOWER = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
35135
35341
  var PIN_KEYS = ["key_algo", "owner_identity_id", "public_key"];
@@ -35159,7 +35365,7 @@ function parsePinObject(v) {
35159
35365
  pin: Object.freeze({ ownerIdentityId: o.owner_identity_id, keyAlgo: "ed25519", publicKey, publicKeyB64: o.public_key })
35160
35366
  };
35161
35367
  }
35162
- var NODE_FS = { lstat: (p) => lstatSync7(p), readFile: (p) => readFileSync9(p, "utf8") };
35368
+ var NODE_FS = { lstat: (p) => lstatSync8(p), readFile: (p) => readFileSync9(p, "utf8") };
35163
35369
  function fromValue(v) {
35164
35370
  const r = parsePinObject(v);
35165
35371
  return r.ok ? { kind: "ok", pin: r.pin } : { kind: "bad", state: r.state };
@@ -35193,7 +35399,7 @@ function samePin(a, b) {
35193
35399
  }
35194
35400
  function loadOwnerGrantPin(opts) {
35195
35401
  const fs = opts.fs ?? NODE_FS;
35196
- const file2 = readPinFile(join16(dirname4(opts.credentialPath), PIN_FILE_NAME), fs);
35402
+ const file2 = readPinFile(join17(dirname5(opts.credentialPath), PIN_FILE_NAME), fs);
35197
35403
  const field = "owner_signing_key" in opts.credentialJson && opts.credentialJson.owner_signing_key !== void 0 ? fromValue(opts.credentialJson.owner_signing_key) : { kind: "absent" };
35198
35404
  if (file2.kind === "bad") return { state: file2.state, pin: null };
35199
35405
  if (field.kind === "bad") return { state: field.state, pin: null };
@@ -35205,6 +35411,193 @@ function loadOwnerGrantPin(opts) {
35205
35411
  return { state: "ok", pin: one };
35206
35412
  }
35207
35413
 
35414
+ // src/grants/mcp-section.ts
35415
+ var MCP_MAX_SERVERS = 8;
35416
+ var MCP_MAX_TOOLS_PER_SERVER = 64;
35417
+ var MCP_MAX_SIGNED_TOOLS = 160;
35418
+ var MCP_MAX_REMOVED = 8;
35419
+ var SECURECOMS_CIMD_CLIENT_ID = "https://app.securecoms.ai/oauth/securecoms-agent-client.json";
35420
+ var MCP_SERVER_ID = /^[a-z0-9-]{1,24}$/;
35421
+ var MCP_TOOL_NAME = /^[A-Za-z0-9_.-]{1,64}$/;
35422
+ var VENDOR_KEY = /^[a-z0-9_]{1,32}$/;
35423
+ var MCP_RESERVED_NAMES = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
35424
+ var HEX64 = /^[0-9a-f]{64}$/;
35425
+ var PRINTABLE_ASCII = /^[\x21-\x7e]{1,256}$/;
35426
+ var MCP_GRANT = /^mcp:([a-z0-9-]{1,24})\/([A-Za-z0-9_.-]{1,64})$/;
35427
+ var isBag = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
35428
+ var keysExactly = (o, keys) => {
35429
+ const have = Object.keys(o);
35430
+ return have.length === keys.length && keys.every((k) => Object.prototype.hasOwnProperty.call(o, k));
35431
+ };
35432
+ function canonicalHttpsUrl(v) {
35433
+ if (typeof v !== "string" || !PRINTABLE_ASCII.test(v) || !URL.canParse(v)) return false;
35434
+ const u = new URL(v);
35435
+ return u.protocol === "https:" && u.username === "" && u.password === "" && u.href === v;
35436
+ }
35437
+ function httpsIssuer(v) {
35438
+ if (typeof v !== "string" || !PRINTABLE_ASCII.test(v) || !URL.canParse(v)) return false;
35439
+ const u = new URL(v);
35440
+ return u.protocol === "https:" && u.username === "" && u.password === "" && u.search === "" && u.hash === "" && (u.href === v || u.href === `${v}/`);
35441
+ }
35442
+ var MCP_AUTH_KEYS = Object.freeze({
35443
+ oauth_securecoms_cimd: Object.freeze(["mode", "issuer", "client"]),
35444
+ oauth_customer: Object.freeze(["mode", "issuer", "client", "client_id"]),
35445
+ key: Object.freeze(["mode"])
35446
+ });
35447
+ function parseAuth(v) {
35448
+ if (!isBag(v)) return null;
35449
+ if (v.mode === "key") return keysExactly(v, MCP_AUTH_KEYS.key) ? Object.freeze({ mode: "key" }) : null;
35450
+ if (v.mode !== "oauth" || !httpsIssuer(v.issuer)) return null;
35451
+ if (v.client === "securecoms_cimd") {
35452
+ if (!keysExactly(v, MCP_AUTH_KEYS.oauth_securecoms_cimd)) return null;
35453
+ return Object.freeze({ mode: "oauth", issuer: v.issuer, client: "securecoms_cimd" });
35454
+ }
35455
+ if (v.client === "customer") {
35456
+ if (!keysExactly(v, MCP_AUTH_KEYS.oauth_customer)) return null;
35457
+ if (typeof v.client_id !== "string" || !PRINTABLE_ASCII.test(v.client_id)) return null;
35458
+ return Object.freeze({ mode: "oauth", issuer: v.issuer, client: "customer", client_id: v.client_id });
35459
+ }
35460
+ return null;
35461
+ }
35462
+ function parseTools(v) {
35463
+ if (!isBag(v)) return null;
35464
+ const entries = Object.entries(v);
35465
+ if (entries.length > MCP_MAX_TOOLS_PER_SERVER) return null;
35466
+ const out = /* @__PURE__ */ Object.create(null);
35467
+ for (const [name, t] of entries) {
35468
+ if (!MCP_TOOL_NAME.test(name) || MCP_RESERVED_NAMES.has(name) || !isBag(t) || !keysExactly(t, ["sha256", "class"])) return null;
35469
+ if (typeof t.sha256 !== "string" || !HEX64.test(t.sha256) || t.class !== "read") return null;
35470
+ out[name] = Object.freeze({ sha256: t.sha256, class: "read" });
35471
+ }
35472
+ return out;
35473
+ }
35474
+ function parseServer(v) {
35475
+ if (!isBag(v) || !keysExactly(v, ["vendor", "url", "auth", "tools"])) return null;
35476
+ if (typeof v.vendor !== "string" || !VENDOR_KEY.test(v.vendor) || !canonicalHttpsUrl(v.url)) return null;
35477
+ const auth = parseAuth(v.auth);
35478
+ const tools = parseTools(v.tools);
35479
+ if (auth === null || tools === null) return null;
35480
+ return Object.freeze({ vendor: v.vendor, url: v.url, auth, tools: Object.freeze(tools) });
35481
+ }
35482
+ function parseRemoved(v, servers) {
35483
+ if (v === "*") return Object.keys(servers).length === 0 ? "*" : null;
35484
+ if (!Array.isArray(v) || v.length < 1 || v.length > MCP_MAX_REMOVED) return null;
35485
+ const seen = /* @__PURE__ */ new Set();
35486
+ for (const id of v) {
35487
+ if (typeof id !== "string" || !MCP_SERVER_ID.test(id) || MCP_RESERVED_NAMES.has(id)) return null;
35488
+ if (seen.has(id) || Object.prototype.hasOwnProperty.call(servers, id)) return null;
35489
+ seen.add(id);
35490
+ }
35491
+ return Object.freeze([...seen]);
35492
+ }
35493
+ var NOT_REMOVED = Object.freeze([]);
35494
+ function parseMcp(v) {
35495
+ if (!isBag(v) || !(keysExactly(v, ["servers"]) || keysExactly(v, ["servers", "removed"])) || !isBag(v.servers)) return null;
35496
+ const entries = Object.entries(v.servers);
35497
+ if (entries.length > MCP_MAX_SERVERS) return null;
35498
+ const servers = /* @__PURE__ */ Object.create(null);
35499
+ let signedTools = 0;
35500
+ for (const [id, raw] of entries) {
35501
+ if (!MCP_SERVER_ID.test(id) || MCP_RESERVED_NAMES.has(id)) return null;
35502
+ const s = parseServer(raw);
35503
+ if (s === null) return null;
35504
+ signedTools += Object.keys(s.tools).length;
35505
+ if (signedTools > MCP_MAX_SIGNED_TOOLS) return null;
35506
+ servers[id] = s;
35507
+ }
35508
+ let removed = NOT_REMOVED;
35509
+ if (Object.prototype.hasOwnProperty.call(v, "removed")) {
35510
+ const r = parseRemoved(v.removed, servers);
35511
+ if (r === null) return null;
35512
+ removed = r;
35513
+ }
35514
+ return Object.freeze({ servers: Object.freeze(servers), removed });
35515
+ }
35516
+ function mcpServerCatalogRow(s, catalog) {
35517
+ if (!Object.prototype.hasOwnProperty.call(catalog, s.vendor)) return null;
35518
+ const row = catalog[s.vendor];
35519
+ if (s.url !== row.readOnlyUrl) return null;
35520
+ if (s.auth.mode === "oauth" && s.auth.issuer !== row.issuer) return null;
35521
+ return row;
35522
+ }
35523
+ function parseMcpGrant(name) {
35524
+ const g = MCP_GRANT.exec(name);
35525
+ return g === null ? null : { serverId: g[1], tool: g[2] };
35526
+ }
35527
+ function mcpGrantWithinCeiling(name, mcp, catalog) {
35528
+ const g = parseMcpGrant(name);
35529
+ if (g === null || mcp === null || !Object.prototype.hasOwnProperty.call(mcp.servers, g.serverId)) return false;
35530
+ const s = mcp.servers[g.serverId];
35531
+ if (!Object.prototype.hasOwnProperty.call(s.tools, g.tool)) return false;
35532
+ const row = mcpServerCatalogRow(s, catalog);
35533
+ return row !== null && row.readTools.includes(g.tool);
35534
+ }
35535
+
35536
+ // src/grants/mcp-catalog.ts
35537
+ var LINEAR = Object.freeze({
35538
+ displayName: "Linear",
35539
+ host: "mcp.linear.app",
35540
+ readOnlyUrl: "https://mcp.linear.app/mcp/readonly",
35541
+ readTools: Object.freeze([
35542
+ "get_attachment",
35543
+ "list_agent_skills",
35544
+ "get_agent_skill",
35545
+ "list_comments",
35546
+ "list_cycles",
35547
+ "list_custom_views",
35548
+ "get_document",
35549
+ "list_documents",
35550
+ "extract_images",
35551
+ "get_issue",
35552
+ "list_issue_statuses",
35553
+ "get_issue_status",
35554
+ "list_issue_labels",
35555
+ "list_projects",
35556
+ "get_project",
35557
+ "list_project_labels",
35558
+ "list_release_pipelines",
35559
+ "list_releases",
35560
+ "get_release",
35561
+ "list_release_notes",
35562
+ "get_release_note",
35563
+ "list_milestones",
35564
+ "get_milestone",
35565
+ "get_notifications",
35566
+ "list_teams",
35567
+ "get_team",
35568
+ "get_triage_responsibility",
35569
+ "list_templates",
35570
+ "get_template",
35571
+ "list_users",
35572
+ "get_user",
35573
+ "get_workspace",
35574
+ "search_documentation",
35575
+ "get_diff",
35576
+ "list_diffs",
35577
+ "get_diff_threads",
35578
+ "list_initiatives",
35579
+ "get_initiative",
35580
+ "list_initiative_labels",
35581
+ "get_status_updates"
35582
+ ]),
35583
+ scopes: Object.freeze(["read"]),
35584
+ issuer: "https://mcp.linear.app",
35585
+ authorizationEndpoint: "https://mcp.linear.app/authorize",
35586
+ tokenEndpoint: "https://mcp.linear.app/token",
35587
+ revocationEndpoint: "https://mcp.linear.app/token",
35588
+ docs: Object.freeze({
35589
+ host: "https://linear.app/docs/mcp",
35590
+ readOnlyUrl: "https://linear.app/docs/mcp",
35591
+ readTools: "https://linear.app/docs/mcp",
35592
+ scopes: "https://linear.app/docs/mcp",
35593
+ issuer: "https://mcp.linear.app/.well-known/oauth-authorization-server",
35594
+ authorizationEndpoint: "https://mcp.linear.app/.well-known/oauth-authorization-server",
35595
+ tokenEndpoint: "https://mcp.linear.app/.well-known/oauth-authorization-server",
35596
+ revocationEndpoint: "https://mcp.linear.app/.well-known/oauth-authorization-server"
35597
+ })
35598
+ });
35599
+ var MCP_CATALOG = Object.freeze({ linear: LINEAR });
35600
+
35208
35601
  // src/grants/manifest.ts
35209
35602
  var TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
35210
35603
  var MANIFEST_KNOWN_TOOLS = Object.freeze({
@@ -35230,9 +35623,9 @@ function wellTyped(v) {
35230
35623
  return false;
35231
35624
  }
35232
35625
  var WRAPPER_KEYS = ["algo", "manifest", "signature"];
35233
- var HEX64 = /^[0-9a-f]{64}$/;
35234
- var PRINTABLE_ASCII = /^[\x21-\x7e]+$/;
35235
- var absolutePath = (v) => typeof v === "string" && v.startsWith("/") && PRINTABLE_ASCII.test(v);
35626
+ var HEX642 = /^[0-9a-f]{64}$/;
35627
+ var PRINTABLE_ASCII2 = /^[\x21-\x7e]+$/;
35628
+ var absolutePath = (v) => typeof v === "string" && v.startsWith("/") && PRINTABLE_ASCII2.test(v);
35236
35629
  var exactKeys = (o, required, optional = []) => required.every((k) => k in o) && Object.keys(o).every((k) => required.includes(k) || optional.includes(k));
35237
35630
  var isStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
35238
35631
  function parseEndpoints(v) {
@@ -35244,7 +35637,7 @@ function parseEndpoints(v) {
35244
35637
  const e = { ...raw };
35245
35638
  if (e.headersFile === "") delete e.headersFile;
35246
35639
  if (!exactKeys(e, ["baseUrl"], ["headersFile"])) return null;
35247
- if (typeof e.baseUrl !== "string" || e.baseUrl === "" || !PRINTABLE_ASCII.test(e.baseUrl)) return null;
35640
+ if (typeof e.baseUrl !== "string" || e.baseUrl === "" || !PRINTABLE_ASCII2.test(e.baseUrl)) return null;
35248
35641
  let url;
35249
35642
  try {
35250
35643
  url = new URL(e.baseUrl);
@@ -35275,8 +35668,8 @@ function parseDatabases(v) {
35275
35668
  function parseConfig(v) {
35276
35669
  if (!isPlainObject(v)) return null;
35277
35670
  if (!exactKeys(v, ["persona_sha256", "flags", "memory", "legacy_assign_sha256"], ["llm", "search", "tools_allow"])) return null;
35278
- if (typeof v.persona_sha256 !== "string" || !HEX64.test(v.persona_sha256)) return null;
35279
- if (typeof v.legacy_assign_sha256 !== "string" || !HEX64.test(v.legacy_assign_sha256)) return null;
35671
+ if (typeof v.persona_sha256 !== "string" || !HEX642.test(v.persona_sha256)) return null;
35672
+ if (typeof v.legacy_assign_sha256 !== "string" || !HEX642.test(v.legacy_assign_sha256)) return null;
35280
35673
  if (!isStringArray(v.flags)) return null;
35281
35674
  if ("tools_allow" in v && !isStringArray(v.tools_allow)) return null;
35282
35675
  if ("llm" in v) {
@@ -35320,7 +35713,7 @@ function verifyToolGrantManifest(raw, ctx) {
35320
35713
  const m = raw.manifest;
35321
35714
  if (!isPlainObject(m)) return { state: "malformed" };
35322
35715
  if (m.type !== "securecoms.tool-grants" || m.v !== 1) return { state: "malformed" };
35323
- if (!wellTyped(m)) return { state: "malformed" };
35716
+ if (!wellTyped(Object.fromEntries(Object.entries(m).filter(([k]) => k !== "mcp")))) return { state: "malformed" };
35324
35717
  if (typeof m.agent_identity_id !== "string" || typeof m.owner_identity_id !== "string") return { state: "malformed" };
35325
35718
  for (const k of ["owner_dm_channel_id", "owner_dm_relay_identity_id"]) {
35326
35719
  if (k in m && typeof m[k] !== "string") return { state: "malformed" };
@@ -35350,6 +35743,11 @@ function verifyToolGrantManifest(raw, ctx) {
35350
35743
  if (counter === ctx.storedCounter) return { state: "unchanged" };
35351
35744
  if (counter < ctx.storedCounter) return { state: "rollback" };
35352
35745
  }
35746
+ const rawOwnerDm = m.grants.owner_dm;
35747
+ const signedMcpPresent = Array.isArray(rawOwnerDm) && rawOwnerDm.some((t) => t.startsWith("mcp:"));
35748
+ const mcpKey = "mcp" in m ? "present" : "absent";
35749
+ const mcp = mcpKey === "present" ? parseMcp(m.mcp) : null;
35750
+ const catalog = ctx.mcpCatalog ?? MCP_CATALOG;
35353
35751
  let droppedUnknown = false;
35354
35752
  const grants = {};
35355
35753
  for (const [audience, list] of Object.entries(m.grants)) {
@@ -35358,7 +35756,7 @@ function verifyToolGrantManifest(raw, ctx) {
35358
35756
  continue;
35359
35757
  }
35360
35758
  const known = MANIFEST_KNOWN_TOOLS[audience];
35361
- const kept = list.filter((t) => known.includes(t));
35759
+ const kept = list.filter((t) => known.includes(t) || audience === "owner_dm" && mcpGrantWithinCeiling(t, mcp, catalog));
35362
35760
  if (kept.length !== list.length) droppedUnknown = true;
35363
35761
  grants[audience] = Object.freeze(kept);
35364
35762
  }
@@ -35373,7 +35771,10 @@ function verifyToolGrantManifest(raw, ctx) {
35373
35771
  grants: Object.freeze(grants),
35374
35772
  endpoints: Object.freeze(endpoints),
35375
35773
  databases: Object.freeze(databases),
35376
- config
35774
+ config,
35775
+ mcp,
35776
+ mcpKey,
35777
+ signedMcpPresent
35377
35778
  });
35378
35779
  return { state: "ok", manifest, droppedUnknown, wrapper: raw };
35379
35780
  }
@@ -35694,21 +36095,32 @@ var AGENT_TOTAL_KEY = "agent-total";
35694
36095
  // src/grants/effective.ts
35695
36096
  var TOOL_GRANT_CODE_CEILING = MANIFEST_KNOWN_TOOLS;
35696
36097
  var CURRENT_TIME = Object.freeze({ tool: "current_time", mode: "read" });
36098
+ var NO_WITHHELD = Object.freeze([]);
35697
36099
  function effectiveToolGrants(i) {
35698
- if (i.base.length === 0) return { grants: i.base, source: "none", withheldBySearchCap: false };
35699
- if (i.manifest === null) return { grants: i.base, source: "constant", withheldBySearchCap: false };
36100
+ if (i.base.length === 0) return { grants: i.base, source: "none", withheldBySearchCap: false, withheld: NO_WITHHELD };
36101
+ if (i.manifest === null) return { grants: i.base, source: "constant", withheldBySearchCap: false, withheld: NO_WITHHELD };
35700
36102
  const ceiling = TOOL_GRANT_CODE_CEILING[i.audience];
35701
36103
  const listed = new Set(i.manifest.grants[i.audience] ?? []);
35702
36104
  const signed = ceiling.filter((t) => listed.has(t));
35703
36105
  if (!i.eligible || !i.manifestSwitchOn) {
35704
36106
  const fallback = i.base.filter((g) => g.tool !== "current_time" && signed.includes(g.tool));
35705
- return { grants: Object.freeze([CURRENT_TIME, ...fallback]), source: "fallback", withheldBySearchCap: false };
36107
+ return { grants: Object.freeze([CURRENT_TIME, ...fallback]), source: "fallback", withheldBySearchCap: false, withheld: NO_WITHHELD };
35706
36108
  }
35707
36109
  let withheld = false;
36110
+ const dropped = [];
35708
36111
  const tools = signed.filter((t) => {
35709
- if (i.disabled.has(t)) return false;
35710
- if (t === "http_request" && i.signedTargets.endpoints === 0) return false;
35711
- if (t === "sql_query" && i.signedTargets.databases === 0) return false;
36112
+ if (i.disabled.has(t)) {
36113
+ dropped.push(Object.freeze({ tool: t, why: "disabled_by_owner" }));
36114
+ return false;
36115
+ }
36116
+ if (t === "http_request" && i.signedTargets.endpoints === 0) {
36117
+ dropped.push(Object.freeze({ tool: t, why: "no_endpoint" }));
36118
+ return false;
36119
+ }
36120
+ if (t === "sql_query" && i.signedTargets.databases === 0) {
36121
+ dropped.push(Object.freeze({ tool: t, why: "no_database" }));
36122
+ return false;
36123
+ }
35712
36124
  if (i.audience === "team_room" && t === "web_search" && i.roomSearchCapped) {
35713
36125
  withheld = true;
35714
36126
  return false;
@@ -35718,7 +36130,8 @@ function effectiveToolGrants(i) {
35718
36130
  return {
35719
36131
  grants: Object.freeze([CURRENT_TIME, ...tools.map((tool) => Object.freeze({ tool, mode: "read", source: "manifest" }))]),
35720
36132
  source: "manifest",
35721
- withheldBySearchCap: withheld
36133
+ withheldBySearchCap: withheld,
36134
+ withheld: Object.freeze(dropped)
35722
36135
  };
35723
36136
  }
35724
36137
 
@@ -36023,6 +36436,200 @@ function classifyVoiceFrame(f) {
36023
36436
  return b.text.trim() === VOICE_CALL_START_MARKER ? "greeting" : "drop";
36024
36437
  }
36025
36438
 
36439
+ // src/grants/tool-status.ts
36440
+ var TOOL_STATUS_ALLOWLIST = [
36441
+ "current_time",
36442
+ "web_search",
36443
+ "web_extract",
36444
+ "fetch_url",
36445
+ "http_request",
36446
+ "sql_query",
36447
+ "weather_forecast",
36448
+ "currency_convert"
36449
+ ];
36450
+ var ALLOW2 = new Set(TOOL_STATUS_ALLOWLIST);
36451
+ var TOOL_STATUS_BUCKET_MS = 9e5;
36452
+ function bucketStartAt(ms) {
36453
+ return new Date(Math.floor(ms / TOOL_STATUS_BUCKET_MS) * TOOL_STATUS_BUCKET_MS).toISOString().slice(0, 16) + "Z";
36454
+ }
36455
+ function bucketEndMs(at) {
36456
+ return Date.parse(at) + TOOL_STATUS_BUCKET_MS;
36457
+ }
36458
+ var SIGNATURE_FAILURES = /* @__PURE__ */ new Set(["bad_signature", "malformed", "unknown_algo", "no_signature"]);
36459
+ function ownerDmCause(i) {
36460
+ try {
36461
+ const v = i.verdict;
36462
+ if (v.kind === "failed" && v.reason === "replay") return null;
36463
+ if (v.kind !== "verified" && v.kind !== "constants_only" && v.kind !== "failed") return null;
36464
+ const failedReason = v.kind === "failed" ? v.reason : void 0;
36465
+ const blocked = i.killSwitchOff || i.pinMissingOrAnomalous || i.senderNotOwner || i.stale || v.kind === "failed";
36466
+ const eligible = v.kind === "verified" && i.signedDmChannel === i.channelId && i.boundNow && i.consumerAttached;
36467
+ const source = blocked ? "none" : !i.manifestPresent ? "constant" : !eligible || i.manifestSwitchOff ? "fallback" : "manifest";
36468
+ const at = (cause, sub) => sub === void 0 ? { source, cause } : { source, cause, sub };
36469
+ if (i.killSwitchOff) return at("owner_dm_unavailable", "kill_switch");
36470
+ if (i.manifestSwitchOff) return at("manifest_switch_off");
36471
+ if (i.pinMissingOrAnomalous) return at("owner_dm_unavailable", "pin");
36472
+ if (i.senderNotOwner) return at("owner_dm_unavailable", "sender_not_owner");
36473
+ if (i.stale) return at("owner_dm_unavailable", "stale");
36474
+ if (failedReason === "not_owner_dm_leaves") return at("unexpected_member_in_dm");
36475
+ if (failedReason !== void 0 && SIGNATURE_FAILURES.has(failedReason)) return at("owner_turn_failed");
36476
+ if (failedReason === "verify_error") return at("agent_state_error");
36477
+ if (failedReason === "wrong_dm_channel" || v.kind === "verified" && i.manifestPresent && i.signedDmChannel !== void 0 && i.signedDmChannel !== i.channelId) {
36478
+ return at("dm_channel_mismatch");
36479
+ }
36480
+ if (failedReason !== void 0) return null;
36481
+ if (i.manifestPresent && i.signedDmChannel === void 0) return at("dm_not_linked");
36482
+ if (v.kind === "constants_only") return at("unsigned_relay_in_dm");
36483
+ if (!i.manifestPresent || !i.boundNow) return at("manifest_not_bound");
36484
+ if (!i.consumerAttached) return at("no_runtime_consumer");
36485
+ return source === "manifest" ? { source } : null;
36486
+ } catch {
36487
+ return null;
36488
+ }
36489
+ }
36490
+ function sameTuple(a, b) {
36491
+ if (!a || !b) return false;
36492
+ return a.source === b.source && a.cause === b.cause && a.sub === b.sub && a.relay === b.relay && a.mcp_issued === b.mcp_issued && a.counter === b.counter && a.issued.length === b.issued.length && a.issued.every((t, k) => t === b.issued[k]) && a.withheld.length === b.withheld.length && a.withheld.every((w, k) => w.tool === b.withheld[k].tool && w.why === b.withheld[k].why);
36493
+ }
36494
+ function sanitizeIssued(names) {
36495
+ return [...new Set(names.filter((n) => ALLOW2.has(n)))].sort();
36496
+ }
36497
+
36498
+ // src/tool-status-sender.ts
36499
+ var TOOL_STATUS_JITTER_MS = 6e4;
36500
+ var RETRY_AFTER_FLOOR_MS = 6e4;
36501
+ function defaultToolStatusSetTimer(fn, ms) {
36502
+ const t = setTimeout(fn, ms);
36503
+ t.unref?.();
36504
+ return t;
36505
+ }
36506
+ function defaultToolStatusClearTimer(h) {
36507
+ clearTimeout(h);
36508
+ }
36509
+ function retryAfterMs(raw) {
36510
+ if (typeof raw !== "string" || !/^\d{1,9}$/.test(raw.trim())) return RETRY_AFTER_FLOOR_MS;
36511
+ return Math.max(RETRY_AFTER_FLOOR_MS, Number(raw.trim()) * 1e3);
36512
+ }
36513
+ var ToolStatusSender = class {
36514
+ postFn;
36515
+ now;
36516
+ setTimer;
36517
+ clearTimer;
36518
+ random;
36519
+ onFailure;
36520
+ /** The latest noted record while its bucket is still open. */
36521
+ open = null;
36522
+ /** The latest noted record whose bucket has closed: the only record an attempt or carry() may send. */
36523
+ closed = null;
36524
+ lastPosted = null;
36525
+ terminalFor = null;
36526
+ notBefore = Number.NEGATIVE_INFINITY;
36527
+ timer = null;
36528
+ inFlight = false;
36529
+ shut = false;
36530
+ constructor(o) {
36531
+ this.postFn = o.post;
36532
+ this.now = o.now ?? (() => Date.now());
36533
+ this.setTimer = o.setTimer ?? defaultToolStatusSetTimer;
36534
+ this.clearTimer = o.clearTimer ?? defaultToolStatusClearTimer;
36535
+ this.random = o.random ?? Math.random;
36536
+ this.onFailure = o.onFailure ?? (() => {
36537
+ });
36538
+ }
36539
+ note(rec) {
36540
+ if (this.shut) return;
36541
+ try {
36542
+ this.promote(this.now());
36543
+ this.open = rec;
36544
+ this.schedule();
36545
+ } catch {
36546
+ }
36547
+ }
36548
+ /** Ruling M-2: what the apply-time report carries. The closed record only; an open bucket is never revealed early. */
36549
+ carry() {
36550
+ try {
36551
+ this.promote(this.now());
36552
+ } catch {
36553
+ }
36554
+ return this.closed;
36555
+ }
36556
+ /** R2-M1: the record belongs to an owner who is no longer the manifest's; forget it and what was posted. */
36557
+ drop() {
36558
+ this.open = null;
36559
+ this.closed = null;
36560
+ this.lastPosted = null;
36561
+ this.terminalFor = null;
36562
+ this.cancelTimer();
36563
+ }
36564
+ close() {
36565
+ this.shut = true;
36566
+ this.cancelTimer();
36567
+ }
36568
+ cancelTimer() {
36569
+ if (this.timer !== null) this.clearTimer(this.timer);
36570
+ this.timer = null;
36571
+ }
36572
+ promote(t) {
36573
+ if (this.open !== null && bucketEndMs(this.open.at) <= t) {
36574
+ this.closed = this.open;
36575
+ this.open = null;
36576
+ }
36577
+ }
36578
+ pending() {
36579
+ return this.closed !== null && !sameTuple(this.closed, this.lastPosted) && !sameTuple(this.closed, this.terminalFor);
36580
+ }
36581
+ schedule() {
36582
+ if (this.shut || this.inFlight || this.timer !== null) return;
36583
+ if (this.open === null && !this.pending()) return;
36584
+ const t = this.now();
36585
+ let b = (Math.floor(t / TOOL_STATUS_BUCKET_MS) + 1) * TOOL_STATUS_BUCKET_MS;
36586
+ while (b < this.notBefore) b += TOOL_STATUS_BUCKET_MS;
36587
+ const jitter = Math.floor(this.random() * TOOL_STATUS_JITTER_MS);
36588
+ this.timer = this.setTimer(() => {
36589
+ this.timer = null;
36590
+ this.fire();
36591
+ }, b - t + jitter);
36592
+ }
36593
+ fire() {
36594
+ try {
36595
+ if (this.shut) return;
36596
+ this.promote(this.now());
36597
+ if (this.pending()) {
36598
+ void this.attempt(this.closed);
36599
+ return;
36600
+ }
36601
+ this.schedule();
36602
+ } catch {
36603
+ }
36604
+ }
36605
+ async attempt(rec) {
36606
+ this.inFlight = true;
36607
+ const attemptAt = this.now();
36608
+ let result;
36609
+ try {
36610
+ result = await this.postFn(rec);
36611
+ } catch {
36612
+ result = "network";
36613
+ }
36614
+ this.inFlight = false;
36615
+ try {
36616
+ if (typeof result === "object" && result.status >= 200 && result.status < 300) {
36617
+ this.lastPosted = rec;
36618
+ } else if (typeof result === "object" && result.status === 429) {
36619
+ this.notBefore = attemptAt + retryAfterMs(result.retryAfter);
36620
+ this.onFailure("http_error");
36621
+ } else if (typeof result === "object" && result.status >= 400 && result.status < 500) {
36622
+ this.terminalFor = rec;
36623
+ this.onFailure("http_error");
36624
+ } else {
36625
+ this.onFailure(typeof result === "object" ? "http_error" : result);
36626
+ }
36627
+ this.schedule();
36628
+ } catch {
36629
+ }
36630
+ }
36631
+ };
36632
+
36026
36633
  // src/grants/owner-turn.ts
36027
36634
  import { ed25519 as ed255196 } from "@noble/curves/ed25519";
36028
36635
  var HEX = /^(?:[0-9a-fA-F]{2})+$/;
@@ -36066,6 +36673,310 @@ function classifyOwnerDmLeaves(leaves, allowed) {
36066
36673
  }
36067
36674
  return "foreign";
36068
36675
  }
36676
+ function ownerDmLeavesRelayFree(leaves, ids) {
36677
+ let owner = false;
36678
+ let agent = false;
36679
+ for (const l of leaves) {
36680
+ if (!l.basic || l.identity === null) return false;
36681
+ if (l.identity === ids.ownerIdentityId) owner = true;
36682
+ else if (l.identity === ids.agentIdentityId) agent = true;
36683
+ else return false;
36684
+ }
36685
+ return owner && agent;
36686
+ }
36687
+ function relayLeafFromPhase0Relay(relay) {
36688
+ return relay !== "none";
36689
+ }
36690
+ function ownerDmRelayOf(leafClass, leaves, ids) {
36691
+ if (leafClass === "unsigned_relay") return "unsigned";
36692
+ if (ownerDmLeavesRelayFree(leaves, ids)) return "none";
36693
+ if (ids.relayIdentityId !== void 0 && leaves.some((l) => l.identity === ids.relayIdentityId)) return "signed";
36694
+ return void 0;
36695
+ }
36696
+
36697
+ // src/grants/mcp-policy.ts
36698
+ var MCP_POLICY_WRITER_FEATURE = "mcp_policy_writer_v1";
36699
+ function mcpEntitlementReading(mcpEntitled, mcpCause) {
36700
+ if (mcpEntitled === true && mcpCause === "entitled") return "entitled";
36701
+ if (mcpEntitled === false && mcpCause === "not_entitled") return "not_entitled";
36702
+ return "suspended";
36703
+ }
36704
+ function mcpGrantsHostSwitch(env, log = writeServiceLog2) {
36705
+ const v = env.SECURECOMS_MCP_GRANTS;
36706
+ if (v === void 0 || v === "on") return "on";
36707
+ if (v === "off") return "off";
36708
+ log("warn", "config.mcp_grants_switch_suspended", { state: ServiceLogEnum.of("suspended", ["suspended"]) });
36709
+ return "suspended";
36710
+ }
36711
+ var empty = (state, r3Hold) => Object.freeze({ v: 1, state, r3_hold: r3Hold, servers: Object.freeze({}), remove: NO_REMOVE });
36712
+ var NO_REMOVE = Object.freeze([]);
36713
+ function policyAuth(s) {
36714
+ if (s.auth.mode === "key") return Object.freeze({ mode: "key" });
36715
+ const clientId = s.auth.client === "customer" ? s.auth.client_id : SECURECOMS_CIMD_CLIENT_ID;
36716
+ return Object.freeze({ mode: "oauth", issuer: s.auth.issuer, client: s.auth.client, client_id: clientId });
36717
+ }
36718
+ function mcpPolicyFor(i) {
36719
+ const m = i.manifest;
36720
+ const r3Hold = m?.signedMcpPresent === true || i.mcpSeen;
36721
+ if (i.hostSwitch === "off") return empty("kill_switch", r3Hold);
36722
+ if (i.hostSwitch !== "on") return empty("suspended", r3Hold);
36723
+ if (!i.config.fetched) return empty("no_config", r3Hold);
36724
+ const ent = mcpEntitlementReading(i.config.mcp_entitled, i.config.mcp_cause);
36725
+ if (ent !== "entitled") return empty(ent, r3Hold);
36726
+ if (!i.ownerDmManifestSwitchOn) return empty("switch_off", r3Hold);
36727
+ if (m === null) return empty("no_manifest", r3Hold);
36728
+ if (m.mcpKey === "absent") return empty("section_absent", r3Hold);
36729
+ if (m.mcp === null) return empty("malformed", r3Hold);
36730
+ if (!i.capability) return empty("no_capability", r3Hold);
36731
+ const catalog = i.catalog ?? MCP_CATALOG;
36732
+ const granted = /* @__PURE__ */ new Map();
36733
+ for (const name of m.grants.owner_dm ?? []) {
36734
+ const g = parseMcpGrant(name);
36735
+ if (g === null) continue;
36736
+ if (!granted.has(g.serverId)) granted.set(g.serverId, /* @__PURE__ */ new Set());
36737
+ granted.get(g.serverId).add(g.tool);
36738
+ }
36739
+ const servers = {};
36740
+ for (const id of Object.keys(m.mcp.servers).sort()) {
36741
+ const s = m.mcp.servers[id];
36742
+ const row = mcpServerCatalogRow(s, catalog);
36743
+ if (row === null) {
36744
+ servers[id] = Object.freeze({ state: "refused" });
36745
+ continue;
36746
+ }
36747
+ const tools = {};
36748
+ for (const t of Object.keys(s.tools).sort()) {
36749
+ if (granted.get(id)?.has(t) === true && row.readTools.includes(t)) tools[t] = Object.freeze({ sha256: s.tools[t].sha256, class: "read" });
36750
+ }
36751
+ servers[id] = Object.freeze({ state: "ok", vendor: s.vendor, url: row.readOnlyUrl, auth: policyAuth(s), tools: Object.freeze(tools) });
36752
+ }
36753
+ const remove = m.mcp.removed === "*" ? "*" : Object.freeze([...m.mcp.removed]);
36754
+ return Object.freeze({ v: 1, state: "ok", r3_hold: r3Hold, servers: Object.freeze(servers), remove });
36755
+ }
36756
+
36757
+ // src/grants/mcp-issuance.ts
36758
+ var NONE = Object.freeze([]);
36759
+ var own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
36760
+ function mcpPolicyAllows(policy, serverId, tool) {
36761
+ if (policy === null || policy.state !== "ok" || !own(policy.servers, serverId)) return false;
36762
+ const s = policy.servers[serverId];
36763
+ return s.state === "ok" && own(s.tools, tool);
36764
+ }
36765
+ function mcpIssuance(i) {
36766
+ if (i.source !== "manifest") return NONE;
36767
+ if (i.verdict?.kind !== "verified" || i.verdict.relayLeaf !== false) return NONE;
36768
+ if (i.entitlement !== "entitled" || i.hostSwitch !== "on" || !i.ownerDmManifestSwitchOn) return NONE;
36769
+ if (i.manifest === null || i.policy === null || i.policy.state !== "ok") return NONE;
36770
+ const out = [];
36771
+ for (const name of i.manifest.grants.owner_dm ?? []) {
36772
+ const g = parseMcpGrant(name);
36773
+ if (g === null || i.disabled.has(name) || !mcpPolicyAllows(i.policy, g.serverId, g.tool)) continue;
36774
+ out.push(Object.freeze({ tool: name, mode: "read", source: "manifest" }));
36775
+ }
36776
+ return out.length === 0 ? NONE : Object.freeze(out);
36777
+ }
36778
+
36779
+ // src/consumer-grants.ts
36780
+ var NO_GRANTS = Object.freeze([]);
36781
+ var PER_TOOL_GRANTS_FEATURE = "per_tool_grants";
36782
+ function withConsumerGrants(m, perToolConsumer) {
36783
+ if (perToolConsumer) return m;
36784
+ const a = m.authorization;
36785
+ if (!a) return m;
36786
+ const stripGrants = !(a.tool_grants.length === 0 || a.audience === "owner_dm" || a.relay_speaker === "owner");
36787
+ const stripMemory = "memory_write" in a;
36788
+ if (!stripGrants && !stripMemory) return m;
36789
+ const { memory_write: _dropped, ...rest } = a;
36790
+ return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
36791
+ }
36792
+ var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
36793
+ var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
36794
+ var UTILITY_TOOLS_FEATURE = "utility_tools_v1";
36795
+ var MCP_CLIENT_FEATURE = "mcp_client_v1";
36796
+ var CONSTANT_TOOL_NAMES = Object.freeze({
36797
+ owner_dm: Object.freeze(["current_time", "web_search"]),
36798
+ team_room: Object.freeze(["current_time"])
36799
+ });
36800
+ var isManifestGrant = (g) => g.mode === "read" && g.source === "manifest";
36801
+ function withManifestCapability(m, manifestConsumer) {
36802
+ if (manifestConsumer) return m;
36803
+ const a = m.authorization;
36804
+ if (!a || !a.tool_grants.some(isManifestGrant)) return m;
36805
+ const constants5 = new Set(a.audience === "owner_dm" || a.audience === "team_room" ? CONSTANT_TOOL_NAMES[a.audience] : []);
36806
+ const narrowed = a.tool_grants.flatMap((g) => {
36807
+ if (!isManifestGrant(g)) return [g];
36808
+ return constants5.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
36809
+ });
36810
+ return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(narrowed) }) };
36811
+ }
36812
+ function withHarnessGrants(m, withheld) {
36813
+ if (!withheld) return m;
36814
+ const a = m.authorization;
36815
+ if (!a || a.tool_grants.length === 0) return m;
36816
+ return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
36817
+ }
36818
+ function withSearchCapability(m, consumerHasFeature, signedProvider) {
36819
+ if (consumerHasFeature) return m;
36820
+ const a = m.authorization;
36821
+ if (!a) return m;
36822
+ const stripSearch = signedProvider !== void 0 && signedProvider !== "brave";
36823
+ const kept = a.tool_grants.filter((g) => g.tool !== "web_extract" && !(stripSearch && g.tool === "web_search"));
36824
+ if (kept.length === a.tool_grants.length) return m;
36825
+ return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
36826
+ }
36827
+ var UTILITY_TOOL_NAMES = /* @__PURE__ */ new Set(["weather_forecast", "currency_convert"]);
36828
+ function withUtilityCapability(m, consumerHasFeature) {
36829
+ if (consumerHasFeature) return m;
36830
+ const a = m.authorization;
36831
+ if (!a) return m;
36832
+ const kept = a.tool_grants.filter((g) => !UTILITY_TOOL_NAMES.has(g.tool));
36833
+ if (kept.length === a.tool_grants.length) return m;
36834
+ return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
36835
+ }
36836
+ var MCP_GRANT_PREFIX = "mcp:";
36837
+ var isMcpGrant = (g) => g.tool.startsWith(MCP_GRANT_PREFIX);
36838
+ function withMcpCapability(m, consumerHasFeature) {
36839
+ const a = m.authorization;
36840
+ if (!a) return m;
36841
+ const { relay_free: _relayFree, ...rest } = a;
36842
+ if (!a.tool_grants.some(isMcpGrant)) return "relay_free" in a ? { ...m, authorization: Object.freeze(rest) } : m;
36843
+ if (consumerHasFeature && a.audience === "owner_dm" && a.relay_speaker === void 0) return m;
36844
+ const kept = a.tool_grants.filter((g) => !isMcpGrant(g));
36845
+ return { ...m, authorization: Object.freeze({ ...rest, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
36846
+ }
36847
+ var R3_KEEP_TOOLS = /* @__PURE__ */ new Set(["current_time", "web_search", "weather_forecast", "currency_convert"]);
36848
+ function withR3Holds(m, hold) {
36849
+ if (!hold) return m;
36850
+ const a = m.authorization;
36851
+ if (!a || !(a.audience === "owner_dm" || a.relay_speaker === "owner")) return m;
36852
+ const kept = a.tool_grants.filter((g) => R3_KEEP_TOOLS.has(g.tool) || isMcpGrant(g));
36853
+ if (kept.length === a.tool_grants.length) return m;
36854
+ return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
36855
+ }
36856
+
36857
+ // src/mcp-catalog-message.ts
36858
+ var MCP_CATALOG_MAX_BYTES = 384 * 1024;
36859
+ var MCP_CATALOG_MAX_TOOLS = 64;
36860
+ var MCP_CATALOG_LIST_TIMEOUT_MS = 5e4;
36861
+ var REQUEST_ID = /^[0-9a-f]{32}$/;
36862
+ var SERVER_ID = /^[a-z0-9-]{1,24}$/;
36863
+ var TOOL_NAME = /^[A-Za-z0-9_.-]{1,64}$/;
36864
+ var RESERVED = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
36865
+ var ISO_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
36866
+ var MCP_CATALOG_REASONS = /* @__PURE__ */ new Set(["catalog", "write_name", "hint", "schema", "header_param", "too_large"]);
36867
+ var isBag2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
36868
+ var keysExactly2 = (o, keys) => {
36869
+ const k = Object.keys(o);
36870
+ return k.length === keys.length && keys.every((x) => Object.prototype.hasOwnProperty.call(o, x));
36871
+ };
36872
+ function parseMcpCatalogRequest(body) {
36873
+ if (!isBag2(body) || !keysExactly2(body, ["v", "type", "server_id", "request_id"])) return null;
36874
+ if (body.v !== 1 || body.type !== "mcp_catalog_request") return null;
36875
+ const { server_id: serverId, request_id: requestId } = body;
36876
+ if (typeof serverId !== "string" || !SERVER_ID.test(serverId) || RESERVED.has(serverId)) return null;
36877
+ if (typeof requestId !== "string" || !REQUEST_ID.test(requestId)) return null;
36878
+ return Object.freeze({ serverId, requestId });
36879
+ }
36880
+ var PROJECTION_KEYS = /* @__PURE__ */ new Set(["name", "title", "description", "inputSchema", "annotations"]);
36881
+ function validProjection(p, toolName) {
36882
+ if (!Object.keys(p).every((k) => PROJECTION_KEYS.has(k))) return false;
36883
+ if (p.name !== toolName || typeof p.description !== "string" || !isBag2(p.inputSchema)) return false;
36884
+ if (Object.prototype.hasOwnProperty.call(p, "title") && typeof p.title !== "string") return false;
36885
+ if (Object.prototype.hasOwnProperty.call(p, "annotations") && !isBag2(p.annotations)) return false;
36886
+ return true;
36887
+ }
36888
+ function validTool(t) {
36889
+ if (!isBag2(t) || typeof t.name !== "string" || typeof t.grantable !== "boolean") return null;
36890
+ if (t.grantable) {
36891
+ if (!TOOL_NAME.test(t.name) || t.reason !== null || !isBag2(t.projection) || !validProjection(t.projection, t.name)) return null;
36892
+ return { name: t.name, projection: t.projection, grantable: true, reason: null };
36893
+ }
36894
+ if (t.projection !== void 0 || typeof t.reason !== "string" || !MCP_CATALOG_REASONS.has(t.reason)) return null;
36895
+ if (t.name.length < 1 || t.name.length > 64 || !/^[\x20-\x7e]+$/.test(t.name)) return null;
36896
+ return { name: t.name, grantable: false, reason: t.reason };
36897
+ }
36898
+ function validateCatalogListReply(frame, serverId, vendor) {
36899
+ if (!isBag2(frame) || frame.ok !== true || frame.server_id !== serverId || frame.vendor !== vendor) return null;
36900
+ if (typeof frame.listed_at !== "string" || !ISO_TIME.test(frame.listed_at) || typeof frame.truncated !== "boolean") return null;
36901
+ if (!Array.isArray(frame.tools) || frame.tools.length > MCP_CATALOG_MAX_TOOLS) return null;
36902
+ const tools = [];
36903
+ const names = /* @__PURE__ */ new Set();
36904
+ for (const t of frame.tools) {
36905
+ const v = validTool(t);
36906
+ if (v === null) return null;
36907
+ if (names.has(v.name)) return null;
36908
+ names.add(v.name);
36909
+ tools.push(v);
36910
+ }
36911
+ return { listedAt: frame.listed_at, truncated: frame.truncated, tools };
36912
+ }
36913
+ var utf8 = (v) => Buffer.byteLength(JSON.stringify(v), "utf8");
36914
+ var mcpCatalogBodyBytes = (b) => utf8(b);
36915
+ function buildMcpCatalogBody(i) {
36916
+ const head = { v: 1, type: "mcp_catalog", server_id: i.serverId, request_id: i.requestId, vendor: i.vendor, issuer: i.issuer, listed_at: i.reply.listedAt };
36917
+ const full = { ...head, tools: [...i.reply.tools], truncated: i.reply.truncated };
36918
+ if (mcpCatalogBodyBytes(full) <= MCP_CATALOG_MAX_BYTES) return full;
36919
+ const stub = (t) => ({ name: t.name, grantable: false, reason: "too_large" });
36920
+ const tools = i.reply.tools.map(stub);
36921
+ const body = { ...head, tools, truncated: true };
36922
+ let size = mcpCatalogBodyBytes(body);
36923
+ for (let k = 0; k < tools.length; k++) {
36924
+ const want = i.reply.tools[k];
36925
+ const delta = utf8(want) - utf8(tools[k]);
36926
+ if (size + delta <= MCP_CATALOG_MAX_BYTES) {
36927
+ tools[k] = want;
36928
+ size += delta;
36929
+ }
36930
+ }
36931
+ return body;
36932
+ }
36933
+
36934
+ // src/mcp-catalog-rate.ts
36935
+ var MINUTE_MS = 6e4;
36936
+ var HOUR_MS = 60 * MINUTE_MS;
36937
+ var MCP_CATALOG_PER_SERVER_MINUTE = 3;
36938
+ var MCP_CATALOG_PER_SERVER_HOUR = 20;
36939
+ var MCP_CATALOG_PER_AGENT_HOUR = 40;
36940
+ var McpCatalogRateLimiter = class {
36941
+ now;
36942
+ onRateLimited;
36943
+ inFlight = /* @__PURE__ */ new Set();
36944
+ perServer = /* @__PURE__ */ new Map();
36945
+ agent = [];
36946
+ /** Per scope key: no further log until this time (the end of the window that was full when it last logged). */
36947
+ quietUntil = /* @__PURE__ */ new Map();
36948
+ constructor(deps = {}) {
36949
+ this.now = deps.now ?? Date.now;
36950
+ this.onRateLimited = deps.onRateLimited ?? (() => {
36951
+ });
36952
+ }
36953
+ /** "ok" admits (and counts) the request and marks the server in flight; the caller MUST call release(serverId). */
36954
+ admit(serverId) {
36955
+ if (this.inFlight.has(serverId)) return "in_flight";
36956
+ const t = this.now();
36957
+ const server = (this.perServer.get(serverId) ?? []).filter((x) => t - x < HOUR_MS);
36958
+ this.perServer.set(serverId, server);
36959
+ this.agent = this.agent.filter((x) => t - x < HOUR_MS);
36960
+ const lastMinute = server.filter((x) => t - x < MINUTE_MS);
36961
+ if (lastMinute.length >= MCP_CATALOG_PER_SERVER_MINUTE) return this.limited(`m:${serverId}`, "server_minute", lastMinute[0] + MINUTE_MS, t);
36962
+ if (server.length >= MCP_CATALOG_PER_SERVER_HOUR) return this.limited(`h:${serverId}`, "server_hour", server[0] + HOUR_MS, t);
36963
+ if (this.agent.length >= MCP_CATALOG_PER_AGENT_HOUR) return this.limited("a", "agent_hour", this.agent[0] + HOUR_MS, t);
36964
+ server.push(t);
36965
+ this.agent.push(t);
36966
+ this.inFlight.add(serverId);
36967
+ return "ok";
36968
+ }
36969
+ release(serverId) {
36970
+ this.inFlight.delete(serverId);
36971
+ }
36972
+ limited(key, scope, windowEnd, t) {
36973
+ if (t >= (this.quietUntil.get(key) ?? -Infinity)) {
36974
+ this.quietUntil.set(key, windowEnd);
36975
+ this.onRateLimited(scope);
36976
+ }
36977
+ return "rate_limited";
36978
+ }
36979
+ };
36069
36980
 
36070
36981
  // src/bridge-agent.ts
36071
36982
  function logSendSupersededAdoption(kind2) {
@@ -36205,6 +37116,7 @@ function selectVoiceGuidance(isDm, voiceDefaultResponder) {
36205
37116
  }
36206
37117
  return VOICE_MODE_GUIDANCE;
36207
37118
  }
37119
+ var MCP_INERT_STATES = /* @__PURE__ */ new Set(["suspended", "no_config", "switch_off", "no_manifest", "section_absent", "malformed", "no_capability"]);
36208
37120
  var VOICE_COALESCE_WINDOW_MS = 0;
36209
37121
  var VOICE_COALESCE_WINDOW_MAX_MS = 5e3;
36210
37122
  function voiceCoalesceWindowFromEnv(env = process.env) {
@@ -36217,8 +37129,8 @@ function voiceWindowEligible(t) {
36217
37129
  }
36218
37130
  function waitMs(ms) {
36219
37131
  if (ms <= 0) return Promise.resolve();
36220
- return new Promise((resolve) => {
36221
- const t = setTimeout(resolve, ms);
37132
+ return new Promise((resolve2) => {
37133
+ const t = setTimeout(resolve2, ms);
36222
37134
  t.unref?.();
36223
37135
  });
36224
37136
  }
@@ -36255,10 +37167,10 @@ var ChannelPendingError = class extends Error {
36255
37167
  }
36256
37168
  };
36257
37169
  var RateLimitedError = class extends Error {
36258
- constructor(channelId, retryAfterMs) {
36259
- super(`channel ${channelId} send rate limit exceeded (retry in ${retryAfterMs}ms)`);
37170
+ constructor(channelId, retryAfterMs2) {
37171
+ super(`channel ${channelId} send rate limit exceeded (retry in ${retryAfterMs2}ms)`);
36260
37172
  this.channelId = channelId;
36261
- this.retryAfterMs = retryAfterMs;
37173
+ this.retryAfterMs = retryAfterMs2;
36262
37174
  this.name = "RateLimitedError";
36263
37175
  }
36264
37176
  };
@@ -36355,7 +37267,7 @@ var FileOwnerAuthorityStore = class {
36355
37267
  }
36356
37268
  async readAll() {
36357
37269
  try {
36358
- return JSON.parse(await fsp.readFile(this.path, "utf8"));
37270
+ return JSON.parse(await fsp2.readFile(this.path, "utf8"));
36359
37271
  } catch (e) {
36360
37272
  if (e.code === "ENOENT") return {};
36361
37273
  throw e;
@@ -36368,8 +37280,8 @@ var FileOwnerAuthorityStore = class {
36368
37280
  async save(enclaveId, pin) {
36369
37281
  const all = await this.readAll();
36370
37282
  all[enclaveId] = pin;
36371
- await fsp.mkdir(dirname5(this.path), { recursive: true });
36372
- await fsp.writeFile(this.path, JSON.stringify(all), "utf8");
37283
+ await fsp2.mkdir(dirname6(this.path), { recursive: true });
37284
+ await fsp2.writeFile(this.path, JSON.stringify(all), "utf8");
36373
37285
  }
36374
37286
  };
36375
37287
  var AgentPausedSendError = class extends Error {
@@ -36386,6 +37298,31 @@ var SendForbiddenError = class extends Error {
36386
37298
  this.name = "SendForbiddenError";
36387
37299
  }
36388
37300
  };
37301
+ var MCP_POLICY_RETRY_DELAYS_MS = Object.freeze([1e3, 5e3, 3e4, 12e4, 3e5]);
37302
+ function logMcpCatalogRequest(state) {
37303
+ writeServiceLog2(state === "sent" ? "info" : "warn", "mcp.catalog_request", { state: ServiceLogEnum.of(state, [
37304
+ "malformed",
37305
+ "not_owner_dm",
37306
+ "not_verified",
37307
+ "replay",
37308
+ "relay_leaf",
37309
+ "not_ready",
37310
+ "in_flight",
37311
+ "runtime_timeout",
37312
+ "runtime_refused",
37313
+ "withheld",
37314
+ "send_failed",
37315
+ "failed",
37316
+ "sent"
37317
+ ]) });
37318
+ }
37319
+ var McpReplyWithheldError = class extends SendForbiddenError {
37320
+ constructor(channelId) {
37321
+ super(channelId);
37322
+ this.name = "McpReplyWithheldError";
37323
+ this.message = "reply withheld: the chat is no longer private to the owner and the agent; no retry";
37324
+ }
37325
+ };
36389
37326
  var RoomClassChangedSendError = class extends SendForbiddenError {
36390
37327
  constructor(channelId) {
36391
37328
  super(channelId);
@@ -36405,6 +37342,7 @@ function rosterFreshAt(entry, heldEpoch, heldGroupId) {
36405
37342
  var RoomGrantEpochMovedError = class extends Error {
36406
37343
  };
36407
37344
  var ROOM_GRANT_TRACK_MAX = 4096;
37345
+ var MCP_DELIVERED_TRACK_MAX = 4096;
36408
37346
  var VOICE_FRAME_PARENT_MAX = 4096;
36409
37347
  var SessionSupersededError = class extends Error {
36410
37348
  constructor() {
@@ -36556,8 +37494,12 @@ var BridgeAgent = class _BridgeAgent {
36556
37494
  onSupersedeUnopenable: (channelId, groupId) => this.healSupersedeUnopenable(channelId, groupId, this.drainExemptionAtCallIn())
36557
37495
  });
36558
37496
  this.home = deps.home ?? homedir4();
37497
+ this.mcpCatalog = deps.mcpCatalog;
37498
+ this.mcpDeliveredTrackMax = deps.mcpDeliveredTrackMax ?? MCP_DELIVERED_TRACK_MAX;
37499
+ this.mcpPolicyRetryDelaysMs = deps.mcpPolicyRetryDelaysMs?.length ? deps.mcpPolicyRetryDelaysMs : MCP_POLICY_RETRY_DELAYS_MS;
36559
37500
  this.grantWriters = {
36560
37501
  writeManifestPolicy: deps.grantWriters?.writeManifestPolicy ?? writeManifestPolicy,
37502
+ writeMcpPolicy: deps.grantWriters?.writeMcpPolicy ?? writeMcpPolicy,
36561
37503
  writeManagedFiles: deps.grantWriters?.writeManagedFiles ?? writeManagedFiles,
36562
37504
  bindRuntimeConfig: deps.grantWriters?.bindRuntimeConfig ?? bindRuntimeConfig,
36563
37505
  removeManagedLlmSource: deps.grantWriters?.removeManagedLlmSource ?? removeManagedLlmSource
@@ -36576,7 +37518,7 @@ var BridgeAgent = class _BridgeAgent {
36576
37518
  threshold: EMBEDDING_MANIFEST.similarityThreshold,
36577
37519
  warm: windowsSemanticOn(this.platform)
36578
37520
  };
36579
- return deps.rankingTransport ? deps.rankingTransport(init) : workerTransport(init, dirname5(fileURLToPath2(import.meta.url)));
37521
+ return deps.rankingTransport ? deps.rankingTransport(init) : workerTransport(init, dirname6(fileURLToPath2(import.meta.url)));
36580
37522
  }
36581
37523
  });
36582
37524
  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);
@@ -36587,7 +37529,7 @@ var BridgeAgent = class _BridgeAgent {
36587
37529
  modelId: EMBEDDING_MANIFEST.modelId,
36588
37530
  threshold: EMBEDDING_MANIFEST.similarityThreshold
36589
37531
  });
36590
- this.heartbeat = new CallHeartbeat(this.machinePaths ? join17(this.home, ".securecoms", "run", "voice-call") : null, cfg.identityId, VOICE_LIVE_WINDOW_MS);
37532
+ this.heartbeat = new CallHeartbeat(this.machinePaths ? join18(this.home, ".securecoms", "run", "voice-call") : null, cfg.identityId, VOICE_LIVE_WINDOW_MS);
36591
37533
  this.reembed = new ReembedQueue({
36592
37534
  store: this.store,
36593
37535
  embedder: this.embedder,
@@ -36646,15 +37588,15 @@ var BridgeAgent = class _BridgeAgent {
36646
37588
  this.bootDrainWaitMs = deps.bootDrainWaitMs ?? BOOT_DRAIN_WAIT_MS_DEFAULT;
36647
37589
  this.searchCapabilityRefetchDebounceMs = deps.searchCapabilityRefetchDebounceMs ?? SEARCH_CAPABILITY_REFETCH_DEBOUNCE_MS;
36648
37590
  this.searchCapabilityRefetchMaxWaitMs = deps.searchCapabilityRefetchMaxWaitMs ?? SEARCH_CAPABILITY_REFETCH_MAX_WAIT_MS;
36649
- this.replySleep = deps.replySleep ?? ((ms) => new Promise((resolve) => {
36650
- const t = setTimeout(resolve, ms);
37591
+ this.replySleep = deps.replySleep ?? ((ms) => new Promise((resolve2) => {
37592
+ const t = setTimeout(resolve2, ms);
36651
37593
  t.unref?.();
36652
37594
  }));
36653
37595
  this.actionRateFloor = new TokenBucketFloor({ capacity: 1, refillPerMinute: 30, now: this.actionClock });
36654
37596
  this.globalActionRateFloor = new TokenBucketFloor({ capacity: 1, refillPerMinute: 30, now: this.actionClock });
36655
37597
  const self = this;
36656
37598
  this.downloadBlobImpl = deps.downloadBlob ?? async function(blobId) {
36657
- const url = `${cfg.apiBase}/api/v1/attachments/${encodeURIComponent(blobId)}?caller_identity_id=${encodeURIComponent(cfg.identityId)}`;
37599
+ const url = `${cfg.apiBase}/api/v1/attachments/${encodeURIComponent(assertBlobId(blobId))}?caller_identity_id=${encodeURIComponent(cfg.identityId)}`;
36658
37600
  const res = await fetchWithDeadline(self.fetchImpl, url, { headers: { authorization: `Bearer ${cfg.apiKey}` } }, {
36659
37601
  op: "attachment_download",
36660
37602
  timeoutMs: FETCH_DEADLINE_MS.blobBase,
@@ -36674,8 +37616,9 @@ var BridgeAgent = class _BridgeAgent {
36674
37616
  const j = await res.json();
36675
37617
  return String(j.attachment_id);
36676
37618
  };
36677
- this.attachmentsDirImpl = deps.attachmentsDir ?? join17(dirname5(cfg.storePath), "attachments");
36678
- this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join17(dirname5(cfg.storePath), "vault-owner-pins.json"));
37619
+ this.attachmentsDirImpl = deps.attachmentsDir ?? join18(dirname6(cfg.storePath), "attachments");
37620
+ this.vaultOutboxDirImpl = deps.vaultOutboxDir !== void 0 ? deps.vaultOutboxDir : cfg.storePath === ":memory:" ? null : join18(dirname6(cfg.storePath), VAULT_OUTBOX_DIRNAME);
37621
+ this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join18(dirname6(cfg.storePath), "vault-owner-pins.json"));
36679
37622
  this.action = {
36680
37623
  begin: (channelId) => this.actionBegin(channelId),
36681
37624
  start: (opts) => this.actionStart(opts),
@@ -37042,6 +37985,39 @@ var BridgeAgent = class _BridgeAgent {
37042
37985
  roomDefaultGrants = roomDefaultGrantsEnabled();
37043
37986
  /** Owner grants (spec §7.4): the two manifest switches, read once at construction. */
37044
37987
  ownerDmManifestGrants = ownerDmManifestGrantsEnabled();
37988
+ /**
37989
+ * MCP client Task 16 (OQ-16, coordinator ruling D-7): SECURECOMS_MCP_GRANTS, read ONCE at construction. "off" writes
37990
+ * kill_switch (deleting); any value other than unset/"on"/"off" is suspended (non-deleting, one content-free warn).
37991
+ */
37992
+ mcpHostSwitch = mcpGrantsHostSwitch(process.env);
37993
+ /** MCP Task 16 test seam (BridgeAgentDeps.mcpCatalog); undefined in production, so MCP_CATALOG decides. */
37994
+ mcpCatalog;
37995
+ /**
37996
+ * MCP Task 16 (OQ-1; PR-B1 audit M-2): the two raw /files fields from the LAST fetch. Any failed or rejected fetch
37997
+ * sets `fetched: false` (no_config, non-deleting): the last values are never kept to keep an `ok` document alive.
37998
+ */
37999
+ mcpConfig = Object.freeze({ fetched: false });
38000
+ /**
38001
+ * MCP Task 16: the mcp-policy.json document this process last WROTE (the file the runtime reads), or null before the
38002
+ * first write and after a failed one (fail closed: issuance reads only a document known to be on disk).
38003
+ */
38004
+ mcpPolicyWritten = null;
38005
+ mcpPolicyWrittenJson = null;
38006
+ /** PR-B2 follow-up: a failed write is retried on this backoff (the last value repeats) until one lands or close(). */
38007
+ mcpPolicyRetryDelaysMs;
38008
+ mcpPolicyRetryTimer = null;
38009
+ mcpPolicyRetryAttempt = 0;
38010
+ /** MCP Task 18: the loopback catalog listers of the attached runtime sockets that declared mcp_client_v1 (ws-server.ts). */
38011
+ mcpCatalogListers = /* @__PURE__ */ new Map();
38012
+ /** MCP Task 18 (OQ-15): per server 1 in flight, 3 / 60 s, 20 / hour; per agent 40 / hour. Memory only. */
38013
+ mcpCatalogRate = new McpCatalogRateLimiter({
38014
+ onRateLimited: (scope) => writeServiceLog2("warn", "mcp.catalog_request", {
38015
+ state: ServiceLogEnum.of("catalog_rate_limited", ["catalog_rate_limited"]),
38016
+ scope: ServiceLogEnum.of(scope, ["server_minute", "server_hour", "agent_hour"])
38017
+ })
38018
+ });
38019
+ /** MCP Task 18: answers in progress (outside the ingest cycle). Observable by tests; close() does not wait on them. */
38020
+ mcpCatalogWork = /* @__PURE__ */ new Set();
37045
38021
  roomManifestGrants = roomManifestGrantsEnabled();
37046
38022
  /**
37047
38023
  * Audit M-B4: DM channels whose roster was epoch-fresh at THIS ingest
@@ -37052,6 +38028,39 @@ var BridgeAgent = class _BridgeAgent {
37052
38028
  // RECORDED here and in the sealed store; no grant code reads it in step 1.
37053
38029
  toolGrantManifest = null;
37054
38030
  toolGrantCounter = null;
38031
+ /**
38032
+ * Tool status Phase 0 (spec §3.1, §3.2; plan Task 8): the latest owner-DM last-turn record, in memory only (no store
38033
+ * table: after a restart there is none until the next typed owner-DM turn). ADVISORY: nothing reads it to decide a
38034
+ * grant. Content-free: enums, booleans, one 15-minute bucket `at` (ruling M-2), one counter, built-in names from the
38035
+ * allowlist. This is the latest record, open bucket included; what leaves the host is the sender's closed record.
38036
+ */
38037
+ ownerDmLastTurn = null;
38038
+ /**
38039
+ * Test seams (Task 8; ruling M-2): the cause function, the bridge clock the record and the sender read, and the
38040
+ * sender's timer and jitter source.
38041
+ */
38042
+ toolStatusCause = ownerDmCause;
38043
+ toolStatusNow = () => Date.now();
38044
+ toolStatusSetTimer = defaultToolStatusSetTimer;
38045
+ toolStatusClearTimer = defaultToolStatusClearTimer;
38046
+ toolStatusRandom = Math.random;
38047
+ /**
38048
+ * Phase 0 Task 9 (spec §3.5), ruling M-2: the post of the record to the narrow route, only just after a 15-minute
38049
+ * UTC bucket boundary, and only a record whose bucket has closed.
38050
+ */
38051
+ toolStatusSender = new ToolStatusSender({
38052
+ post: (rec) => this.postToolStatus(rec),
38053
+ now: () => this.toolStatusNow(),
38054
+ setTimer: (fn, ms) => this.toolStatusSetTimer(fn, ms),
38055
+ clearTimer: (h) => this.toolStatusClearTimer(h),
38056
+ random: () => this.toolStatusRandom(),
38057
+ onFailure: (kind2) => writeServiceLog2("warn", "tool_status.post_failed", { status: ServiceLogEnum.of(kind2, ["http_error", "timeout", "network"]) })
38058
+ });
38059
+ /**
38060
+ * MCP Phase 1 Task 14 (OQ-3 ruling item 3): the sticky R-3 flag, mirrored from the sealed store. Starts false and is
38061
+ * set true at start() by the store (or by a failed read, fail closed). Only applyToolGrantManifest changes it after.
38062
+ */
38063
+ mcpSeen = false;
37055
38064
  manifestState = "no_manifest";
37056
38065
  /** Failed owner-turn checks since the last successful runtime-config report (§7.7). */
37057
38066
  ownerVerifyFailures = 0;
@@ -37406,6 +38415,9 @@ var BridgeAgent = class _BridgeAgent {
37406
38415
  uploadBlobImpl;
37407
38416
  /** Track 5: directory where received attachments are saved. */
37408
38417
  attachmentsDirImpl;
38418
+ vaultOutboxDirImpl;
38419
+ /** Audit 2026-10-30 HIGH 2: the outbox pinned at start(); null = every upload refused. */
38420
+ outboxPin = null;
37409
38421
  /** B7: persistence for the vault kekset's owner-pin TOFU state. */
37410
38422
  vaultOwnerStoreImpl;
37411
38423
  /** B7: last-seen kekset generation per enclave, enforced monotonic across
@@ -37514,10 +38526,12 @@ var BridgeAgent = class _BridgeAgent {
37514
38526
  const payload = await res.json();
37515
38527
  if (!payload) {
37516
38528
  writeServiceLog2("warn", "runtime_config.empty_response");
38529
+ this.noteMcpConfigUnfetched();
37517
38530
  return;
37518
38531
  }
37519
38532
  if (typeof payload.version !== "number") {
37520
38533
  writeServiceLog2("warn", "runtime_config.invalid_version");
38534
+ this.noteMcpConfigUnfetched();
37521
38535
  return;
37522
38536
  }
37523
38537
  const version = payload.version;
@@ -37531,6 +38545,9 @@ var BridgeAgent = class _BridgeAgent {
37531
38545
  manifestStepThrew = true;
37532
38546
  writeServiceLog2("warn", "authz.config_binding", { state: ServiceLogEnum.of("bind_failed", ["bind_failed"]) }, e);
37533
38547
  }
38548
+ const raw = payload;
38549
+ this.mcpConfig = Object.freeze({ fetched: true, mcp_entitled: raw.mcp_entitled, mcp_cause: raw.mcp_cause });
38550
+ this.recomputeMcpPolicy();
37534
38551
  const pin = this.cfg.ownerGrantPin?.pin ?? null;
37535
38552
  const inForce = pin !== null ? this.toolGrantManifest : null;
37536
38553
  if (inForce !== null && pin !== null) {
@@ -37597,8 +38614,81 @@ var BridgeAgent = class _BridgeAgent {
37597
38614
  await this.postRuntimeConfigReport(base, version, skillsJson);
37598
38615
  } catch (e) {
37599
38616
  writeServiceLog2("warn", "runtime_config.refetch_failed", {}, e);
38617
+ this.noteMcpConfigUnfetched();
37600
38618
  }
37601
38619
  }
38620
+ /** MCP Task 16 (OQ-1): the bridge's in-force entitlement reading; not fetched (or failed) is suspended. */
38621
+ mcpEntitlementInForce() {
38622
+ return this.mcpConfig.fetched ? mcpEntitlementReading(this.mcpConfig.mcp_entitled, this.mcpConfig.mcp_cause) : "suspended";
38623
+ }
38624
+ /** PR-B1 audit M-2: forget the last /files fields and rewrite mcp-policy.json (no_config: no grants, no deletion). */
38625
+ noteMcpConfigUnfetched() {
38626
+ this.mcpConfig = Object.freeze({ fetched: false });
38627
+ this.recomputeMcpPolicy();
38628
+ }
38629
+ /** MCP Task 16: the live inputs of mcp-policy.json (the pinned manifest in force only). */
38630
+ mcpPolicyInput() {
38631
+ const pin = this.cfg.ownerGrantPin?.pin ?? null;
38632
+ return {
38633
+ hostSwitch: this.mcpHostSwitch,
38634
+ config: this.mcpConfig,
38635
+ ownerDmManifestSwitchOn: this.ownerDmManifestGrants,
38636
+ manifest: pin !== null ? this.toolGrantManifest : null,
38637
+ capability: this.mcpClientCapable(),
38638
+ mcpSeen: this.mcpSeen,
38639
+ ...this.mcpCatalog ? { catalog: this.mcpCatalog } : {}
38640
+ };
38641
+ }
38642
+ /**
38643
+ * MCP Task 16 (spec §12.1): recompute mcp-policy.json and write it when it changed. The written document is what MCP
38644
+ * issuance and release-time re-narrowing read, so it is cleared BEFORE the write and set only after it lands: a failed
38645
+ * write leaves no document in force here (no MCP grant), and the next trigger tries again. Never throws.
38646
+ */
38647
+ recomputeMcpPolicy() {
38648
+ const doc = mcpPolicyFor(this.mcpPolicyInput());
38649
+ const json2 = JSON.stringify(doc);
38650
+ if (json2 === this.mcpPolicyWrittenJson) return;
38651
+ if (MCP_INERT_STATES.has(doc.state) && !doc.r3_hold && Object.keys(doc.servers).length === 0 && !existsSync5(join18(runtimeConfigDir(this.home, this.cfg.identityId), MCP_POLICY_FILE))) {
38652
+ this.mcpPolicyWritten = doc;
38653
+ this.mcpPolicyWrittenJson = json2;
38654
+ this.clearMcpPolicyRetry();
38655
+ return;
38656
+ }
38657
+ this.mcpPolicyWritten = null;
38658
+ this.mcpPolicyWrittenJson = null;
38659
+ try {
38660
+ this.grantWriters.writeMcpPolicy(this.home, this.cfg.identityId, doc);
38661
+ } catch (e) {
38662
+ writeServiceLog2("warn", "authz.mcp_policy", { state: ServiceLogEnum.of("write_failed", ["write_failed"]) }, e);
38663
+ this.scheduleMcpPolicyRetry();
38664
+ return;
38665
+ }
38666
+ this.mcpPolicyWritten = doc;
38667
+ this.mcpPolicyWrittenJson = json2;
38668
+ this.clearMcpPolicyRetry();
38669
+ }
38670
+ /** A document is in force: no pending retry, and the next failure starts the backoff from the first delay. */
38671
+ clearMcpPolicyRetry() {
38672
+ this.mcpPolicyRetryAttempt = 0;
38673
+ if (this.mcpPolicyRetryTimer !== null) clearTimeout(this.mcpPolicyRetryTimer);
38674
+ this.mcpPolicyRetryTimer = null;
38675
+ }
38676
+ /**
38677
+ * PR-B2 follow-up: one pending retry at a time. The retry recomputes from the state THEN (never re-writes a stale
38678
+ * document); any other trigger that writes successfully meanwhile cancels it. Nothing is in force on the bridge side
38679
+ * until a write lands (mcpPolicyWritten stays null).
38680
+ */
38681
+ scheduleMcpPolicyRetry() {
38682
+ if (this.closingDown || this.mcpPolicyRetryTimer !== null) return;
38683
+ const d = this.mcpPolicyRetryDelaysMs;
38684
+ const delay = d[Math.min(this.mcpPolicyRetryAttempt, d.length - 1)];
38685
+ this.mcpPolicyRetryAttempt++;
38686
+ this.mcpPolicyRetryTimer = setTimeout(() => {
38687
+ this.mcpPolicyRetryTimer = null;
38688
+ if (!this.closingDown) this.recomputeMcpPolicy();
38689
+ }, delay);
38690
+ this.mcpPolicyRetryTimer.unref?.();
38691
+ }
37602
38692
  /**
37603
38693
  * Plan 4 Task 11: the refetch under a manifest in force. Returns false when the binding failed (H-4),
37604
38694
  * after the memory was applied; the caller then writes nothing else. Throws only from a file write, with
@@ -37682,7 +38772,7 @@ var BridgeAgent = class _BridgeAgent {
37682
38772
  }
37683
38773
  /** No manifest in force: a manifest-policy.json left by an earlier one is replaced by an empty tool list. */
37684
38774
  overwriteStalePolicy() {
37685
- if (!existsSync5(join17(runtimeConfigDir(this.home, this.cfg.identityId), "manifest-policy.json"))) return;
38775
+ if (!existsSync5(join18(runtimeConfigDir(this.home, this.cfg.identityId), "manifest-policy.json"))) return;
37686
38776
  this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, { v: 1, manifest_tools: [], secret_path_mode: "enforce" });
37687
38777
  }
37688
38778
  /** The report half of refetchRuntimeConfigOnce (split out for the plan 4 Task 11 bind_failed path). */
@@ -37690,6 +38780,7 @@ var BridgeAgent = class _BridgeAgent {
37690
38780
  const local = readLocalReport(this.home, this.cfg.identityId);
37691
38781
  const reportedSearchProviders = this.searchProvidersCapable();
37692
38782
  const reportedUtilityTools = this.utilityToolsCapable();
38783
+ const carriedOwnerDmTurn = this.toolStatusSender.carry();
37693
38784
  const report = {
37694
38785
  applied_version: version,
37695
38786
  paths: checkConfiguredPaths(skillsJson),
@@ -37759,8 +38850,17 @@ var BridgeAgent = class _BridgeAgent {
37759
38850
  // D19 item 4: content-free; the signed provider is one the attached runtime(s) cannot honour.
37760
38851
  search_provider_unsupported: this.searchProviderUnsupported(reportedSearchProviders),
37761
38852
  // Weather/currency §8 item 5: a separate key, false with no runtime attached.
37762
- utility_tools_v1: reportedUtilityTools
37763
- }
38853
+ utility_tools_v1: reportedUtilityTools,
38854
+ // MCP Task 19 (spec §12.3): separate from `capability`; content-free enums and pattern tokens only.
38855
+ mcp_capability: this.reportedMcpCapability(),
38856
+ mcp_servers: this.reportedMcpServers(),
38857
+ // Tool status Phase 0 (spec §3.5, round 2 H-5): an updated bridge, with or without a record yet.
38858
+ tool_status_v1: true
38859
+ },
38860
+ // Tool status Phase 0 (spec §3.5): the record rides every apply-time report, so record_report's wholesale replace
38861
+ // never erases it. Ruling M-2: the sender's CLOSED record, never the latest one, so a report sent inside a bucket
38862
+ // never reveals that bucket's change early. Advisory and content-free.
38863
+ ...carriedOwnerDmTurn ? { owner_dm_last_turn: carriedOwnerDmTurn } : {}
37764
38864
  };
37765
38865
  const reportedVerifyFailures = this.ownerVerifyFailures;
37766
38866
  const posted = await this.fetchBounded("runtime_config_report", "control", `${base}/report`, {
@@ -37849,8 +38949,16 @@ var BridgeAgent = class _BridgeAgent {
37849
38949
  }
37850
38950
  async startBody() {
37851
38951
  this.startedAt = Date.now();
38952
+ if (this.vaultOutboxDirImpl) {
38953
+ try {
38954
+ this.outboxPin = ensureOutbox(this.vaultOutboxDirImpl);
38955
+ } catch (e) {
38956
+ writeServiceLog2("warn", "vault_outbox.unavailable", {}, e);
38957
+ }
38958
+ }
37852
38959
  await this.restoreRuntimeInbox();
37853
38960
  await this.loadToolGrantState();
38961
+ this.recomputeMcpPolicy();
37854
38962
  this.recomputeManifestPolicy(NO_FEATURES);
37855
38963
  try {
37856
38964
  this.store.pruneRoomSearchCap(utcDayOf(Date.now()));
@@ -37874,8 +38982,8 @@ var BridgeAgent = class _BridgeAgent {
37874
38982
  const drain = this.ingestOnce().then(() => true, () => true);
37875
38983
  const drainedInTime = await Promise.race([
37876
38984
  drain,
37877
- new Promise((resolve) => {
37878
- const t = setTimeout(() => resolve(false), this.bootDrainWaitMs);
38985
+ new Promise((resolve2) => {
38986
+ const t = setTimeout(() => resolve2(false), this.bootDrainWaitMs);
37879
38987
  t.unref?.();
37880
38988
  void drain.then(() => clearTimeout(t));
37881
38989
  })
@@ -37885,7 +38993,7 @@ var BridgeAgent = class _BridgeAgent {
37885
38993
  await this.bootReconcileKeyPackage();
37886
38994
  } else {
37887
38995
  writeServiceLog2("warn", "boot.drain_slow", { wait_ms: this.bootDrainWaitMs });
37888
- const settled = new Promise((resolve) => this.ingestSettledWaiters.push(resolve));
38996
+ const settled = new Promise((resolve2) => this.ingestSettledWaiters.push(resolve2));
37889
38997
  this.deferredBootPublish = settled.then(async () => {
37890
38998
  this.bootDrainPending = false;
37891
38999
  if (this.closingDown) return;
@@ -38868,7 +39976,9 @@ var BridgeAgent = class _BridgeAgent {
38868
39976
  * for every envelope built, BEFORE it is published (SEC-06 #18 persists it
38869
39977
  * there so a retry can resend the exact bytes).
38870
39978
  */
38871
- async publishBody(channelId, body, id, onEnvelope, sealCheck) {
39979
+ async publishBody(channelId, body, id, onEnvelope, sealCheck, inReplyTo, mcpTurn) {
39980
+ const mcpGuard = mcpTurn === true || this.mcpReplyGuarded(channelId, inReplyTo ?? void 0);
39981
+ let attempt = 0;
38872
39982
  this.enforceBodyRateFloor(channelId, body);
38873
39983
  this.statusGate.replyStarted(channelId);
38874
39984
  const joined = await this.reconcileJoin(channelId);
@@ -38908,6 +40018,8 @@ var BridgeAgent = class _BridgeAgent {
38908
40018
  );
38909
40019
  },
38910
40020
  async (envelope) => {
40021
+ attempt++;
40022
+ if (mcpGuard) this.assertMcpReplyRelayFree(channelId, envelope, attempt);
38911
40023
  if (onEnvelope) await onEnvelope(envelope);
38912
40024
  return this.publishEnvelope(
38913
40025
  channelId,
@@ -38917,6 +40029,77 @@ var BridgeAgent = class _BridgeAgent {
38917
40029
  );
38918
40030
  this.latency.record("send.total", performance.now() - sendStart);
38919
40031
  }
40032
+ /**
40033
+ * MCP Task 16 (round 2 H-D): message ids whose DELIVERED authorization (after release-time re-narrowing) carried an
40034
+ * `mcp:` grant, with their channel and whether the runtime acked them. Recorded at the buffer exit, the one place
40035
+ * every consumer's copy leaves from (over-recording a turn a non-capable consumer then received without the grant only
40036
+ * adds a check). Kept after the ack, so a late frame citing the turn is still checked. Bounded: the oldest ACKED entry
40037
+ * goes first.
40038
+ */
40039
+ mcpDeliveredTurns = /* @__PURE__ */ new Map();
40040
+ mcpDeliveredTrackMax;
40041
+ /**
40042
+ * Record `m` when its authorization carries an `mcp:` grant. Returns false ONLY when `m` is a new turn, the record is
40043
+ * at its bound and no entry is acked: the caller (the buffer exit) must then issue no mcp: grant (audit M-2, fail
40044
+ * closed). `rebuild` (PR-B2 audit C-1 (b): a restored runtime-inbox row, or a replayed turn) records unconditionally:
40045
+ * those grants were issued before, so the record may exceed its bound by at most the runtime inbox's size.
40046
+ */
40047
+ noteMcpDelivered(m, rebuild = false) {
40048
+ if (!m.authorization?.tool_grants.some((g) => g.tool.startsWith(MCP_GRANT_PREFIX))) return true;
40049
+ if (!this.mcpDeliveredTurns.has(m.message_id) && this.mcpDeliveredTurns.size >= this.mcpDeliveredTrackMax) {
40050
+ let victim;
40051
+ for (const [id, r] of this.mcpDeliveredTurns) if (r.acked) {
40052
+ victim = id;
40053
+ break;
40054
+ }
40055
+ if (victim !== void 0) this.mcpDeliveredTurns.delete(victim);
40056
+ else if (!rebuild) return false;
40057
+ }
40058
+ this.mcpDeliveredTurns.set(m.message_id, { channelId: m.channel_id, acked: false });
40059
+ return true;
40060
+ }
40061
+ /**
40062
+ * H-D: whether a frame to `channelId` must pass the relay-free check: it answers a recorded MCP turn (any channel:
40063
+ * a frame of that turn sent elsewhere is checked against ITS channel), or it cites no turn while an MCP turn is
40064
+ * unacked in the same channel.
40065
+ */
40066
+ mcpReplyGuarded(channelId, inReplyTo) {
40067
+ if (inReplyTo !== void 0 && this.mcpDeliveredTurns.has(inReplyTo)) return true;
40068
+ if (inReplyTo !== void 0) return false;
40069
+ for (const r of this.mcpDeliveredTurns.values()) if (!r.acked && r.channelId === channelId) return true;
40070
+ return false;
40071
+ }
40072
+ /**
40073
+ * H-D, synchronous: read the channel's local leaves from the warm manager in one step and tie the read to the
40074
+ * envelope's SEALED group and epoch, so the leaf set is exactly the membership that can open these bytes. Owner and
40075
+ * agent only: pass. Anyone else: withhold. The tree moved past the seal on the first attempt: StaleEpochError, which
40076
+ * sendWithSelfHeal answers with its one converge-and-re-seal (the existing semantics); still unprovable on the retry,
40077
+ * or no warm manager, no pin, or any throw: withhold (fail closed). Read-only; never touches the group.
40078
+ *
40079
+ * Scope (PR-B2 audit item 5): this guards the `send` path (publishBody) only. `status` detail, the `action_*` run
40080
+ * frames, `/v1/send-attachment` and summary frames are NOT checked; they are safe for MCP turns only because the
40081
+ * runtime emits none of them (pinned by connect-agent-runtime/src/outbound-frames.arch.test.ts). A runtime that starts
40082
+ * emitting any of them needs this guard extended to that frame first.
40083
+ */
40084
+ assertMcpReplyRelayFree(channelId, envelope, attempt) {
40085
+ let verdict = "unknown";
40086
+ try {
40087
+ const pin = this.cfg.ownerGrantPin?.pin ?? null;
40088
+ const snap = this.provider.leafSnapshotCached(channelId);
40089
+ const sealedGroup = sealedGroupIdOf(envelope);
40090
+ if (pin !== null && snap !== null && sealedGroup !== void 0 && snap.groupId === sealedGroup) {
40091
+ if (snap.epoch === envelope.mls_group_epoch) {
40092
+ verdict = ownerDmLeavesRelayFree(snap.leaves, { ownerIdentityId: pin.ownerIdentityId, agentIdentityId: this.cfg.identityId }) ? "free" : "relay";
40093
+ } else if (snap.epoch > envelope.mls_group_epoch) verdict = "moved";
40094
+ }
40095
+ } catch {
40096
+ verdict = "unknown";
40097
+ }
40098
+ if (verdict === "free") return;
40099
+ if (verdict === "moved" && attempt === 1) throw new StaleEpochError(envelope.mls_group_epoch, envelope.mls_group_epoch + 1);
40100
+ writeServiceLog2("warn", "mcp.reply_withheld", { state: ServiceLogEnum.of(verdict, ["relay", "moved", "unknown"]) });
40101
+ throw new McpReplyWithheldError(channelId);
40102
+ }
38920
40103
  /**
38921
40104
  * Audit M-B1 (controller ruling): a reply to a room-granted turn is sealed
38922
40105
  * only at the epoch the room was classified at. On a move, the room is
@@ -38928,9 +40111,9 @@ var BridgeAgent = class _BridgeAgent {
38928
40111
  * the redelivery replay (replayReplyIntents, which has no in_reply_to in its
38929
40112
  * options) is checked the same as a live send.
38930
40113
  */
38931
- async publishBodyForReply(channelId, body, id, inReplyTo, onEnvelope) {
40114
+ async publishBodyForReply(channelId, body, id, inReplyTo, onEnvelope, mcpTurn) {
38932
40115
  const grant = inReplyTo !== null ? this.roomGrantEpochs.get(inReplyTo) : void 0;
38933
- if (grant === void 0) return this.publishBody(channelId, body, id, onEnvelope);
40116
+ if (grant === void 0) return this.publishBody(channelId, body, id, onEnvelope, void 0, inReplyTo, mcpTurn);
38934
40117
  if (channelId !== grant.channelId) {
38935
40118
  writeServiceLog2("warn", "reply.room_class_changed");
38936
40119
  throw new RoomClassChangedSendError(channelId);
@@ -38952,7 +40135,9 @@ var BridgeAgent = class _BridgeAgent {
38952
40135
  },
38953
40136
  (epoch) => {
38954
40137
  if (epoch !== grant.epoch) throw new RoomGrantEpochMovedError();
38955
- }
40138
+ },
40139
+ inReplyTo,
40140
+ mcpTurn
38956
40141
  );
38957
40142
  } catch (e) {
38958
40143
  if (!(e instanceof RoomGrantEpochMovedError)) throw e;
@@ -39087,14 +40272,15 @@ var BridgeAgent = class _BridgeAgent {
39087
40272
  const k = replyIntentKey({ inReplyTo: opts.inReplyTo, idempotencyKey: opts.idempotencyKey, ordinal: () => 0 });
39088
40273
  if (!k) throw new Error("idempotent send without a key");
39089
40274
  const id = replyMessageId(this.replyIdKey, this.cfg.identityId, channelId, k);
39090
- const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, opts.credit);
39091
- const { intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit: opts.credit ?? null });
39092
- if (intent.bodyMac !== mac2 || intent.channelId !== channelId || intent.state === "skipped") {
40275
+ const mcpTurn = this.mcpReplyGuarded(channelId, opts.inReplyTo);
40276
+ const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, opts.credit, mcpTurn);
40277
+ const { intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit: opts.credit ?? null, mcpTurn });
40278
+ if (intent.bodyMac !== replyBodyMac(this.replyIdKey, id, channelId, bytes, opts.credit, intent.mcpTurn) || intent.channelId !== channelId || intent.state === "skipped") {
39093
40279
  throw new IdempotencyConflictError();
39094
40280
  }
39095
40281
  return k;
39096
40282
  }
39097
- return this.claimUnkeyedPart(opts.inReplyTo, channelId, bytes, true, opts.credit ?? null);
40283
+ return this.claimUnkeyedPart(opts.inReplyTo, channelId, bytes, true, opts.credit ?? null, false);
39098
40284
  });
39099
40285
  return this.publishIntent(key, opts);
39100
40286
  }
@@ -39103,25 +40289,26 @@ var BridgeAgent = class _BridgeAgent {
39103
40289
  * the reply to `inReplyTo`. Caller holds the scope lock. `matchRetry` false
39104
40290
  * after a slot collision: the body must go to a NEW part.
39105
40291
  */
39106
- async claimUnkeyedPart(inReplyTo, channelId, bytes, matchRetry, replyCredit) {
40292
+ async claimUnkeyedPart(inReplyTo, channelId, bytes, matchRetry, replyCredit, inheritedMcpTurn) {
39107
40293
  const credit = replyCredit ?? void 0;
40294
+ const mcpTurn = inheritedMcpTurn || this.mcpReplyGuarded(channelId, inReplyTo);
39108
40295
  const scope = `reply:${inReplyTo}`;
39109
40296
  let latest = null;
39110
40297
  for (const intent of await this.store.listReplyIntents(inReplyTo)) {
39111
40298
  const n = intent.scope === scope ? partOrdinal(intent.part) : null;
39112
40299
  if (n !== null && (latest === null || n > latest.ordinal)) latest = { ordinal: n, intent };
39113
40300
  }
39114
- if (matchRetry && latest && latest.intent.state !== "skipped" && latest.intent.channelId === channelId && latest.intent.bodyMac === replyBodyMac(this.replyIdKey, latest.intent.replyMessageId, channelId, bytes, credit)) {
40301
+ if (matchRetry && latest && latest.intent.state !== "skipped" && latest.intent.channelId === channelId && latest.intent.bodyMac === replyBodyMac(this.replyIdKey, latest.intent.replyMessageId, channelId, bytes, credit, latest.intent.mcpTurn)) {
39115
40302
  return { scope, part: latest.intent.part, inReplyTo };
39116
40303
  }
39117
40304
  let ordinal = latest ? latest.ordinal + 1 : 0;
39118
40305
  for (let i = 0; i < REPLY_MAX_PART_ADVANCES; i++, ordinal++) {
39119
40306
  const k = { scope, part: `p:${ordinal}`, inReplyTo };
39120
40307
  const id = replyMessageId(this.replyIdKey, this.cfg.identityId, channelId, k);
39121
- const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, credit);
39122
- const { created, intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit });
40308
+ const mac2 = replyBodyMac(this.replyIdKey, id, channelId, bytes, credit, mcpTurn);
40309
+ const { created, intent } = await this.store.claimReplyIntent({ ...k, channelId, replyMessageId: id, bodyMac: mac2, body: bytes, replyCredit, mcpTurn });
39123
40310
  if (created) return k;
39124
- if (matchRetry && intent.state !== "skipped" && intent.bodyMac === mac2 && intent.channelId === channelId) return k;
40311
+ if (matchRetry && intent.state !== "skipped" && intent.channelId === channelId && intent.bodyMac === replyBodyMac(this.replyIdKey, id, channelId, bytes, credit, intent.mcpTurn)) return k;
39125
40312
  writeServiceLog2("info", "reply.part_slot_taken", { attempt: i + 1 });
39126
40313
  }
39127
40314
  throw new Error("could not claim a reply part");
@@ -39173,7 +40360,7 @@ var BridgeAgent = class _BridgeAgent {
39173
40360
  const purgeEpochBeforePublish = this.store.conversationPurgeEpoch(intent.channelId);
39174
40361
  const replyCapture = this.unifiedReplyCapture(intent.channelId, intent.inReplyTo ?? void 0);
39175
40362
  try {
39176
- await this.publishIntentWithRetry(intent.channelId, body, intent.replyMessageId, key, intent.inReplyTo);
40363
+ await this.publishIntentWithRetry(intent.channelId, body, intent.replyMessageId, key, intent.inReplyTo, intent.mcpTurn);
39177
40364
  } catch (e) {
39178
40365
  if (e instanceof AgentPausedSendError || e instanceof SendForbiddenError) {
39179
40366
  if (intent.inReplyTo !== null) this.store.deletePendingReplyIntents(intent.inReplyTo);
@@ -39200,7 +40387,7 @@ var BridgeAgent = class _BridgeAgent {
39200
40387
  JSON.stringify(["scope", key.scope]),
39201
40388
  () => (
39202
40389
  // r2 BLOCKER 1: the colliding row's OWN credit (verified above), never `opts`.
39203
- this.claimUnkeyedPart(inReplyTo, intent.channelId, bytes, false, isReplyCredit(intent.replyCredit) ? intent.replyCredit : null)
40390
+ this.claimUnkeyedPart(inReplyTo, intent.channelId, bytes, false, isReplyCredit(intent.replyCredit) ? intent.replyCredit : null, intent.mcpTurn)
39204
40391
  )
39205
40392
  );
39206
40393
  return this.publishIntent(next, opts, advances + 1);
@@ -39222,7 +40409,7 @@ var BridgeAgent = class _BridgeAgent {
39222
40409
  if (credit !== null && !isReplyCredit(credit)) return false;
39223
40410
  const id = replyMessageId(this.replyIdKey, this.cfg.identityId, intent.channelId, intent);
39224
40411
  if (id !== intent.replyMessageId) return false;
39225
- return replyBodyMac(this.replyIdKey, intent.replyMessageId, intent.channelId, intent.body, credit ?? void 0) === intent.bodyMac;
40412
+ return replyBodyMac(this.replyIdKey, intent.replyMessageId, intent.channelId, intent.body, credit ?? void 0, intent.mcpTurn === true) === intent.bodyMac;
39226
40413
  }
39227
40414
  /**
39228
40415
  * Weather/currency Task 17: what a verified row's reply is RECORDED as. Null credit: the decoded text unchanged (null
@@ -39243,12 +40430,12 @@ var BridgeAgent = class _BridgeAgent {
39243
40430
  * failure. Either way the intent stays pending, so the redelivery
39244
40431
  * republishes the same reply.
39245
40432
  */
39246
- async publishIntentWithRetry(channelId, body, id, key, inReplyTo) {
40433
+ async publishIntentWithRetry(channelId, body, id, key, inReplyTo, mcpTurn) {
39247
40434
  let rateLimited = 0;
39248
40435
  let inFlight = 0;
39249
40436
  for (; ; ) {
39250
40437
  try {
39251
- await this.publishIntentOnce(channelId, body, id, key, inReplyTo);
40438
+ await this.publishIntentOnce(channelId, body, id, key, inReplyTo, mcpTurn);
39252
40439
  return;
39253
40440
  } catch (e) {
39254
40441
  if (e instanceof ReplyInFlightError) {
@@ -39319,8 +40506,9 @@ var BridgeAgent = class _BridgeAgent {
39319
40506
  * original ack; a conflict can only mean the slot belongs to another
39320
40507
  * message (for example an old reply whose intent was pruned).
39321
40508
  */
39322
- async publishIntentOnce(channelId, body, id, key, inReplyTo) {
40509
+ async publishIntentOnce(channelId, body, id, key, inReplyTo, mcpTurn) {
39323
40510
  const current = await this.store.getReplyIntent(key.scope, key.part);
40511
+ const guardMcp = mcpTurn || current?.mcpTurn === true;
39324
40512
  try {
39325
40513
  if (current?.envelope) {
39326
40514
  const cached = JSON.parse(new TextDecoder().decode(current.envelope));
@@ -39370,7 +40558,8 @@ var BridgeAgent = class _BridgeAgent {
39370
40558
  key.part,
39371
40559
  new TextEncoder().encode(JSON.stringify(envelope)),
39372
40560
  sealedGroupIdOf(envelope)
39373
- )
40561
+ ),
40562
+ guardMcp
39374
40563
  );
39375
40564
  } catch (e) {
39376
40565
  if (publishConflictCode(e) === "message_id_conflict") throw new ReplyIdCollisionError();
@@ -39392,7 +40581,8 @@ var BridgeAgent = class _BridgeAgent {
39392
40581
  uploadBlob: (ciphertext, channelId) => this.uploadBlobImpl(ciphertext, channelId),
39393
40582
  encryptFile,
39394
40583
  bytesToBase64: bytesToBase642,
39395
- readFile: (p) => fsp.readFile(p).then((b) => new Uint8Array(b))
40584
+ // Security fix 2026-10-30: attachments upload only from the outbox.
40585
+ readFile: (p) => readOutboxFile(this.outboxPin, p).then((r) => r.bytes)
39396
40586
  };
39397
40587
  }
39398
40588
  receiveDeps() {
@@ -39402,8 +40592,8 @@ var BridgeAgent = class _BridgeAgent {
39402
40592
  decryptFile,
39403
40593
  computeFileDigest,
39404
40594
  base64ToBytes: base64ToBytes2,
39405
- mkdirp: (d) => fsp.mkdir(d, { recursive: true }).then(() => void 0),
39406
- writeFile: (p, data) => fsp.writeFile(p, data)
40595
+ mkdirp: (d) => fsp2.mkdir(d, { recursive: true }).then(() => void 0),
40596
+ writeFile: (p, data) => fsp2.writeFile(p, data)
39407
40597
  };
39408
40598
  }
39409
40599
  async sendAttachment(channelId, filePath2, caption) {
@@ -39411,8 +40601,11 @@ var BridgeAgent = class _BridgeAgent {
39411
40601
  const joined = await this.reconcileJoin(channelId);
39412
40602
  if (!joined) throw new ChannelPendingError(channelId);
39413
40603
  let body;
40604
+ let read;
39414
40605
  try {
39415
- body = await prepareAttachment(filePath2, caption, channelId, this.sendDeps());
40606
+ read = await readOutboxFile(this.outboxPin, filePath2);
40607
+ const bytes = read.bytes;
40608
+ body = await prepareAttachment(filePath2, caption, channelId, { ...this.sendDeps(), readFile: async () => bytes });
39416
40609
  } catch (e) {
39417
40610
  if (e.code === "ENOENT" || e.code === "EACCES" || e.code === "ENOTDIR" || e.code === "EISDIR") {
39418
40611
  throw new InvalidPathError(filePath2, e);
@@ -39453,6 +40646,7 @@ var BridgeAgent = class _BridgeAgent {
39453
40646
  );
39454
40647
  this.latency.record("send.total", performance.now() - sendStart);
39455
40648
  this.agentHops.noteOwnSend(channelId);
40649
+ await this.consumeUploaded(read);
39456
40650
  return { message_id: id, status: "sent" };
39457
40651
  }
39458
40652
  // ── B7: FileSafe vault commands (loopback boundary) ──────────────────────
@@ -39978,25 +41172,26 @@ var BridgeAgent = class _BridgeAgent {
39978
41172
  }
39979
41173
  };
39980
41174
  }
39981
- /** Read `path` off local disk, encrypt + wrap its DEK under the vault's
41175
+ /** Read `path` from the vault outbox (security fix 2026-10-30; see
41176
+ * ./outbox.ts), encrypt + wrap its DEK under the vault's
39982
41177
  * current KEK generation, and create+upload+commit it as a new vault
39983
41178
  * file node named `name` (which may differ from `path`'s basename). */
39984
41179
  async vaultWrite(enclaveId, vaultChannelId, path, name) {
39985
- const session = await this.getVaultSession(enclaveId, await this.resolveVaultChannelId(enclaveId, vaultChannelId));
39986
- let bytes;
41180
+ let read;
39987
41181
  try {
39988
- bytes = new Uint8Array(await fsp.readFile(path));
41182
+ read = await readOutboxFile(this.outboxPin, path);
39989
41183
  } catch (e) {
39990
41184
  const code = e.code;
39991
41185
  if (code === "ENOENT" || code === "EACCES" || code === "ENOTDIR" || code === "EISDIR") throw new InvalidPathError(path, e);
39992
41186
  throw e;
39993
41187
  }
41188
+ const session = await this.getVaultSession(enclaveId, await this.resolveVaultChannelId(enclaveId, vaultChannelId));
39994
41189
  const nodeId = crypto.randomUUID();
39995
- return vaultWriteImpl({
41190
+ const written = await vaultWriteImpl({
39996
41191
  enclaveId,
39997
41192
  nodeId,
39998
41193
  name,
39999
- bytes,
41194
+ bytes: read.bytes,
40000
41195
  deps: {
40001
41196
  exporterKekForGeneration: session.kekForGeneration,
40002
41197
  currentGeneration: session.currentGeneration,
@@ -40005,6 +41200,16 @@ var BridgeAgent = class _BridgeAgent {
40005
41200
  commitNode: (committedNodeId) => this.vaultCommitNode(enclaveId, committedNodeId)
40006
41201
  }
40007
41202
  });
41203
+ await this.consumeUploaded(read);
41204
+ return written;
41205
+ }
41206
+ /** Audit 2026-10-30 MED 5: an uploaded file leaves the outbox (no plaintext lingers). Content-free log. */
41207
+ async consumeUploaded(read) {
41208
+ if (!this.outboxPin || !await consumeOutboxFile(this.outboxPin, read)) writeServiceLog2("warn", "vault_outbox.consume_failed");
41209
+ }
41210
+ /** Audit 2026-10-30 MED 3: the pinned outbox path, served only on the authenticated GET /v1/vault-outbox. */
41211
+ vaultOutboxPath() {
41212
+ return this.outboxPin?.dir ?? null;
40008
41213
  }
40009
41214
  /** Download + verify + decrypt a vault file node. Returns the plaintext
40010
41215
  * base64-encoded (the loopback boundary is JSON-only, mirroring how
@@ -41865,16 +43070,20 @@ var BridgeAgent = class _BridgeAgent {
41865
43070
  }
41866
43071
  if (v.sender_id !== pin.ownerIdentityId) return this.ownerTurnFailed(m, "malformed");
41867
43072
  let leafClass = "ok";
43073
+ let relay = void 0;
41868
43074
  if (this.freshMeta(keyChannel)?.is_dm === true) {
41869
43075
  const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
41870
43076
  if (signedDm !== void 0 && signedDm !== keyChannel) return this.ownerTurnFailed(m, "wrong_dm_channel");
41871
- leafClass = classifyOwnerDmLeaves((await this.provider.leafTreeSummary(m.channelId)).leaves, {
43077
+ const leaves = (await this.provider.leafTreeSummary(m.channelId)).leaves;
43078
+ const relayIdentityId = this.toolGrantManifest?.owner_dm_relay_identity_id;
43079
+ leafClass = classifyOwnerDmLeaves(leaves, {
41872
43080
  ownerIdentityId: pin.ownerIdentityId,
41873
43081
  agentIdentityId: this.cfg.identityId,
41874
- relayIdentityId: this.toolGrantManifest?.owner_dm_relay_identity_id,
43082
+ relayIdentityId,
41875
43083
  isServerRelay: (id) => this.freshRoster(m.channelId)?.get(id)?.is_relay === true
41876
43084
  });
41877
43085
  if (leafClass === "foreign") return this.ownerTurnFailed(m, "not_owner_dm_leaves");
43086
+ relay = ownerDmRelayOf(leafClass, leaves, { ownerIdentityId: pin.ownerIdentityId, agentIdentityId: this.cfg.identityId, relayIdentityId });
41878
43087
  }
41879
43088
  const held = await this.heldPosition(m.channelId);
41880
43089
  if (held.groupId === null) return this.ownerTurnFailed(m, "verify_error");
@@ -41883,7 +43092,9 @@ var BridgeAgent = class _BridgeAgent {
41883
43092
  }
41884
43093
  const replay = Object.freeze({ channelId: m.channelId, groupId: held.groupId, epoch: v.mls_group_epoch });
41885
43094
  this.ownerReplayPending.set(m.messageId, replay);
41886
- return Object.freeze({ kind: leafClass === "unsigned_relay" ? "constants_only" : "verified", replay });
43095
+ const relayField = relay !== void 0 ? { relay } : {};
43096
+ if (leafClass === "unsigned_relay") return Object.freeze({ kind: "constants_only", replay, ...relayField });
43097
+ return Object.freeze({ kind: "verified", replay, ...relayField, relayLeaf: relayLeafFromPhase0Relay(relay) });
41887
43098
  } catch {
41888
43099
  return this.ownerTurnFailed(m, "verify_error");
41889
43100
  }
@@ -41959,6 +43170,12 @@ var BridgeAgent = class _BridgeAgent {
41959
43170
  }
41960
43171
  /** start(): drop other keys' manifest state, re-verify the stored one for the pinned key, and prune. */
41961
43172
  async loadToolGrantState() {
43173
+ try {
43174
+ this.mcpSeen = await this.store.loadMcpSeen();
43175
+ } catch (e) {
43176
+ this.mcpSeen = true;
43177
+ writeServiceLog2("warn", "authz.mcp_seen_load_failed", {}, e);
43178
+ }
41962
43179
  const pin = this.cfg.ownerGrantPin?.pin;
41963
43180
  if (!pin) return;
41964
43181
  try {
@@ -41966,9 +43183,10 @@ var BridgeAgent = class _BridgeAgent {
41966
43183
  this.store.dropToolGrantAppliedExcept(pin.publicKeyB64);
41967
43184
  const saved = await this.store.loadToolGrantManifest(pin.publicKeyB64);
41968
43185
  if (saved) {
41969
- const v = verifyToolGrantManifest(saved.wrapper, { pin, agentIdentityId: this.cfg.identityId, storedCounter: null });
43186
+ const v = verifyToolGrantManifest(saved.wrapper, { pin, agentIdentityId: this.cfg.identityId, storedCounter: null, ...this.mcpCatalog ? { mcpCatalog: this.mcpCatalog } : {} });
41970
43187
  if (v.state === "ok") {
41971
43188
  this.toolGrantManifest = v.manifest;
43189
+ if (v.manifest.signedMcpPresent && !this.mcpSeen) await this.setMcpSeen(true);
41972
43190
  this.toolGrantCounter = v.manifest.counter;
41973
43191
  } else {
41974
43192
  this.logManifest("malformed");
@@ -41993,8 +43211,9 @@ var BridgeAgent = class _BridgeAgent {
41993
43211
  this.logManifest("no_manifest");
41994
43212
  return;
41995
43213
  }
41996
- const v = verifyToolGrantManifest(raw, { pin, agentIdentityId: this.cfg.identityId, storedCounter: this.toolGrantCounter });
43214
+ const v = verifyToolGrantManifest(raw, { pin, agentIdentityId: this.cfg.identityId, storedCounter: this.toolGrantCounter, ...this.mcpCatalog ? { mcpCatalog: this.mcpCatalog } : {} });
41997
43215
  if (v.state === "ok") {
43216
+ if (v.manifest.signedMcpPresent) await this.setMcpSeen(true);
41998
43217
  try {
41999
43218
  await this.store.saveToolGrantManifest(pin.publicKeyB64, v.manifest.counter, v.wrapper);
42000
43219
  } catch {
@@ -42003,13 +43222,34 @@ var BridgeAgent = class _BridgeAgent {
42003
43222
  }
42004
43223
  this.toolGrantManifest = v.manifest;
42005
43224
  this.toolGrantCounter = v.manifest.counter;
43225
+ this.recomputeMcpPolicy();
43226
+ if (v.manifest.mcpKey === "present" && !v.manifest.signedMcpPresent && this.mcpSeen) await this.setMcpSeen(false);
42006
43227
  this.logManifest("ok");
42007
43228
  if (v.droppedUnknown) this.logManifest("unknown_entry");
42008
43229
  return;
42009
43230
  }
42010
43231
  this.logManifest(v.state);
43232
+ if (v.state === "wrong_owner") this.dropOwnerDmLastTurn();
42011
43233
  if (v.state !== "unchanged" && this.toolGrantManifest) this.logManifest("kept_last_good");
42012
43234
  }
43235
+ /**
43236
+ * MCP Task 14: the R-3 hold Task 17 filters on and Task 15 writes as `r3_hold`. True while the manifest in force signs
43237
+ * an owner_dm `mcp:` string, or while the sticky flag is set (an old client's re-sign cannot lift it, audit C-1).
43238
+ */
43239
+ r3Hold() {
43240
+ return this.toolGrantManifest?.signedMcpPresent === true || this.mcpSeen;
43241
+ }
43242
+ /** Persist then mirror. Setting mirrors even on a failed write; clearing mirrors only after the write (fail closed). */
43243
+ async setMcpSeen(seen) {
43244
+ if (seen) this.mcpSeen = true;
43245
+ try {
43246
+ await this.store.saveMcpSeen(seen);
43247
+ } catch (e) {
43248
+ writeServiceLog2("warn", "authz.mcp_seen_save_failed", {}, e);
43249
+ return;
43250
+ }
43251
+ this.mcpSeen = seen;
43252
+ }
42013
43253
  logManifest(state) {
42014
43254
  this.manifestState = state;
42015
43255
  const quiet = state === "ok" || state === "unchanged" || state === "no_pin" || state === "no_manifest";
@@ -42106,6 +43346,7 @@ var BridgeAgent = class _BridgeAgent {
42106
43346
  let tool_grants = NO_TOOL_GRANTS;
42107
43347
  let relaySpeaker;
42108
43348
  let memoryWrite = false;
43349
+ let relayFree = false;
42109
43350
  const epochFresh = rosterFreshAt(entry, heldEpoch, heldGroupId) && (opts.voice === void 0 || rosterFreshAt(vEntry, opts.voice.heldEpoch, opts.voice.heldGroupId));
42110
43351
  const ownerDmFresh = epochFresh || opts.voice === void 0 && this.ownerDmCycleFresh.has(channelId);
42111
43352
  if (ownerDmFresh && this.ownerDmDefaultGrants && meta?.is_dm === true && sender) {
@@ -42132,6 +43373,7 @@ var BridgeAgent = class _BridgeAgent {
42132
43373
  tool_grants = ROOM_DEFAULT_READ_GRANTS;
42133
43374
  }
42134
43375
  let grantSource = tool_grants.length ? "constant" : "none";
43376
+ let ownerDmEffective = null;
42135
43377
  const manifest = this.cfg.ownerGrantPin?.pin ? this.toolGrantManifest : null;
42136
43378
  const binding = this.manifestBinding;
42137
43379
  const boundNow = binding !== null && binding.state === "bound" && binding.counter === this.toolGrantCounter;
@@ -42150,6 +43392,21 @@ var BridgeAgent = class _BridgeAgent {
42150
43392
  });
42151
43393
  tool_grants = r.grants;
42152
43394
  grantSource = r.source;
43395
+ ownerDmEffective = r;
43396
+ const mcp = mcpIssuance({
43397
+ source: r.source,
43398
+ verdict: opts.ownerVerdict,
43399
+ entitlement: this.mcpEntitlementInForce(),
43400
+ hostSwitch: this.mcpHostSwitch,
43401
+ ownerDmManifestSwitchOn: this.ownerDmManifestGrants,
43402
+ disabled: this.payloadToolDeny,
43403
+ manifest,
43404
+ policy: this.mcpPolicyWritten
43405
+ });
43406
+ if (mcp.length > 0) {
43407
+ tool_grants = Object.freeze([...tool_grants, ...mcp]);
43408
+ relayFree = true;
43409
+ }
42153
43410
  } else if (tool_grants === ROOM_DEFAULT_READ_GRANTS) {
42154
43411
  const r = effectiveToolGrants({
42155
43412
  audience: "team_room",
@@ -42167,12 +43424,39 @@ var BridgeAgent = class _BridgeAgent {
42167
43424
  if (opts.ownerVerdict?.kind === "failed") {
42168
43425
  tool_grants = NO_TOOL_GRANTS;
42169
43426
  memoryWrite = false;
43427
+ relayFree = false;
42170
43428
  relaySpeaker = void 0;
42171
43429
  grantSource = "none";
42172
43430
  }
42173
43431
  const roomClass = meta?.is_dm !== false ? "na" : entry?.crossOrg === false ? "internal" : entry?.crossOrg === true ? "cross_org" : "unknown";
42174
43432
  if (opts.log !== false) {
42175
43433
  this.logAuthzDecision(channelId, senderId, relaySpeakerId, audience, tool_grants.length, roomClass, epochFresh, memoryWrite, grantSource);
43434
+ const pinOwner = this.cfg.ownerGrantPin?.pin?.ownerIdentityId;
43435
+ const verdict = opts.ownerVerdict;
43436
+ if (pinOwner !== void 0 && senderId === pinOwner && meta?.is_dm === true && relaySpeakerId === void 0 && opts.voiceScoped !== true && opts.voice === void 0 && verdict !== void 0 && verdict.kind !== "unchecked" && !(verdict.kind === "failed" && verdict.reason === "replay")) {
43437
+ const pinned = this.channelOwner.get(channelId);
43438
+ const ownerId = pinned && pinned !== "anomalous" ? pinned.identityId : void 0;
43439
+ const ownerDirect = ownerId !== void 0 && audience === "owner_dm" && senderId === ownerId;
43440
+ this.noteOwnerDmTurn({
43441
+ cause: {
43442
+ killSwitchOff: !this.ownerDmDefaultGrants,
43443
+ manifestSwitchOff: !this.ownerDmManifestGrants,
43444
+ pinMissingOrAnomalous: ownerId === void 0,
43445
+ // OQ-5: only from a PRESENT roster entry; a missing (TTL-expired) entry is `stale` below.
43446
+ senderNotOwner: entry !== void 0 && (sender === void 0 || !ownerDirect),
43447
+ stale: !ownerDmFresh || entry === void 0,
43448
+ verdict,
43449
+ manifestPresent: manifest !== null,
43450
+ signedDmChannel: manifest?.owner_dm_channel_id,
43451
+ channelId,
43452
+ boundNow,
43453
+ consumerAttached: this.manifestConsumer !== null
43454
+ },
43455
+ grantSource,
43456
+ toolGrants: tool_grants,
43457
+ withheld: grantSource === "manifest" && ownerDmEffective !== null ? ownerDmEffective.withheld : []
43458
+ });
43459
+ }
42176
43460
  }
42177
43461
  return Object.freeze({
42178
43462
  version: 1,
@@ -42180,9 +43464,78 @@ var BridgeAgent = class _BridgeAgent {
42180
43464
  sender_membership: sender ? "active" : "unknown",
42181
43465
  tool_grants,
42182
43466
  ...relaySpeaker ? { relay_speaker: relaySpeaker } : {},
42183
- ...memoryWrite ? { memory_write: true } : {}
43467
+ ...memoryWrite ? { memory_write: true } : {},
43468
+ ...relayFree ? { relay_free: true } : {}
42184
43469
  });
42185
43470
  }
43471
+ /**
43472
+ * Tool status Phase 0 (spec §3.2, §3.5; plan Task 8; round 1 H-2, M-4; round 2 H-4): build the candidate record and
43473
+ * keep the previous one (and its `at`) when the change tuple is the same, so `at` means "last changed". Never throws
43474
+ * into the authorization path. Never logs a channel id, sender id or tool name.
43475
+ */
43476
+ noteOwnerDmTurn(t) {
43477
+ try {
43478
+ const v = t.cause.verdict;
43479
+ const relay = (v.kind === "verified" || v.kind === "constants_only") && v.relay !== void 0 ? v.relay : "unknown";
43480
+ let reason = {};
43481
+ if (t.grantSource !== "manifest") {
43482
+ const c = this.toolStatusCause(t.cause);
43483
+ if (c !== null && c.source === t.grantSource && c.cause !== void 0) {
43484
+ reason = c.sub !== void 0 ? { cause: c.cause, sub: c.sub } : { cause: c.cause };
43485
+ } else {
43486
+ writeServiceLog2("warn", "tool_status.source_mismatch");
43487
+ }
43488
+ } else {
43489
+ const c = this.toolStatusCause(t.cause);
43490
+ if (c === null || c.source !== "manifest") writeServiceLog2("warn", "tool_status.source_mismatch");
43491
+ }
43492
+ const names = t.toolGrants.map((g) => g.tool);
43493
+ const candidate = Object.freeze({
43494
+ at: bucketStartAt(this.toolStatusNow()),
43495
+ // ruling M-2: the 15-minute UTC bucket, never the minute
43496
+ counter: this.toolGrantCounter,
43497
+ source: t.grantSource,
43498
+ ...reason,
43499
+ relay,
43500
+ issued: Object.freeze(sanitizeIssued(names)),
43501
+ withheld: Object.freeze(t.withheld.map((w) => Object.freeze({ tool: w.tool, why: w.why }))),
43502
+ // A count only, never a name (content-free). 0 unless the MCP issuance above added mcp: grants.
43503
+ mcp_issued: Math.min(160, names.filter((n) => n.startsWith("mcp:")).length)
43504
+ });
43505
+ if (sameTuple(this.ownerDmLastTurn, candidate)) return;
43506
+ this.ownerDmLastTurn = candidate;
43507
+ this.toolStatusSender.note(candidate);
43508
+ } catch (e) {
43509
+ writeServiceLog2("warn", "tool_status.record_failed", {}, e);
43510
+ }
43511
+ }
43512
+ /** R2-M1: the served manifest names another owner than the pin; the held record belongs to the old owner. */
43513
+ dropOwnerDmLastTurn() {
43514
+ this.ownerDmLastTurn = null;
43515
+ this.toolStatusSender.drop();
43516
+ }
43517
+ /**
43518
+ * Phase 0 Task 9 (spec §3.5): POST the record, body = the record only, to the narrow agent-key route. The backend
43519
+ * resolves the identity from the device credential; no identity, channel or sender id is in the URL or the body.
43520
+ * Returns the status class only; never throws.
43521
+ */
43522
+ async postToolStatus(rec) {
43523
+ try {
43524
+ const res = await this.fetchBounded("runtime_config_tool_status", "control", `${this.cfg.apiBase}/api/v1/agents/me/runtime-config/tool-status`, {
43525
+ method: "POST",
43526
+ headers: { authorization: `Bearer ${this.cfg.apiKey}`, "content-type": "application/json" },
43527
+ body: JSON.stringify(rec)
43528
+ });
43529
+ const retryAfter = res.headers.get("retry-after");
43530
+ try {
43531
+ await res.body?.cancel();
43532
+ } catch {
43533
+ }
43534
+ return { status: res.status, retryAfter };
43535
+ } catch (e) {
43536
+ return e?.name === "FetchDeadlineError" ? "timeout" : "network";
43537
+ }
43538
+ }
42186
43539
  /**
42187
43540
  * One content-free INFO line per authorization decision (every call site of
42188
43541
  * authorizationFor stages exactly one runtime turn). It says WHY a turn got
@@ -42406,6 +43759,111 @@ var BridgeAgent = class _BridgeAgent {
42406
43759
  * client's own wait already renders "Summary unavailable" — so there is
42407
43760
  * nothing to catch here besides the send itself.
42408
43761
  */
43762
+ /**
43763
+ * MCP client Task 18 (spec §8 item 2; audit M-3, H-3; OQ-2, OQ-15). Accept an `mcp_catalog_request` only when: its
43764
+ * body is exactly the §8 shape; it is a direct frame (never a voice channel's) in a channel the server marks as a DM
43765
+ * that IS the signed owner DM; verifyOwnerTurn says `verified` (the pinned owner's signature, the signed DM, the leaf
43766
+ * check, not a replay) with `relayLeaf === false`; mcp-policy.json as written is `ok` with this server `ok`; a runtime
43767
+ * socket that declared mcp_client_v1 is attached; and the rate limits admit it. The replay row is written HERE
43768
+ * (M-3: the request never reaches stageRuntimeInbox, where it is otherwise written), for every verified request, before
43769
+ * anything is answered; the ownerReplayPending entry is removed in `finally`. A failure to record answers nothing.
43770
+ * Never throws. The answer runs outside the ingest cycle, so the row is acked now.
43771
+ */
43772
+ async acceptMcpCatalogRequest(m, body) {
43773
+ try {
43774
+ const req = parseMcpCatalogRequest(body);
43775
+ if (req === null) return logMcpCatalogRequest("malformed");
43776
+ const signedDm = this.toolGrantManifest?.owner_dm_channel_id;
43777
+ if (this.keyChannelForFrame(m) !== m.channelId || this.freshMeta(m.channelId)?.is_dm !== true || signedDm === void 0 || signedDm !== m.channelId) {
43778
+ return logMcpCatalogRequest("not_owner_dm");
43779
+ }
43780
+ const verdict = await this.verifyOwnerTurn(m);
43781
+ if (verdict.kind !== "verified") return logMcpCatalogRequest(verdict.kind === "failed" && verdict.reason === "replay" ? "replay" : "not_verified");
43782
+ this.store.recordOwnerMessage(verdict.replay.channelId, m.messageId, verdict.replay.groupId, verdict.replay.epoch, Date.now());
43783
+ if (verdict.relayLeaf !== false) return logMcpCatalogRequest("relay_leaf");
43784
+ const target = this.mcpCatalogTarget(req.serverId);
43785
+ const lister = this.mcpClientCapable() ? this.mcpCatalogListers.values().next().value : void 0;
43786
+ if (target === null || lister === void 0) return logMcpCatalogRequest("not_ready");
43787
+ const admitted = this.mcpCatalogRate.admit(req.serverId);
43788
+ if (admitted !== "ok") return admitted === "in_flight" ? logMcpCatalogRequest("in_flight") : void 0;
43789
+ const work = this.outsideIngestCycle(() => this.answerMcpCatalogRequest(m.channelId, req, lister)).finally(() => {
43790
+ this.mcpCatalogRate.release(req.serverId);
43791
+ this.mcpCatalogWork.delete(work);
43792
+ });
43793
+ this.mcpCatalogWork.add(work);
43794
+ } catch {
43795
+ logMcpCatalogRequest("failed");
43796
+ } finally {
43797
+ this.ownerReplayPending.delete(m.messageId);
43798
+ }
43799
+ }
43800
+ /** The signed server's vendor and the bridge catalog row's issuer, when mcp-policy.json as WRITTEN is `ok` with it `ok`. */
43801
+ mcpCatalogTarget(serverId) {
43802
+ const doc = this.mcpPolicyWritten;
43803
+ if (doc === null || doc.state !== "ok" || !Object.prototype.hasOwnProperty.call(doc.servers, serverId)) return null;
43804
+ const s = doc.servers[serverId];
43805
+ if (s.state !== "ok" || s.vendor === void 0) return null;
43806
+ const catalog = this.mcpCatalog ?? MCP_CATALOG;
43807
+ if (!Object.prototype.hasOwnProperty.call(catalog, s.vendor)) return null;
43808
+ return { vendor: s.vendor, issuer: catalog[s.vendor].issuer };
43809
+ }
43810
+ /** Ask the runtime, re-check, build (384 KB cap) and send. Never throws. */
43811
+ async answerMcpCatalogRequest(channelId, req, lister) {
43812
+ try {
43813
+ const target = this.mcpCatalogTarget(req.serverId);
43814
+ if (target === null) return logMcpCatalogRequest("not_ready");
43815
+ const frame = await lister(req.serverId, MCP_CATALOG_LIST_TIMEOUT_MS);
43816
+ if (frame === null || frame === void 0) return logMcpCatalogRequest("runtime_timeout");
43817
+ const listed = validateCatalogListReply(frame, req.serverId, target.vendor);
43818
+ if (listed === null) return logMcpCatalogRequest("runtime_refused");
43819
+ const still = this.mcpCatalogTarget(req.serverId);
43820
+ if (still === null || still.vendor !== target.vendor) return logMcpCatalogRequest("not_ready");
43821
+ await this.sendMcpCatalog(channelId, buildMcpCatalogBody({ serverId: req.serverId, requestId: req.requestId, vendor: still.vendor, issuer: still.issuer, reply: listed }));
43822
+ logMcpCatalogRequest("sent");
43823
+ } catch (e) {
43824
+ logMcpCatalogRequest(e instanceof McpReplyWithheldError ? "withheld" : "send_failed");
43825
+ }
43826
+ }
43827
+ /**
43828
+ * The `mcp_catalog` reply: a system_event envelope with a body-level type (the sendSummary shape), on the shared
43829
+ * self-heal seam and the one publish chokepoint. H-D applies to EVERY attempt (it carries vendor-supplied tool
43830
+ * descriptions): after the seal, before the POST, the leaves at the SEALED group and epoch must be exactly the owner
43831
+ * and the agent, or the frame is withheld. One message id across the retry.
43832
+ */
43833
+ async sendMcpCatalog(channelId, body) {
43834
+ const joined = await this.reconcileJoin(channelId);
43835
+ if (!joined) throw new ChannelPendingError(channelId);
43836
+ const messageId = crypto.randomUUID();
43837
+ let attempt = 0;
43838
+ await this.sendWithSelfHealTerminal(
43839
+ this.provider,
43840
+ channelId,
43841
+ (epoch) => buildOutboundEnvelope({
43842
+ custody: this.custody,
43843
+ groupKey: this.provider,
43844
+ messageId,
43845
+ channelId,
43846
+ enclaveId: this.resolveEnclave(channelId),
43847
+ senderId: this.cfg.identityId,
43848
+ senderType: "agent",
43849
+ type: "system_event",
43850
+ bodyPlaintext: JSON.stringify(body),
43851
+ bindingId: "",
43852
+ bindingVersion: "0.0.0",
43853
+ agentSkillId: null,
43854
+ epoch,
43855
+ traceparent: ZERO_TRACEPARENT,
43856
+ references: [],
43857
+ stage5Outcome: "allow",
43858
+ stage5Evaluators: []
43859
+ }),
43860
+ async (envelope) => {
43861
+ attempt++;
43862
+ this.assertMcpReplyRelayFree(channelId, envelope, attempt);
43863
+ return this.publishEnvelope(channelId, () => this.transport.publish(envelope));
43864
+ }
43865
+ );
43866
+ }
42409
43867
  async handleSummarize(channelId, request) {
42410
43868
  const body = await runSummarize({
42411
43869
  request,
@@ -42437,13 +43895,13 @@ var BridgeAgent = class _BridgeAgent {
42437
43895
  throw new Error("summarize_no_runtime");
42438
43896
  }
42439
43897
  const messages = [{ role: "user", content: prompt }];
42440
- return new Promise((resolve, reject) => {
43898
+ return new Promise((resolve2, reject) => {
42441
43899
  const onAbort = () => reject(new Error("aborted"));
42442
43900
  signal.addEventListener("abort", onAbort);
42443
43901
  this.fastLlm.complete(messages).then(
42444
43902
  (text) => {
42445
43903
  signal.removeEventListener("abort", onAbort);
42446
- resolve(text);
43904
+ resolve2(text);
42447
43905
  },
42448
43906
  (err) => {
42449
43907
  signal.removeEventListener("abort", onAbort);
@@ -42916,6 +44374,11 @@ var BridgeAgent = class _BridgeAgent {
42916
44374
  new TextDecoder().decode(m.plaintext)
42917
44375
  );
42918
44376
  if (parsed.type && parsed.type !== "text") {
44377
+ if (parsed.type === "mcp_catalog_request" || parsed.type === "mcp_catalog") {
44378
+ if (parsed.type === "mcp_catalog_request") await this.acceptMcpCatalogRequest(m, parsed);
44379
+ acked.push(m.queueId);
44380
+ continue;
44381
+ }
42919
44382
  if (parsed.type === "summarize") {
42920
44383
  const request = parseSummarizeBody(parsed, this.cfg.identityId, writeServiceLog2);
42921
44384
  if (request !== null) {
@@ -43391,7 +44854,7 @@ var BridgeAgent = class _BridgeAgent {
43391
44854
  return got;
43392
44855
  }
43393
44856
  const cap = Math.min(waitSeconds, 50) * 1e3;
43394
- await new Promise((resolve) => {
44857
+ await new Promise((resolve2) => {
43395
44858
  let fired = false;
43396
44859
  let timer;
43397
44860
  const wake = () => {
@@ -43400,7 +44863,7 @@ var BridgeAgent = class _BridgeAgent {
43400
44863
  clearTimeout(timer);
43401
44864
  const i = this.waiters.indexOf(wake);
43402
44865
  if (i >= 0) this.waiters.splice(i, 1);
43403
- resolve();
44866
+ resolve2();
43404
44867
  };
43405
44868
  timer = setTimeout(wake, cap);
43406
44869
  this.waiters.push(wake);
@@ -43424,7 +44887,11 @@ var BridgeAgent = class _BridgeAgent {
43424
44887
  }
43425
44888
  /** Every buffer exit: re-narrow manifest grants (review H-1), then note the frame's attribution class (review H-3). */
43426
44889
  delivering(m) {
43427
- const out = this.renarrowManifestGrants(m);
44890
+ let out = this.renarrowManifestGrants(m);
44891
+ if (!this.noteMcpDelivered(out)) {
44892
+ out = withMcpCapability(out, false);
44893
+ writeServiceLog2("warn", "mcp.delivered_record_full");
44894
+ }
43428
44895
  this.noteDeliveredFrame(out);
43429
44896
  return out;
43430
44897
  }
@@ -43435,6 +44902,50 @@ var BridgeAgent = class _BridgeAgent {
43435
44902
  * audience that is still signed, else dropped. Only narrows; an unchanged message is returned by reference.
43436
44903
  */
43437
44904
  renarrowManifestGrants(m) {
44905
+ const narrowed = this.renarrowManifestGrantsBase(m);
44906
+ return this.renarrowMcpGrants(narrowed);
44907
+ }
44908
+ /**
44909
+ * MCP Task 16 (audit H-2; round 2 LOW), after the base re-narrowing: a buffered `mcp:` grant survives release only
44910
+ * when, NOW, the turn is still a plain owner-DM turn in the signed DM channel, the host switch reads "on", the
44911
+ * entitlement reading is in force, the name is not in skills.disabled, the mcp-policy.json document last written
44912
+ * lists the server ok with the tool, and a FRESH synchronous read of the DM's local leaves holds exactly the pinned
44913
+ * owner and the agent (MlsGroupKeyProvider.leafSnapshotCached: one `state` object, no await; null fails closed). Any
44914
+ * other mcp: grant is dropped, never kept as a fallback. relay_free is dropped whenever no mcp: grant survives.
44915
+ * Read-only: never touches the MLS group. Only narrows; returns `m` by reference when nothing changes.
44916
+ */
44917
+ renarrowMcpGrants(m) {
44918
+ const a = m.authorization;
44919
+ if (!a) return m;
44920
+ const isMcp = (g) => g.tool.startsWith(MCP_GRANT_PREFIX);
44921
+ if (!a.tool_grants.some(isMcp) && a.relay_free === void 0) return m;
44922
+ const pin = this.cfg.ownerGrantPin?.pin ?? null;
44923
+ const manifest = pin !== null ? this.toolGrantManifest : null;
44924
+ const turnOk = pin !== null && manifest !== null && a.audience === "owner_dm" && a.relay_speaker === void 0 && manifest.owner_dm_channel_id === m.channel_id && this.mcpHostSwitch === "on" && this.ownerDmManifestGrants && this.mcpEntitlementInForce() === "entitled" && this.ownerDmRelayFreeNow(m.channel_id, pin.ownerIdentityId);
44925
+ const kept = a.tool_grants.filter((g) => {
44926
+ if (!isMcp(g)) return true;
44927
+ const parsed = parseMcpGrant(g.tool);
44928
+ return turnOk && g.mode === "read" && g.source === "manifest" && parsed !== null && !this.payloadToolDeny.has(g.tool) && mcpPolicyAllows(this.mcpPolicyWritten, parsed.serverId, parsed.tool);
44929
+ });
44930
+ const keepRelayFree = a.relay_free === true && kept.some(isMcp);
44931
+ if (kept.length === a.tool_grants.length && keepRelayFree === (a.relay_free === true)) return m;
44932
+ const { relay_free: _relayFree, ...rest } = a;
44933
+ return { ...m, authorization: Object.freeze({ ...rest, tool_grants: Object.freeze(kept), ...keepRelayFree ? { relay_free: true } : {} }) };
44934
+ }
44935
+ /**
44936
+ * MCP Task 16 (OQ-2; audit H-2, round 2 H-D): a FRESH read of the channel's local leaf set holds exactly the pinned
44937
+ * owner and this agent. Synchronous (one manager `state`, so the leaves belong to one epoch); a cold manager or any
44938
+ * throw reads as NOT relay-free.
44939
+ */
44940
+ ownerDmRelayFreeNow(channelId, ownerIdentityId) {
44941
+ try {
44942
+ const snap = this.provider.leafSnapshotCached(channelId);
44943
+ return snap !== null && ownerDmLeavesRelayFree(snap.leaves, { ownerIdentityId, agentIdentityId: this.cfg.identityId });
44944
+ } catch {
44945
+ return false;
44946
+ }
44947
+ }
44948
+ renarrowManifestGrantsBase(m) {
43438
44949
  const a = m.authorization;
43439
44950
  if (!a || !a.tool_grants.some((g) => g.mode === "read" && g.source === "manifest")) return m;
43440
44951
  const audience = a.audience === "owner_dm" || a.audience === "team_room" ? a.audience : null;
@@ -43443,13 +44954,13 @@ var BridgeAgent = class _BridgeAgent {
43443
44954
  const bound = b !== null && b.state === "bound" && b.counter === this.toolGrantCounter;
43444
44955
  const switchOn = audience === "owner_dm" ? this.ownerDmManifestGrants : audience === "team_room" ? this.roomManifestGrants : false;
43445
44956
  const signedNow = new Set(audience && manifest ? manifest.grants[audience] ?? [] : []);
43446
- 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) : []);
44957
+ 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) : []);
43447
44958
  let changed = false;
43448
44959
  const out = a.tool_grants.flatMap((g) => {
43449
44960
  if (!(g.mode === "read" && g.source === "manifest")) return [g];
43450
44961
  if (switchOn && bound && signedNow.has(g.tool)) return [g];
43451
44962
  changed = true;
43452
- return constants4.has(g.tool) && signedNow.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
44963
+ return constants5.has(g.tool) && signedNow.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
43453
44964
  });
43454
44965
  if (!changed) return m;
43455
44966
  return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(out) }) };
@@ -43530,6 +45041,7 @@ var BridgeAgent = class _BridgeAgent {
43530
45041
  return;
43531
45042
  }
43532
45043
  if (!this.admitRuntimeDelivery(m)) return;
45044
+ this.noteMcpDelivered(m, true);
43533
45045
  const ch = m.channel_id;
43534
45046
  this.replayingChannels.add(ch);
43535
45047
  this.replyIntentReplays++;
@@ -43880,11 +45392,11 @@ ${channelId}`;
43880
45392
  if (!res.ok) return void 0;
43881
45393
  const rows = await res.json();
43882
45394
  if (!Array.isArray(rows)) return void 0;
43883
- const own = rows.find(
45395
+ const own2 = rows.find(
43884
45396
  (r) => r?.identity_id === this.cfg.identityId
43885
45397
  );
43886
- if (!own) return void 0;
43887
- return typeof own.paused_at === "string" && own.paused_at.length > 0;
45398
+ if (!own2) return void 0;
45399
+ return typeof own2.paused_at === "string" && own2.paused_at.length > 0;
43888
45400
  } catch (e) {
43889
45401
  writeServiceLog2("warn", "pause.probe_failed", {}, e);
43890
45402
  return void 0;
@@ -43917,6 +45429,10 @@ ${channelId}`;
43917
45429
  /** Complete the durable handoff only after the runtime explicitly acks. */
43918
45430
  ackRuntimeMessages(messageIds) {
43919
45431
  for (const id of messageIds) this.roomGrantEpochs.delete(id);
45432
+ for (const id of messageIds) {
45433
+ const r = this.mcpDeliveredTurns.get(id);
45434
+ if (r) r.acked = true;
45435
+ }
43920
45436
  for (const messageId of messageIds) {
43921
45437
  this.store.ackRuntimeInbox(messageId);
43922
45438
  this.runtimePendingIds.delete(messageId);
@@ -43998,6 +45514,7 @@ ${channelId}`;
43998
45514
  const message = rest;
43999
45515
  this.applyStagedMarks(message.message_id, marks);
44000
45516
  this.runtimePendingIds.add(message.message_id);
45517
+ this.noteMcpDelivered(message, true);
44001
45518
  if (this.cfg.ownerGrantPin?.pin && this.isOwnerVerifyFailed(message.message_id)) {
44002
45519
  this.ownerVerifyFailedIds.set(message.message_id, message.channel_id);
44003
45520
  }
@@ -44178,6 +45695,67 @@ ${channelId}`;
44178
45695
  clearSearchProvidersSocket(token) {
44179
45696
  if (this.searchProvidersSockets.delete(token)) this.onCapabilityMaybeChanged();
44180
45697
  }
45698
+ /** MCP Task 16 (spec §15): the same AND-over-sockets state for mcp_client_v1, kept per socket. */
45699
+ mcpClientSockets = /* @__PURE__ */ new Map();
45700
+ noteMcpClientSocket(token, declared) {
45701
+ this.mcpClientSockets.set(token, declared);
45702
+ this.onCapabilityMaybeChanged();
45703
+ }
45704
+ clearMcpClientSocket(token) {
45705
+ if (this.mcpClientSockets.delete(token)) this.onCapabilityMaybeChanged();
45706
+ }
45707
+ /** MCP Task 18: ws-server registers (or, with null, removes) a socket's loopback catalog lister. */
45708
+ noteMcpCatalogLister(token, lister) {
45709
+ if (lister === null) this.mcpCatalogListers.delete(token);
45710
+ else this.mcpCatalogListers.set(token, lister);
45711
+ }
45712
+ /**
45713
+ * MCP Task 19 (spec §12.3): "ok" when every attached runtime socket declared mcp_client_v1 (the AND, which harness
45714
+ * sockets hold false: a harness socket counts in the AND and never declares it); else "harness_consumer" when the
45715
+ * latest hello was a harness; else "missing".
45716
+ */
45717
+ reportedMcpCapability() {
45718
+ if (this.mcpClientCapable()) return "ok";
45719
+ return this.lastConsumerCapability === "harness_consumer" ? "harness_consumer" : "missing";
45720
+ }
45721
+ /**
45722
+ * MCP Task 19 (spec §12.3): at most 8 `{server_id, vendor, state}` for the signed servers of the manifest in force,
45723
+ * from mcp-policy.json as WRITTEN. Only the states the bridge itself knows: top `kill_switch` and `not_entitled` give
45724
+ * that state per server; top `ok` gives `ok` for a server the bridge lists `ok` and `malformed` for one it refused.
45725
+ * Every other top state (no capability, suspended, no config, switch off, section absent, malformed section) gives no
45726
+ * row. The runtime-only states (needs_login, login_expired, definition_changed, server_version_unsupported,
45727
+ * server_unreachable) have no runtime-to-bridge report path in Phase 1. `vendor` is reported only when it is a key of
45728
+ * the bridge's compiled catalog; a signed vendor outside it gives no row. Never a tool name, URL or issuer.
45729
+ */
45730
+ reportedMcpServers() {
45731
+ const doc = this.mcpPolicyWritten;
45732
+ const signed = (this.cfg.ownerGrantPin?.pin ?? null) !== null ? this.toolGrantManifest?.mcp?.servers : void 0;
45733
+ if (doc === null || signed === void 0) return [];
45734
+ const catalog = this.mcpCatalog ?? MCP_CATALOG;
45735
+ const out = [];
45736
+ for (const id of Object.keys(signed).sort()) {
45737
+ if (out.length >= 8) break;
45738
+ const vendor = signed[id].vendor;
45739
+ if (!Object.prototype.hasOwnProperty.call(catalog, vendor)) continue;
45740
+ let state;
45741
+ if (doc.state === "kill_switch" || doc.state === "not_entitled") state = doc.state;
45742
+ else if (doc.state === "ok") state = Object.prototype.hasOwnProperty.call(doc.servers, id) && doc.servers[id].state === "ok" ? "ok" : "malformed";
45743
+ else continue;
45744
+ out.push({ server_id: id, vendor, state });
45745
+ }
45746
+ return out;
45747
+ }
45748
+ /**
45749
+ * MCP Task 16: every attached runtime socket declared mcp_client_v1; no socket attached means absent (false).
45750
+ * PR-B2 follow-up (documented): a HARNESS socket counts in this AND. ws-server notes every socket that said hello,
45751
+ * and a harness hello is always noted `false` (harnessClaimed never resets), so one attached harness connector holds
45752
+ * the capability false for files, the report and mcp-policy.json (top `no_capability`) while it is attached.
45753
+ */
45754
+ mcpClientCapable() {
45755
+ if (this.mcpClientSockets.size === 0) return false;
45756
+ for (const v of this.mcpClientSockets.values()) if (!v) return false;
45757
+ return true;
45758
+ }
44181
45759
  noteUtilityToolsSocket(token, declared) {
44182
45760
  this.utilityToolsSockets.set(token, declared);
44183
45761
  this.onCapabilityMaybeChanged();
@@ -44196,6 +45774,7 @@ ${channelId}`;
44196
45774
  const out = /* @__PURE__ */ new Set();
44197
45775
  if (this.searchProvidersCapable()) out.add("search_providers_v2");
44198
45776
  if (this.utilityToolsCapable()) out.add("utility_tools_v1");
45777
+ if (this.mcpClientCapable()) out.add("mcp_client_v1");
44199
45778
  return out;
44200
45779
  }
44201
45780
  /** D28: every attached runtime socket declared search_providers_v2; no socket attached means absent (false). */
@@ -44224,11 +45803,12 @@ ${channelId}`;
44224
45803
  * switches and the new capability, then a debounced refetch (one per burst) only when needed.
44225
45804
  */
44226
45805
  onCapabilityMaybeChanged() {
44227
- const next = { search: this.searchProvidersCapable(), utility: this.utilityToolsCapable() };
45806
+ const next = { search: this.searchProvidersCapable(), utility: this.utilityToolsCapable(), mcp: this.mcpClientCapable() };
44228
45807
  const prev = this.capabilityState;
44229
- if (prev !== null && prev.search === next.search && prev.utility === next.utility) return;
45808
+ if (prev !== null && prev.search === next.search && prev.utility === next.utility && prev.mcp === next.mcp) return;
44230
45809
  this.capabilityState = next;
44231
45810
  this.recomputeManifestPolicy(this.capabilityFeatures());
45811
+ this.recomputeMcpPolicy();
44232
45812
  if (this.closingDown) return;
44233
45813
  if (prev === null && !next.search && !next.utility) return;
44234
45814
  const now = Date.now();
@@ -44266,7 +45846,7 @@ ${channelId}`;
44266
45846
  }
44267
45847
  const withheld = new Set(Object.keys(TOOL_FEATURE).filter((t) => !toolAllowedByFeatures(t, features)));
44268
45848
  if (withheld.size === 0) return;
44269
- const path = join17(runtimeConfigDir(this.home, this.cfg.identityId), MANIFEST_POLICY_FILE);
45849
+ const path = join18(runtimeConfigDir(this.home, this.cfg.identityId), MANIFEST_POLICY_FILE);
44270
45850
  if (!existsSync5(path)) return;
44271
45851
  let doc;
44272
45852
  try {
@@ -44474,6 +46054,7 @@ ${channelId}`;
44474
46054
  async closeOnce() {
44475
46055
  this.closingDown = true;
44476
46056
  this.statusGate.close();
46057
+ this.toolStatusSender.close();
44477
46058
  for (const t of this.pauseRecheckTimers.values()) clearTimeout(t);
44478
46059
  this.pauseRecheckTimers.clear();
44479
46060
  for (const t of this.supersedeRecheckTimers.values()) clearTimeout(t);
@@ -44482,6 +46063,8 @@ ${channelId}`;
44482
46063
  this.redeliveryTimer = null;
44483
46064
  if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
44484
46065
  this.searchCapabilityRefetchTimer = null;
46066
+ if (this.mcpPolicyRetryTimer !== null) clearTimeout(this.mcpPolicyRetryTimer);
46067
+ this.mcpPolicyRetryTimer = null;
44485
46068
  this.pingHandle?.close();
44486
46069
  if (this.safetyTimer) clearInterval(this.safetyTimer);
44487
46070
  if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
@@ -44491,8 +46074,8 @@ ${channelId}`;
44491
46074
  let timer;
44492
46075
  await Promise.race([
44493
46076
  ingest.released,
44494
- new Promise((resolve) => {
44495
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
46077
+ new Promise((resolve2) => {
46078
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44496
46079
  })
44497
46080
  ]);
44498
46081
  if (timer) clearTimeout(timer);
@@ -44502,8 +46085,8 @@ ${channelId}`;
44502
46085
  let timer;
44503
46086
  await Promise.race([
44504
46087
  starting,
44505
- new Promise((resolve) => {
44506
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
46088
+ new Promise((resolve2) => {
46089
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44507
46090
  })
44508
46091
  ]);
44509
46092
  if (timer) clearTimeout(timer);
@@ -44512,8 +46095,8 @@ ${channelId}`;
44512
46095
  let timer;
44513
46096
  await Promise.race([
44514
46097
  this.membershipSweepWork,
44515
- new Promise((resolve) => {
44516
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
46098
+ new Promise((resolve2) => {
46099
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44517
46100
  })
44518
46101
  ]);
44519
46102
  if (timer) clearTimeout(timer);
@@ -44523,8 +46106,8 @@ ${channelId}`;
44523
46106
  let timer;
44524
46107
  await Promise.race([
44525
46108
  this.recallPruneWork,
44526
- new Promise((resolve) => {
44527
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
46109
+ new Promise((resolve2) => {
46110
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44528
46111
  })
44529
46112
  ]);
44530
46113
  if (timer) clearTimeout(timer);
@@ -44534,8 +46117,8 @@ ${channelId}`;
44534
46117
  let timer;
44535
46118
  await Promise.race([
44536
46119
  this.recallMaintenanceWork,
44537
- new Promise((resolve) => {
44538
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
46120
+ new Promise((resolve2) => {
46121
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44539
46122
  })
44540
46123
  ]);
44541
46124
  if (timer) clearTimeout(timer);
@@ -44547,8 +46130,8 @@ ${channelId}`;
44547
46130
  let timer;
44548
46131
  await Promise.race([
44549
46132
  Promise.allSettled([...this.recallTapWork]),
44550
- new Promise((resolve) => {
44551
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
46133
+ new Promise((resolve2) => {
46134
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44552
46135
  })
44553
46136
  ]);
44554
46137
  if (timer) clearTimeout(timer);
@@ -44557,8 +46140,8 @@ ${channelId}`;
44557
46140
  let timer;
44558
46141
  await Promise.race([
44559
46142
  Promise.allSettled([...this.unifiedWork]),
44560
- new Promise((resolve) => {
44561
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
46143
+ new Promise((resolve2) => {
46144
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44562
46145
  })
44563
46146
  ]);
44564
46147
  if (timer) clearTimeout(timer);
@@ -44567,8 +46150,8 @@ ${channelId}`;
44567
46150
  let timer;
44568
46151
  await Promise.race([
44569
46152
  this.recallPrewarmWork,
44570
- new Promise((resolve) => {
44571
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
46153
+ new Promise((resolve2) => {
46154
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44572
46155
  })
44573
46156
  ]);
44574
46157
  if (timer) clearTimeout(timer);
@@ -44578,8 +46161,8 @@ ${channelId}`;
44578
46161
  let timer;
44579
46162
  await Promise.race([
44580
46163
  Promise.allSettled([...this.replyWork]),
44581
- new Promise((resolve) => {
44582
- timer = setTimeout(resolve, _BridgeAgent.GRACEFUL_DRAIN_MS);
46164
+ new Promise((resolve2) => {
46165
+ timer = setTimeout(resolve2, _BridgeAgent.GRACEFUL_DRAIN_MS);
44583
46166
  })
44584
46167
  ]);
44585
46168
  if (timer) clearTimeout(timer);
@@ -44592,7 +46175,7 @@ ${channelId}`;
44592
46175
 
44593
46176
  // src/config.ts
44594
46177
  import { readFileSync as readFileSync11, statSync as statSync5 } from "node:fs";
44595
- import { dirname as dirname6, join as join18 } from "node:path";
46178
+ import { dirname as dirname7, join as join19 } from "node:path";
44596
46179
  var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
44597
46180
  var CONFIG_ERROR_MESSAGE = {
44598
46181
  credential_not_found: "credential file not found",
@@ -44630,7 +46213,7 @@ function loadConfig(opts) {
44630
46213
  throw new ConfigError("credential_not_json");
44631
46214
  }
44632
46215
  for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new ConfigError("credential_missing_field", k);
44633
- const storePath = opts.storePath ?? join18(dirname6(opts.credentialPath), "bridge-state.db");
46216
+ const storePath = opts.storePath ?? join19(dirname7(opts.credentialPath), "bridge-state.db");
44634
46217
  const ownerGrantPin = loadOwnerGrantPin({ credentialPath: opts.credentialPath, credentialJson: json2 });
44635
46218
  writeServiceLog2(ownerGrantPin.state === "ok" || ownerGrantPin.state === "no_pin" ? "info" : "warn", "authz.pin", {
44636
46219
  state: ServiceLogEnum.of(ownerGrantPin.state, ["ok", "no_pin", "pin_conflict", "malformed", "unsupported_algo", "symlink_refused", "unreadable"])
@@ -44818,6 +46401,10 @@ var BRIDGE_ERRORS = {
44818
46401
  // Weather/currency Task 17 (OQ-P1): a send frame's credit is not the closed value, or rides an a2a frame. TERMINAL:
44819
46402
  // the inbound is retired, so the model (and the weather call) never runs again for it.
44820
46403
  reply_credit_unknown: { http: 400, message: "reply credit not recognised; not sent" },
46404
+ // Security fix 2026-10-30: an upload route was given a path that is not a
46405
+ // regular file directly inside the bridge's vault outbox (`vault_outbox` on
46406
+ // /healthz). Fixed and content-free: the path is never echoed or logged.
46407
+ path_outside_outbox: { http: 400, message: "uploads must come from the bridge vault outbox; copy the file there first" },
44821
46408
  internal: { http: 500, message: "internal error" }
44822
46409
  };
44823
46410
  function errorBody(code, detail) {
@@ -44832,63 +46419,6 @@ function wsErrorFrame(code, detail) {
44832
46419
  return { type: "error", ...errorBody(code, detail) };
44833
46420
  }
44834
46421
 
44835
- // src/consumer-grants.ts
44836
- var NO_GRANTS = Object.freeze([]);
44837
- var PER_TOOL_GRANTS_FEATURE = "per_tool_grants";
44838
- function withConsumerGrants(m, perToolConsumer) {
44839
- if (perToolConsumer) return m;
44840
- const a = m.authorization;
44841
- if (!a) return m;
44842
- const stripGrants = !(a.tool_grants.length === 0 || a.audience === "owner_dm" || a.relay_speaker === "owner");
44843
- const stripMemory = "memory_write" in a;
44844
- if (!stripGrants && !stripMemory) return m;
44845
- const { memory_write: _dropped, ...rest } = a;
44846
- return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
44847
- }
44848
- var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
44849
- var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
44850
- var UTILITY_TOOLS_FEATURE = "utility_tools_v1";
44851
- var CONSTANT_TOOL_NAMES = Object.freeze({
44852
- owner_dm: Object.freeze(["current_time", "web_search"]),
44853
- team_room: Object.freeze(["current_time"])
44854
- });
44855
- var isManifestGrant = (g) => g.mode === "read" && g.source === "manifest";
44856
- function withManifestCapability(m, manifestConsumer) {
44857
- if (manifestConsumer) return m;
44858
- const a = m.authorization;
44859
- if (!a || !a.tool_grants.some(isManifestGrant)) return m;
44860
- const constants4 = new Set(a.audience === "owner_dm" || a.audience === "team_room" ? CONSTANT_TOOL_NAMES[a.audience] : []);
44861
- const narrowed = a.tool_grants.flatMap((g) => {
44862
- if (!isManifestGrant(g)) return [g];
44863
- return constants4.has(g.tool) ? [Object.freeze({ tool: g.tool, mode: "read" })] : [];
44864
- });
44865
- return { ...m, authorization: Object.freeze({ ...a, tool_grants: Object.freeze(narrowed) }) };
44866
- }
44867
- function withHarnessGrants(m, withheld) {
44868
- if (!withheld) return m;
44869
- const a = m.authorization;
44870
- if (!a || a.tool_grants.length === 0) return m;
44871
- return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
44872
- }
44873
- function withSearchCapability(m, consumerHasFeature, signedProvider) {
44874
- if (consumerHasFeature) return m;
44875
- const a = m.authorization;
44876
- if (!a) return m;
44877
- const stripSearch = signedProvider !== void 0 && signedProvider !== "brave";
44878
- const kept = a.tool_grants.filter((g) => g.tool !== "web_extract" && !(stripSearch && g.tool === "web_search"));
44879
- if (kept.length === a.tool_grants.length) return m;
44880
- return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
44881
- }
44882
- var UTILITY_TOOL_NAMES = /* @__PURE__ */ new Set(["weather_forecast", "currency_convert"]);
44883
- function withUtilityCapability(m, consumerHasFeature) {
44884
- if (consumerHasFeature) return m;
44885
- const a = m.authorization;
44886
- if (!a) return m;
44887
- const kept = a.tool_grants.filter((g) => !UTILITY_TOOL_NAMES.has(g.tool));
44888
- if (kept.length === a.tool_grants.length) return m;
44889
- return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
44890
- }
44891
-
44892
46422
  // src/http-server.ts
44893
46423
  function writeSse(res, msgs, onWriteError) {
44894
46424
  for (let i = 0; i < msgs.length; i++) {
@@ -44911,7 +46441,8 @@ data: ${JSON.stringify(msgs[i])}
44911
46441
  function deliverSseBatch(res, batch, agent, onWriteError) {
44912
46442
  const withheld = agent.harnessGrantsWithheld();
44913
46443
  const provider = agent.signedSearchProvider();
44914
- const left3 = writeSse(res, batch.map((m) => withUtilityCapability(withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider), false)), onWriteError);
46444
+ const hold = agent.r3Hold();
46445
+ const left3 = writeSse(res, batch.map((m) => withR3Holds(withMcpCapability(withUtilityCapability(withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider), false), false), hold)), onWriteError);
44915
46446
  const leftIds = new Set(left3.map((message) => message.message_id));
44916
46447
  agent.ackRuntimeMessages(batch.filter((message) => !leftIds.has(message.message_id)).map((message) => message.message_id));
44917
46448
  if (left3.length) agent.requeueFront(batch.filter((message) => leftIds.has(message.message_id)));
@@ -45018,7 +46549,7 @@ function jsonError(res, code, detail) {
45018
46549
  json(res, httpStatus(code), errorBody(code, detail));
45019
46550
  }
45020
46551
  function readBody(req, cap) {
45021
- return new Promise((resolve, reject) => {
46552
+ return new Promise((resolve2, reject) => {
45022
46553
  let size = 0;
45023
46554
  let done = false;
45024
46555
  const chunks = [];
@@ -45038,7 +46569,7 @@ function readBody(req, cap) {
45038
46569
  req.on("end", () => {
45039
46570
  if (!done) {
45040
46571
  done = true;
45041
- resolve(Buffer.concat(chunks).toString("utf8"));
46572
+ resolve2(Buffer.concat(chunks).toString("utf8"));
45042
46573
  }
45043
46574
  });
45044
46575
  req.on("error", (e) => {
@@ -45116,6 +46647,9 @@ function createHttpServer(agent, opts) {
45116
46647
  }
45117
46648
  return json(res, 200, r);
45118
46649
  }
46650
+ if (method === "GET" && path === "/v1/vault-outbox") {
46651
+ return json(res, 200, { path: agent.vaultOutboxPath() });
46652
+ }
45119
46653
  if (method === "POST" && path === "/v1/send-attachment") {
45120
46654
  const parsed = sendAttachmentSchema.safeParse(JSON.parse(await readBody(req, OTHER_CAP)));
45121
46655
  if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
@@ -45185,7 +46719,8 @@ function createHttpServer(agent, opts) {
45185
46719
  const drained = await agent.drain(channel, wait);
45186
46720
  const withheld = agent.harnessGrantsWithheld();
45187
46721
  const provider = agent.signedSearchProvider();
45188
- return json(res, 200, drained.map((m) => withUtilityCapability(withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider), false)));
46722
+ const hold = agent.r3Hold();
46723
+ return json(res, 200, drained.map((m) => withR3Holds(withMcpCapability(withUtilityCapability(withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider), false), false), hold)));
45189
46724
  }
45190
46725
  if (method === "GET" && path === "/v1/events") {
45191
46726
  const channel = url.searchParams.get("channel_id") ?? void 0;
@@ -45227,6 +46762,10 @@ function createHttpServer(agent, opts) {
45227
46762
  if (e instanceof SendForbiddenError) return jsonError(res, "send_forbidden");
45228
46763
  if (e instanceof GroupSupersededSendError) return jsonError(res, "group_superseded");
45229
46764
  if (e instanceof IdempotencyConflictError) return jsonError(res, "invalid_request", e.message);
46765
+ if (e instanceof PathOutsideOutboxError) {
46766
+ writeServiceLog2("warn", "vault_outbox.refused");
46767
+ return jsonError(res, "path_outside_outbox");
46768
+ }
45230
46769
  if (e instanceof InvalidPathError) return jsonError(res, "invalid_request", e.message);
45231
46770
  if (e instanceof VaultPinMismatchError) return jsonError(res, "vault_ref_changed", { pin: e.pin });
45232
46771
  if (e instanceof VaultIntegrityError) return jsonError(res, "vault_integrity_failed", e.message);
@@ -45251,6 +46790,7 @@ function parseHarnessReport(raw) {
45251
46790
  }
45252
46791
 
45253
46792
  // src/ws-server.ts
46793
+ import { randomBytes as randomBytes12 } from "node:crypto";
45254
46794
  import { WebSocketServer, WebSocket } from "ws";
45255
46795
 
45256
46796
  // src/memory/consumer-notes.ts
@@ -45361,6 +46901,8 @@ function attachAgentWebSocket(server, agent, opts) {
45361
46901
  manifestGrants: false,
45362
46902
  searchProviders: false,
45363
46903
  utilityTools: false,
46904
+ mcpClient: false,
46905
+ pendingCatalog: /* @__PURE__ */ new Map(),
45364
46906
  // helloWaitMs 0 keeps today's synchronous initial flush exactly (the opt-out the existing tests use).
45365
46907
  deliveryOpen: helloWaitMs <= 0,
45366
46908
  deliveryToken: /* @__PURE__ */ new Map(),
@@ -45414,14 +46956,14 @@ function attachAgentWebSocket(server, agent, opts) {
45414
46956
  continue;
45415
46957
  }
45416
46958
  const out = withConsumerNotes(
45417
- withUtilityCapability(withSearchCapability(
46959
+ withR3Holds(withMcpCapability(withUtilityCapability(withSearchCapability(
45418
46960
  withManifestCapability(
45419
46961
  withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
45420
46962
  session.manifestGrants
45421
46963
  ),
45422
46964
  session.searchProviders,
45423
46965
  agent.signedSearchProvider()
45424
- ), session.utilityTools),
46966
+ ), session.utilityTools), session.mcpClient), agent.r3Hold()),
45425
46967
  batch[i],
45426
46968
  session,
45427
46969
  agent
@@ -45479,6 +47021,10 @@ function attachAgentWebSocket(server, agent, opts) {
45479
47021
  if (session.manifestGrants) agent.clearManifestConsumer(session);
45480
47022
  agent.clearSearchProvidersSocket(session);
45481
47023
  agent.clearUtilityToolsSocket(session);
47024
+ agent.clearMcpClientSocket(session);
47025
+ agent.noteMcpCatalogLister(session, null);
47026
+ for (const settle of session.pendingCatalog.values()) settle(null);
47027
+ session.pendingCatalog.clear();
45482
47028
  };
45483
47029
  current = { sock, cleanup, sweepStale };
45484
47030
  sock.on("close", cleanup);
@@ -45512,6 +47058,32 @@ function attachAgentWebSocket(server, agent, opts) {
45512
47058
  }
45513
47059
  };
45514
47060
  }
47061
+ var MCP_CATALOG_PENDING_MAX = 8;
47062
+ function mcpCatalogLister(sock, session) {
47063
+ return (serverId, timeoutMs) => new Promise((resolve2) => {
47064
+ if (session.closed || sock.readyState !== WebSocket.OPEN || session.pendingCatalog.size >= MCP_CATALOG_PENDING_MAX) {
47065
+ resolve2(null);
47066
+ return;
47067
+ }
47068
+ const requestId = randomBytes12(16).toString("hex");
47069
+ const timer = setTimeout(() => {
47070
+ session.pendingCatalog.delete(requestId);
47071
+ resolve2(null);
47072
+ }, timeoutMs);
47073
+ timer.unref?.();
47074
+ session.pendingCatalog.set(requestId, (frame) => {
47075
+ clearTimeout(timer);
47076
+ resolve2(frame);
47077
+ });
47078
+ try {
47079
+ sock.send(JSON.stringify({ type: "mcp_catalog_list", request_id: requestId, server_id: serverId }));
47080
+ } catch {
47081
+ session.pendingCatalog.delete(requestId);
47082
+ clearTimeout(timer);
47083
+ resolve2(null);
47084
+ }
47085
+ });
47086
+ }
45515
47087
  function sendJson(sock, body) {
45516
47088
  if (sock.readyState !== WebSocket.OPEN) return;
45517
47089
  try {
@@ -45585,6 +47157,9 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
45585
47157
  agent.noteSearchProvidersSocket(session, session.searchProviders);
45586
47158
  session.utilityTools = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(UTILITY_TOOLS_FEATURE);
45587
47159
  agent.noteUtilityToolsSocket(session, session.utilityTools);
47160
+ session.mcpClient = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(MANIFEST_GRANTS_FEATURE) && m.features.includes(MCP_CLIENT_FEATURE);
47161
+ agent.noteMcpClientSocket(session, session.mcpClient);
47162
+ agent.noteMcpCatalogLister(session, session.mcpClient ? mcpCatalogLister(sock, session) : null);
45588
47163
  if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
45589
47164
  agent.noteNotesConsumer();
45590
47165
  }
@@ -45592,13 +47167,21 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
45592
47167
  if (session.recallConsumer) agent.noteRecallConsumer();
45593
47168
  session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
45594
47169
  if (session.unifiedConsumer) agent.noteUnifiedConsumer();
45595
- 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] });
47170
+ 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, MCP_POLICY_WRITER_FEATURE] });
45596
47171
  if (!session.deliveryOpen) {
45597
47172
  session.deliveryOpen = true;
45598
47173
  flush();
45599
47174
  }
45600
47175
  return;
45601
47176
  }
47177
+ if (m.type === "mcp_catalog_list_result") {
47178
+ const id = m.request_id;
47179
+ const settle = typeof id === "string" ? session.pendingCatalog.get(id) : void 0;
47180
+ if (settle === void 0) return;
47181
+ session.pendingCatalog.delete(id);
47182
+ settle(m);
47183
+ return;
47184
+ }
45602
47185
  if (m.type === "nack") {
45603
47186
  if (typeof m.message_id !== "string" || !m.message_id) {
45604
47187
  sendJson(sock, wsErrorFrame("invalid_request"));