@securecoms/agent-bridge 0.30.2 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -7556,7 +7556,7 @@ __export(import_bundle_exports, {
7556
7556
  importModelBundle: () => importModelBundle
7557
7557
  });
7558
7558
  import { createHash as createHash6, randomBytes as randomBytes12 } from "node:crypto";
7559
- import { readFileSync as readFileSync15, statSync as statSync7, writeFileSync as writeFileSync4 } from "node:fs";
7559
+ import { readFileSync as readFileSync16, statSync as statSync7, writeFileSync as writeFileSync4 } from "node:fs";
7560
7560
  import { join as join24 } from "node:path";
7561
7561
  async function importModelBundle(srcDir, root3, m) {
7562
7562
  const verified = [];
@@ -7574,7 +7574,7 @@ async function importModelBundle(srcDir, root3, m) {
7574
7574
  if (size !== f.size) throw mismatch;
7575
7575
  let bytes;
7576
7576
  try {
7577
- bytes = readFileSync15(src);
7577
+ bytes = readFileSync16(src);
7578
7578
  } catch {
7579
7579
  throw new ModelImportError(`${f.name} is missing from the import directory`);
7580
7580
  }
@@ -7742,7 +7742,9 @@ function settingsToVoiceConfig(s, proxy) {
7742
7742
  model: "securecoms-fast",
7743
7743
  apiKey: proxy.apiKey,
7744
7744
  timeoutMs: 2500,
7745
- maxTokens: 80
7745
+ maxTokens: 80,
7746
+ // Spec 7.2: tells the proxy this bridge scopes voice turns. Sent to the SecureComs proxy only.
7747
+ headers: { "x-securecoms-bridge-caps": "voice-scope-v1" }
7746
7748
  },
7747
7749
  settings: s
7748
7750
  };
@@ -7750,6 +7752,7 @@ function settingsToVoiceConfig(s, proxy) {
7750
7752
 
7751
7753
  // src/voice/persona.ts
7752
7754
  var CONTINUITY_CLAUSE = "The text messages in this chat and this voice call are one and the same ongoing conversation, and the earlier turns you can see ARE those messages. If asked whether you remember or received a message, answer from that history \u2014 never say you cannot access previous messages.";
7755
+ var VOICE_CALL_CLAUSE = "This is a live voice call. The earlier turns you can see are from this call only; during a call you cannot see the typed chat, saved notes or memory. If asked about a message, file or note you cannot see, say plainly that you can't see the chat while on a call and offer to pick it up in the chat afterwards. Never guess at what it said.";
7753
7756
  var schema = z2.object({
7754
7757
  persona: z2.object({
7755
7758
  display_name: z2.string().min(1).optional(),
@@ -29882,7 +29885,7 @@ function loadPackageMeta(importMetaUrl) {
29882
29885
  }
29883
29886
 
29884
29887
  // src/bridge-agent.ts
29885
- import { existsSync as existsSync5, promises as fsp, statSync as statSync5 } from "node:fs";
29888
+ import { existsSync as existsSync5, promises as fsp, readFileSync as readFileSync11, statSync as statSync5 } from "node:fs";
29886
29889
  import { join as join19, dirname as dirname7 } from "node:path";
29887
29890
  import { fileURLToPath as fileURLToPath2 } from "node:url";
29888
29891
 
@@ -34056,7 +34059,8 @@ var FastLlmClient = class {
34056
34059
  try {
34057
34060
  res = await this.fetchImpl(`${this.cfg.baseUrl}${this.cfg.path ?? "/chat/completions"}`, {
34058
34061
  method: "POST",
34059
- headers: { "content-type": "application/json", ...this.key ? { authorization: `Bearer ${this.key}` } : {} },
34062
+ // Final fix T8: configured headers first, so they can never displace content-type or the bearer.
34063
+ headers: { ...this.cfg.headers, "content-type": "application/json", ...this.key ? { authorization: `Bearer ${this.key}` } : {} },
34060
34064
  body: JSON.stringify({ model: this.cfg.model, messages, max_tokens: this.cfg.maxTokens, temperature: 0.4 }),
34061
34065
  signal: ac.signal
34062
34066
  });
@@ -34160,7 +34164,7 @@ function buildFastMessages(args2) {
34160
34164
  const sp = systemPrompt(args2.persona, args2.settings, args2.agentName);
34161
34165
  const head = sp.slice(0, sp.length - CONTINUITY_CLAUSE.length).trimEnd();
34162
34166
  const notesLine = args2.notesDeferLine ? ` ${FAST_NOTES_DEFER_LINE}` : "";
34163
- const system = `${head} ${args2.guidance.trim()}${notesLine} ${FAST_DEFER_RULE} ${CONTINUITY_CLAUSE}`;
34167
+ const system = `${head} ${args2.guidance.trim()}${notesLine} ${FAST_DEFER_RULE} ${VOICE_CALL_CLAUSE}`;
34164
34168
  const recent = args2.memory.recent.map((t) => ({
34165
34169
  role: t.role === "agent" ? "assistant" : "user",
34166
34170
  content: t.text
@@ -34197,6 +34201,160 @@ function normalizeFastReply(raw, maxChars = 200, maxSentences = 2) {
34197
34201
  return sentences.slice(0, maxSentences).join(" ").slice(0, maxChars);
34198
34202
  }
34199
34203
 
34204
+ // src/voice/voice-scope.ts
34205
+ function isVoiceScopedTurn(t) {
34206
+ return t.speaker !== void 0 || t.transcribed === true || t.senderIsRelay || t.text.trim() === VOICE_CALL_START_MARKER;
34207
+ }
34208
+ function admitsToCallBuffer(t) {
34209
+ return t.speaker !== void 0 || t.transcribed === true;
34210
+ }
34211
+
34212
+ // src/voice/call-buffer.ts
34213
+ var CALL_BUFFER_CAP = 50;
34214
+ var CALL_BUFFER_IDLE_MS = 30 * 6e4;
34215
+ var CALL_BUFFER_ID_CAP = 5e3;
34216
+ var CallBuffer = class {
34217
+ constructor(opts) {
34218
+ this.opts = opts;
34219
+ }
34220
+ channels = /* @__PURE__ */ new Map();
34221
+ /** messageId -> channelId, insertion ordered (oldest first) for the cap. */
34222
+ voiceIds = /* @__PURE__ */ new Map();
34223
+ /** messageId -> { channelId, the channel's bufSeq at the mark, the channel's generation then }. */
34224
+ seqNotes = /* @__PURE__ */ new Map();
34225
+ /**
34226
+ * Final fix I-1: messageId -> { channelId, the channel's bufSeq just before the turn's own peer entry }. A row the
34227
+ * backend redelivers (a failed ack, runtime_inbox.full backpressure) is ingested again; its id here means its text is
34228
+ * already in the call, so it is not appended again and its snapshot stops before its own entry (A-6).
34229
+ */
34230
+ admitted = /* @__PURE__ */ new Map();
34231
+ /** The last bufSeq handed out per channel (kept across resets, so the sequence never runs backwards). */
34232
+ lastSeq = /* @__PURE__ */ new Map();
34233
+ nextSeq = 1;
34234
+ /** Per-channel reset generation (advanced by every reset, the idle rule included). */
34235
+ gens = /* @__PURE__ */ new Map();
34236
+ /**
34237
+ * Append a spoken turn's text, once per turn id (final fix I-1). Returns false (and appends nothing) when the id was
34238
+ * already admitted in this call, or when the buffer is disabled.
34239
+ */
34240
+ appendPeer(channelId, text, messageId) {
34241
+ if (!this.opts.enabled) return false;
34242
+ if (this.admittedAt(messageId) !== void 0) return false;
34243
+ const before = this.seqAt(channelId);
34244
+ this.append(channelId, { role: "peer", text });
34245
+ this.admitted.set(messageId, { channelId, before });
34246
+ if (this.admitted.size > CALL_BUFFER_ID_CAP) this.admitted.delete(this.admitted.keys().next().value);
34247
+ return true;
34248
+ }
34249
+ /**
34250
+ * Final fix I-1: for a turn already admitted in this call, the channel's bufSeq just before its own entry (the end seq
34251
+ * of its first snapshot); undefined for a turn not admitted (or forgotten by a reset, the idle rule or the id cap).
34252
+ */
34253
+ admittedAt(messageId) {
34254
+ const a = this.admitted.get(messageId);
34255
+ if (a === void 0) return void 0;
34256
+ this.expireIfIdle(a.channelId);
34257
+ return this.admitted.get(messageId)?.before;
34258
+ }
34259
+ appendAgent(channelId, text, origin) {
34260
+ this.append(channelId, { role: "agent", text, origin });
34261
+ }
34262
+ /**
34263
+ * Record a voice-scoped turn's id (for reply admission only, A-4) and note the channel's seq at this point. Final fix
34264
+ * I-1: a turn already admitted (a redelivered row) keeps the seq of its first admission, so the note never moves past
34265
+ * the turn's own entry.
34266
+ */
34267
+ markVoiceScoped(channelId, messageId) {
34268
+ if (!this.opts.enabled) return;
34269
+ this.expireIfIdle(channelId);
34270
+ this.voiceIds.delete(messageId);
34271
+ this.voiceIds.set(messageId, channelId);
34272
+ if (this.voiceIds.size > CALL_BUFFER_ID_CAP) this.voiceIds.delete(this.voiceIds.keys().next().value);
34273
+ const seq = this.admittedAt(messageId) ?? this.seqAt(channelId);
34274
+ this.seqNotes.delete(messageId);
34275
+ this.seqNotes.set(messageId, { channelId, seq, generation: this.generation(channelId) });
34276
+ if (this.seqNotes.size > CALL_BUFFER_ID_CAP) this.seqNotes.delete(this.seqNotes.keys().next().value);
34277
+ }
34278
+ /** A-4: read ONLY by the bridge's `afterPublish` reply admission. */
34279
+ isVoiceScopedId(messageId) {
34280
+ const channelId = this.voiceIds.get(messageId);
34281
+ if (channelId === void 0) return false;
34282
+ this.expireIfIdle(channelId);
34283
+ return this.voiceIds.has(messageId);
34284
+ }
34285
+ /** The channel's mark noted when `messageId` was marked (the release re-read), or undefined (e.g. after a restart). */
34286
+ seqNoted(messageId) {
34287
+ const note = this.seqNotes.get(messageId);
34288
+ if (note === void 0) return void 0;
34289
+ this.expireIfIdle(note.channelId);
34290
+ const kept = this.seqNotes.get(messageId);
34291
+ return kept === void 0 ? void 0 : { seq: kept.seq, generation: kept.generation };
34292
+ }
34293
+ /** The bufSeq of the channel's newest entry (0 before any append). A snapshot's end seq. */
34294
+ seqAt(channelId) {
34295
+ this.expireIfIdle(channelId);
34296
+ return this.lastSeq.get(channelId) ?? 0;
34297
+ }
34298
+ /**
34299
+ * The channel's last `limit` entries, oldest first. With `upTo` (final fix I-1, a redelivered turn's re-snapshot),
34300
+ * only entries at or below that bufSeq.
34301
+ */
34302
+ recent(channelId, limit, upTo) {
34303
+ if (limit <= 0) return [];
34304
+ this.expireIfIdle(channelId);
34305
+ const all = this.channels.get(channelId)?.entries ?? [];
34306
+ const entries = upTo === void 0 ? all : all.filter((e) => e.bufSeq <= upTo);
34307
+ return entries.slice(-limit).map(deliver);
34308
+ }
34309
+ /**
34310
+ * The channel's AGENT entries appended after `since.seq` (lines spoken or answered since a snapshot), oldest first.
34311
+ * Final fix M-4: nothing when the channel has been reset since the mark (a later call's lines).
34312
+ */
34313
+ agentAfter(channelId, since) {
34314
+ if (this.generation(channelId) !== since.generation) return [];
34315
+ const entries = this.channels.get(channelId)?.entries ?? [];
34316
+ return entries.filter((e) => e.role === "agent" && e.bufSeq > since.seq).map(deliver);
34317
+ }
34318
+ /**
34319
+ * The channel's reset generation: it changes on every reset (explicit or idle) and on nothing else. A caller that
34320
+ * holds text across an await (the coalesce window) compares it to skip an append into a call that has since ended.
34321
+ */
34322
+ generation(channelId) {
34323
+ this.expireIfIdle(channelId);
34324
+ return this.gens.get(channelId) ?? 0;
34325
+ }
34326
+ /** Clear the channel: its entries, its voice-scoped ids, its seq notes and its admitted ids. Only ever removes. */
34327
+ reset(channelId) {
34328
+ this.gens.set(channelId, (this.gens.get(channelId) ?? 0) + 1);
34329
+ this.channels.delete(channelId);
34330
+ for (const [id, c] of this.voiceIds) if (c === channelId) this.voiceIds.delete(id);
34331
+ for (const [id, n] of this.seqNotes) if (n.channelId === channelId) this.seqNotes.delete(id);
34332
+ for (const [id, a] of this.admitted) if (a.channelId === channelId) this.admitted.delete(id);
34333
+ }
34334
+ append(channelId, e) {
34335
+ if (!this.opts.enabled) return;
34336
+ this.expireIfIdle(channelId);
34337
+ let ch = this.channels.get(channelId);
34338
+ if (ch === void 0) {
34339
+ ch = { entries: [], lastAppend: 0 };
34340
+ this.channels.set(channelId, ch);
34341
+ }
34342
+ const bufSeq = this.nextSeq++;
34343
+ this.lastSeq.set(channelId, bufSeq);
34344
+ ch.entries.push({ ...e, bufSeq });
34345
+ if (ch.entries.length > this.opts.cap) ch.entries.splice(0, ch.entries.length - this.opts.cap);
34346
+ ch.lastAppend = this.opts.now();
34347
+ }
34348
+ /** Reset (c): more than `idleMs` since the channel's last append clears it, checked on every read and append. */
34349
+ expireIfIdle(channelId) {
34350
+ const ch = this.channels.get(channelId);
34351
+ if (ch !== void 0 && this.opts.now() - ch.lastAppend > this.opts.idleMs) this.reset(channelId);
34352
+ }
34353
+ };
34354
+ function deliver(e) {
34355
+ return { role: e.role, text: e.text, seq: 0, ...e.origin ? { origin: e.origin } : {} };
34356
+ }
34357
+
34200
34358
  // src/vault.ts
34201
34359
  var VaultIntegrityError = class extends Error {
34202
34360
  constructor(message, cause) {
@@ -34397,6 +34555,15 @@ import {
34397
34555
  writeFileSync
34398
34556
  } from "node:fs";
34399
34557
  import { basename as basename4, isAbsolute, join as join18 } from "node:path";
34558
+
34559
+ // src/grants/search-providers.ts
34560
+ var SEARCH_PROVIDERS = /* @__PURE__ */ new Set(["brave", "tavily", "parallel", "exa", "firecrawl"]);
34561
+ var OWNER_DM_SEARCH_DAILY_CAP_MAX = 2e3;
34562
+ function isSearchProvider(value) {
34563
+ return typeof value === "string" && SEARCH_PROVIDERS.has(value);
34564
+ }
34565
+
34566
+ // src/service/runtime-config-dir.ts
34400
34567
  var MEMORY_FILE = "MEMORY.md";
34401
34568
  var REAL_FS = {
34402
34569
  mkdir: (p, mode) => mkdirSync5(p, { recursive: true, mode }),
@@ -34579,10 +34746,14 @@ function readLocalReport(home, identityId) {
34579
34746
  const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
34580
34747
  Object.entries(raw).filter(([, v]) => typeof v === "string")
34581
34748
  ) : {};
34749
+ const capRaw = doc.owner_dm_search_cap_effective;
34750
+ const ownerDmCap = typeof capRaw === "number" && Number.isInteger(capRaw) && capRaw >= 0 && capRaw <= OWNER_DM_SEARCH_DAILY_CAP_MAX ? capRaw : void 0;
34582
34751
  return {
34583
34752
  persona,
34584
34753
  has_search_key: hasSearchKey,
34585
34754
  has_llm_key: hasLlmKey,
34755
+ has_extract_provider: doc.has_extract_provider === true,
34756
+ ...ownerDmCap !== void 0 ? { owner_dm_search_cap_effective: ownerDmCap } : {},
34586
34757
  legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source),
34587
34758
  secret_paths: parseSecretPathsReport(doc.secret_paths)
34588
34759
  };
@@ -34700,7 +34871,8 @@ import { sha256 as sha25611 } from "@noble/hashes/sha256";
34700
34871
  import { ed25519 as ed255195 } from "@noble/curves/ed25519";
34701
34872
  var TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
34702
34873
  var MANIFEST_KNOWN_TOOLS = Object.freeze({
34703
- owner_dm: Object.freeze(["web_search", "fetch_url", "http_request", "sql_query"]),
34874
+ owner_dm: Object.freeze(["web_search", "web_extract", "fetch_url", "http_request", "sql_query"]),
34875
+ // web_extract: owner DM only, never team_room (spec §5.4, D5)
34704
34876
  team_room: Object.freeze(["web_search"])
34705
34877
  });
34706
34878
  function manifestSignedBytes(manifest) {
@@ -34775,7 +34947,11 @@ function parseConfig(v) {
34775
34947
  }
34776
34948
  if ("search" in v) {
34777
34949
  const sr = v.search;
34778
- if (!isPlainObject(sr) || !exactKeys(sr, ["provider"]) || sr.provider !== "brave" && sr.provider !== "tavily") return null;
34950
+ if (!isPlainObject(sr) || !exactKeys(sr, ["provider"], ["owner_dm_daily_cap"]) || !isSearchProvider(sr.provider)) return null;
34951
+ if ("owner_dm_daily_cap" in sr) {
34952
+ const cap = sr.owner_dm_daily_cap;
34953
+ if (typeof cap !== "number" || !Number.isSafeInteger(cap) || cap < 0 || cap > OWNER_DM_SEARCH_DAILY_CAP_MAX) return null;
34954
+ }
34779
34955
  }
34780
34956
  const mem = v.memory;
34781
34957
  if (!isPlainObject(mem) || !exactKeys(mem, ["mode"], ["recall_days"])) return null;
@@ -34785,7 +34961,12 @@ function parseConfig(v) {
34785
34961
  return Object.freeze({
34786
34962
  persona_sha256: v.persona_sha256,
34787
34963
  ...isPlainObject(v.llm) ? { llm: Object.freeze({ provider: v.llm.provider, model: v.llm.model }) } : {},
34788
- ...isPlainObject(v.search) ? { search: Object.freeze({ provider: v.search.provider }) } : {},
34964
+ ...isPlainObject(v.search) ? {
34965
+ search: Object.freeze({
34966
+ provider: v.search.provider,
34967
+ ..."owner_dm_daily_cap" in v.search ? { owner_dm_daily_cap: v.search.owner_dm_daily_cap } : {}
34968
+ })
34969
+ } : {},
34789
34970
  ...isStringArray(v.tools_allow) ? { tools_allow: Object.freeze([...v.tools_allow]) } : {},
34790
34971
  flags: Object.freeze([...v.flags]),
34791
34972
  memory: Object.freeze({ mode: mem.mode, ..."recall_days" in mem ? { recall_days: mem.recall_days } : {} }),
@@ -35016,7 +35197,20 @@ function bindTargets(raw, signed, equals) {
35016
35197
  }
35017
35198
  return { written, mismatches };
35018
35199
  }
35019
- function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true }) {
35200
+ function signedManifestTools(manifest, audience, searchProvidersCapable) {
35201
+ return MANIFEST_KNOWN_TOOLS[audience].filter((t) => (manifest.grants[audience] ?? []).includes(t) && (t !== "web_extract" || searchProvidersCapable));
35202
+ }
35203
+ function manifestPolicyFor(manifest, switches, searchProvidersCapable) {
35204
+ return {
35205
+ v: 1,
35206
+ manifest_tools: [.../* @__PURE__ */ new Set([
35207
+ ...switches.ownerDm ? signedManifestTools(manifest, "owner_dm", searchProvidersCapable) : [],
35208
+ ...switches.room ? signedManifestTools(manifest, "team_room", searchProvidersCapable) : []
35209
+ ])].sort(),
35210
+ secret_path_mode: "enforce"
35211
+ };
35212
+ }
35213
+ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true }, searchProvidersCapable = false) {
35020
35214
  const mismatches = /* @__PURE__ */ new Set();
35021
35215
  const cfg = manifest.config;
35022
35216
  const out = {};
@@ -35045,6 +35239,7 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
35045
35239
  for (const name of Object.keys(files)) {
35046
35240
  if (name !== LLM_SOURCE && !PERSONA_FILES.includes(name)) mismatches.add("files");
35047
35241
  }
35242
+ const ownerDmTools = signedManifestTools(manifest, "owner_dm", searchProvidersCapable);
35048
35243
  const sj = isObj2(payload.skills_json) ? payload.skills_json : void 0;
35049
35244
  const eps = bindTargets(sj?.endpoints, manifest.endpoints, endpointEquals);
35050
35245
  const dbs = bindTargets(sj?.databases, manifest.databases, databaseEquals);
@@ -35062,14 +35257,20 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
35062
35257
  appliedSearch = searchBlock;
35063
35258
  } else {
35064
35259
  mismatches.add("search");
35065
- searchBlock = (prior.search?.provider ?? void 0) === cfg.search?.provider ? prior.search : void 0;
35260
+ const usablePrior = cfg.search !== void 0 && prior.search?.provider === cfg.search.provider ? prior.search : void 0;
35261
+ searchBlock = cfg.search === void 0 ? void 0 : usablePrior ?? Object.freeze({ provider: cfg.search.provider });
35066
35262
  }
35067
35263
  const skillsDoc = {
35068
35264
  endpoints: eps.written,
35069
35265
  databases: dbs.written,
35070
35266
  flags: [...cfg.flags],
35071
35267
  ...deny !== void 0 || cfg.tools_allow !== void 0 ? { tools: { ...deny !== void 0 ? { deny } : {}, ...cfg.tools_allow !== void 0 ? { allow: [...cfg.tools_allow] } : {} } } : {},
35072
- ...searchBlock ? { search: searchBlock } : {}
35268
+ ...searchBlock ? { search: searchBlock } : {},
35269
+ // D31: the marker the runtime keys D24 (iii) on; written under every manifest, true when config.search is signed.
35270
+ search_signed: cfg.search !== void 0,
35271
+ // D36, D39: the signed owner-DM cap, only when the owner-DM switch is on and the manifest grants an owner-DM tool;
35272
+ // otherwise none, so the runtime clamps a signed raise to the default. Never read from the payload.
35273
+ ...cfg.search?.owner_dm_daily_cap !== void 0 && switches.ownerDm && ownerDmTools.length > 0 ? { owner_dm_search_cap: cfg.search.owner_dm_daily_cap } : {}
35073
35274
  };
35074
35275
  out["skills.json"] = JSON.stringify(skillsDoc, null, 2) + "\n";
35075
35276
  const signedMemory = { mode: cfg.memory.mode, ...cfg.memory.recall_days !== void 0 ? { recall_days: cfg.memory.recall_days } : {} };
@@ -35100,12 +35301,7 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
35100
35301
  mismatches.add("legacy_assign");
35101
35302
  legacyDecisions = decisions.filter((d) => isObj2(d) && d.action === "delete");
35102
35303
  }
35103
- const signedTools = (a) => MANIFEST_KNOWN_TOOLS[a].filter((t) => (manifest.grants[a] ?? []).includes(t));
35104
- const policy = {
35105
- v: 1,
35106
- manifest_tools: [.../* @__PURE__ */ new Set([...switches.ownerDm ? signedTools("owner_dm") : [], ...switches.room ? signedTools("team_room") : []])].sort(),
35107
- secret_path_mode: "enforce"
35108
- };
35304
+ const policy = manifestPolicyFor(manifest, switches, searchProvidersCapable);
35109
35305
  return {
35110
35306
  mismatches: CONFIG_BINDING_FIELDS.filter((f) => mismatches.has(f)).sort(),
35111
35307
  targetMismatches: { endpoint: eps.mismatches, database: dbs.mismatches },
@@ -35244,6 +35440,8 @@ var TOOL_ROOM_CLASSES = Object.freeze({
35244
35440
  current_time: Object.freeze(["room_safe_read"]),
35245
35441
  echo: Object.freeze(["room_safe_read"]),
35246
35442
  web_search: Object.freeze(["owner_credential"]),
35443
+ web_extract: Object.freeze(["owner_credential", "egress"]),
35444
+ // D5: spends the owner's key and sends URLs out
35247
35445
  fetch_url: Object.freeze(["egress"]),
35248
35446
  sql_query: Object.freeze(["owner_credential"]),
35249
35447
  http_request: Object.freeze(["owner_credential", "egress", "side_effect"])
@@ -35357,6 +35555,10 @@ var OWNER_DM_DEFAULT_READ_GRANTS = Object.freeze([
35357
35555
  var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
35358
35556
  Object.freeze({ tool: "current_time", mode: "read" })
35359
35557
  ]);
35558
+ var RELAYED_VOICE_GRANTS = Object.freeze([
35559
+ Object.freeze({ tool: "current_time", mode: "read" }),
35560
+ Object.freeze({ tool: "web_search", mode: "read" })
35561
+ ]);
35360
35562
  var NO_TOOL_GRANTS = Object.freeze([]);
35361
35563
  var WS_CLOSE_SESSION_SUPERSEDED2 = 4409;
35362
35564
  var OWNER_REPLAY_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
@@ -35665,6 +35867,8 @@ function hexToBytes4(h) {
35665
35867
  return new Uint8Array(Buffer.from(h, "hex"));
35666
35868
  }
35667
35869
  var BOOT_DRAIN_WAIT_MS_DEFAULT = 6e4;
35870
+ var SEARCH_CAPABILITY_REFETCH_DEBOUNCE_MS = 1e3;
35871
+ var SEARCH_CAPABILITY_REFETCH_MAX_WAIT_MS = 1e4;
35668
35872
  var BootDrainRotationRefusedError = class extends Error {
35669
35873
  constructor() {
35670
35874
  super("key-package rotation refused: the boot drain has not settled");
@@ -35714,6 +35918,12 @@ var BridgeAgent = class _BridgeAgent {
35714
35918
  workspaceDir: this.cfg.memoryWorkspaceDir,
35715
35919
  factsCharBudget: this.cfg.memoryFactsCharBudget
35716
35920
  });
35921
+ this.callBuffer = new CallBuffer({
35922
+ cap: CALL_BUFFER_CAP,
35923
+ idleMs: CALL_BUFFER_IDLE_MS,
35924
+ now: () => Date.now(),
35925
+ enabled: (this.cfg.memoryProvider ?? "securecoms") !== "none"
35926
+ });
35717
35927
  this.provider = new MlsGroupKeyProvider({
35718
35928
  store: this.store,
35719
35929
  backend: this.backend,
@@ -35860,6 +36070,8 @@ var BridgeAgent = class _BridgeAgent {
35860
36070
  this.actionClock = deps.actionThrottleNow ?? Date.now;
35861
36071
  this.rng = deps.rng ?? Math.random;
35862
36072
  this.bootDrainWaitMs = deps.bootDrainWaitMs ?? BOOT_DRAIN_WAIT_MS_DEFAULT;
36073
+ this.searchCapabilityRefetchDebounceMs = deps.searchCapabilityRefetchDebounceMs ?? SEARCH_CAPABILITY_REFETCH_DEBOUNCE_MS;
36074
+ this.searchCapabilityRefetchMaxWaitMs = deps.searchCapabilityRefetchMaxWaitMs ?? SEARCH_CAPABILITY_REFETCH_MAX_WAIT_MS;
35863
36075
  this.replySleep = deps.replySleep ?? ((ms) => new Promise((resolve4) => {
35864
36076
  const t = setTimeout(resolve4, ms);
35865
36077
  t.unref?.();
@@ -36298,6 +36510,8 @@ var BridgeAgent = class _BridgeAgent {
36298
36510
  bootSweepTimer;
36299
36511
  rng;
36300
36512
  bootDrainWaitMs;
36513
+ searchCapabilityRefetchDebounceMs;
36514
+ searchCapabilityRefetchMaxWaitMs;
36301
36515
  /** Resolved (and cleared) each time an ingest cycle settles: see start(). */
36302
36516
  ingestSettledWaiters = [];
36303
36517
  /** The boot key-package publish, when start() had to defer it. Test seam. */
@@ -36533,6 +36747,11 @@ var BridgeAgent = class _BridgeAgent {
36533
36747
  startedAt = 0;
36534
36748
  store;
36535
36749
  memory;
36750
+ /**
36751
+ * Voice fast-path privacy, Task 4b (spec §2.1, OQ-1 ruled): the current calls' spoken turns, in process only. The
36752
+ * ONLY history a voice-scoped turn sees; read through voiceScopedHistory, once per turn, at ingest.
36753
+ */
36754
+ callBuffer;
36536
36755
  /**
36537
36756
  * Markdown memory 7a: the one owner of every note mutation and of the
36538
36757
  * delivery cache (spec §6.3, §7.2). One per store, so one per identity.
@@ -36743,9 +36962,14 @@ var BridgeAgent = class _BridgeAgent {
36743
36962
  }
36744
36963
  const skillsJson = payload.skills_json ?? {};
36745
36964
  if (version && inForce === null) {
36965
+ let asDelivered = skillsJson;
36966
+ if (skillsJson !== null && typeof skillsJson === "object" && !Array.isArray(skillsJson)) {
36967
+ const { owner_dm_search_cap: _cap, search_signed: _signed, ...rest } = skillsJson;
36968
+ asDelivered = rest;
36969
+ }
36746
36970
  this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, {
36747
36971
  ...payload.files ?? {},
36748
- "skills.json": `${JSON.stringify(skillsJson, null, 2)}
36972
+ "skills.json": `${JSON.stringify(asDelivered, null, 2)}
36749
36973
  `
36750
36974
  });
36751
36975
  }
@@ -36800,7 +37024,7 @@ var BridgeAgent = class _BridgeAgent {
36800
37024
  }
36801
37025
  let bound;
36802
37026
  try {
36803
- bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants });
37027
+ bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.searchProvidersCapable());
36804
37028
  } catch (e) {
36805
37029
  bound = null;
36806
37030
  return this.bindFailed(manifest, payload.memory, e);
@@ -36819,9 +37043,9 @@ var BridgeAgent = class _BridgeAgent {
36819
37043
  return true;
36820
37044
  }
36821
37045
  try {
36822
- this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, bound.policy);
36823
37046
  this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, bound.files);
36824
37047
  if (manifest.config.llm === void 0) this.grantWriters.removeManagedLlmSource(this.home, this.cfg.identityId);
37048
+ this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.searchProvidersCapable()));
36825
37049
  } catch (e) {
36826
37050
  this.manifestBinding = null;
36827
37051
  throw e;
@@ -36874,6 +37098,7 @@ var BridgeAgent = class _BridgeAgent {
36874
37098
  /** The report half of refetchRuntimeConfigOnce (split out for the plan 4 Task 11 bind_failed path). */
36875
37099
  async postRuntimeConfigReport(base, version, skillsJson) {
36876
37100
  const local = readLocalReport(this.home, this.cfg.identityId);
37101
+ const reportedSearchProviders = this.searchProvidersCapable();
36877
37102
  const report = {
36878
37103
  applied_version: version,
36879
37104
  paths: checkConfiguredPaths(skillsJson),
@@ -36888,6 +37113,13 @@ var BridgeAgent = class _BridgeAgent {
36888
37113
  // to the backend. The backend needs it to generate a truthful tool
36889
37114
  // paragraph, so it rides along here.
36890
37115
  has_search_key: local?.has_search_key ?? false,
37116
+ // L-2 (spec §7.9): only the runtime knows whether an extract-capable provider key is readable; never derived
37117
+ // from has_search_key. An older runtime omits it: false.
37118
+ // Final review I-2 (controller ruling): AND the current search_providers_v2, so the web never shows extract while
37119
+ // the bridge withholds web_extract from the runtime.
37120
+ has_extract_provider: (local?.has_extract_provider ?? false) && reportedSearchProviders,
37121
+ // D39: the owner-DM daily cap the runtime enforces, passed through when it reported a valid one; never the signed value.
37122
+ ...local?.owner_dm_search_cap_effective !== void 0 ? { owner_dm_search_cap_effective: local.owner_dm_search_cap_effective } : {},
36891
37123
  // Same idea, for the BYOK LLM key file: only the runtime can see
36892
37124
  // whether one is configured in its own environment.
36893
37125
  has_llm_key: local?.has_llm_key ?? false,
@@ -36927,7 +37159,11 @@ var BridgeAgent = class _BridgeAgent {
36927
37159
  // Plan 4 Task 15 (OQ-5): sorted, unique enums; capability is the latest hello's class.
36928
37160
  config_binding_mismatches: [...this.configBindingMismatches],
36929
37161
  target_mismatches: ["database", "endpoint"].filter((k) => this.targetMismatches[k] > 0),
36930
- capability: this.lastConsumerCapability
37162
+ capability: this.lastConsumerCapability,
37163
+ // D28 (§11.1 step 3): a separate key, never a value in the capability enum. False with no runtime attached.
37164
+ search_providers_v2: reportedSearchProviders,
37165
+ // D19 item 4: content-free; the signed provider is one the attached runtime(s) cannot honour.
37166
+ search_provider_unsupported: this.searchProviderUnsupported(reportedSearchProviders)
36931
37167
  }
36932
37168
  };
36933
37169
  const reportedVerifyFailures = this.ownerVerifyFailures;
@@ -36948,6 +37184,7 @@ var BridgeAgent = class _BridgeAgent {
36948
37184
  return;
36949
37185
  }
36950
37186
  this.ownerVerifyFailures = Math.max(0, this.ownerVerifyFailures - reportedVerifyFailures);
37187
+ this.lastReportedSearchProviders = reportedSearchProviders;
36951
37188
  if (version) writeServiceLog("info", "runtime_config.applied", { version });
36952
37189
  else writeServiceLog("info", "runtime_config.legacy_memory_reported");
36953
37190
  }
@@ -37017,6 +37254,7 @@ var BridgeAgent = class _BridgeAgent {
37017
37254
  this.startedAt = Date.now();
37018
37255
  await this.restoreRuntimeInbox();
37019
37256
  await this.loadToolGrantState();
37257
+ this.recomputeManifestPolicy(false);
37020
37258
  try {
37021
37259
  this.store.pruneRoomSearchCap(utcDayOf(Date.now()));
37022
37260
  } catch (e) {
@@ -38161,6 +38399,9 @@ var BridgeAgent = class _BridgeAgent {
38161
38399
  const modality = this.spokenReplyIds.has(id) ? "voice" : "text";
38162
38400
  const bridgeLine = this.unifiedOn && body.kind === "text" && isBridgeMemoryLine(body.text);
38163
38401
  if (opts.inReplyTo !== void 0 && this.isOwnerVerifyFailed(opts.inReplyTo)) return;
38402
+ if (body.kind === "text" && opts.callBufferReply === true && opts.inReplyTo !== void 0 && !this.spokenReplyIds.has(id) && !isBridgeMemoryLine(body.text) && this.callBuffer.isVoiceScopedId(opts.inReplyTo)) {
38403
+ this.callBuffer.appendAgent(channelId, body.text, "reply");
38404
+ }
38164
38405
  const origin = this.spokenReplyIds.has(id) ? "fast_path" : !this.taintScope(channelId) ? "reply" : opts.inReplyTo !== void 0 && this.cleanTurns.has(opts.inReplyTo) && !this.taintedTurns.has(opts.inReplyTo) ? "reply" : "untrusted";
38165
38406
  if (body.kind === "text" && !bridgeLine) void recordOutboundMemory(this.memory, channelId, body.text, id, modality, purgeEpoch, origin);
38166
38407
  if (this.unifiedOn && body.kind === "text" && !bridgeLine) this.spawnUnifiedReply(channelId, body.text, id, replyCapture ?? null, origin);
@@ -38931,6 +39172,27 @@ var BridgeAgent = class _BridgeAgent {
38931
39172
  mayReadHistoryForTurn(messageId, authorization) {
38932
39173
  return !this.isOwnerVerifyFailed(messageId) && this.mayReadOpenClawMemoryForTurn(authorization);
38933
39174
  }
39175
+ /**
39176
+ * Voice fast-path privacy (spec §2.1, OQ-1 ruled): the ONLY history source of a voice-scoped turn, the call buffer.
39177
+ * Never reads the memory provider, the store, the unified store or the 7b index. A-6: called once per turn, at ingest
39178
+ * (takeVoiceSnapshot), after the reset signals and before the turn's own append.
39179
+ */
39180
+ voiceScopedHistory(channelId, limit, upTo) {
39181
+ return this.callBuffer.recent(channelId, limit, upTo);
39182
+ }
39183
+ /**
39184
+ * Task 4b (A-6): the turn's snapshot, taken BEFORE its own text is appended (audit finding 2). Final fix I-1: a turn
39185
+ * already admitted to this call (a redelivered row) is snapshotted up to just before its own entry, as it was the
39186
+ * first time, so its snapshot never contains itself.
39187
+ */
39188
+ takeVoiceSnapshot(channelId, messageId) {
39189
+ const upTo = this.callBuffer.admittedAt(messageId);
39190
+ return {
39191
+ recent: this.voiceScopedHistory(channelId, _BridgeAgent.TURN_MEMORY_LIMIT, upTo),
39192
+ endSeq: upTo ?? this.callBuffer.seqAt(channelId),
39193
+ generation: this.callBuffer.generation(channelId)
39194
+ };
39195
+ }
38934
39196
  /** An inbound turn may receive OpenClaw memory only from an active owner DM. */
38935
39197
  mayReadOpenClawMemoryForTurn(authorization) {
38936
39198
  return this.cfg.memoryProvider !== "openclaw" || authorization.sender_membership === "active" && authorization.audience === "owner_dm";
@@ -38940,16 +39202,26 @@ var BridgeAgent = class _BridgeAgent {
38940
39202
  * stays owner-DM-only; every provider failure degrades to empty context and
38941
39203
  * never falls back to the runtime's retired plaintext JSON files.
38942
39204
  */
38943
- async memoryForTurn(channelId, messageId, authorization, limit = 50) {
39205
+ async memoryForTurn(channelId, messageId, authorization, limit = 50, voice) {
38944
39206
  if (!this.mayReadHistoryForTurn(messageId, authorization)) {
38945
39207
  return { recent: [], facts: [] };
38946
39208
  }
39209
+ if (voice) return { recent: this.voiceFrameTurns(voice.snapshot?.recent ?? []), facts: [] };
38947
39210
  try {
38948
39211
  return this.fenceMemory(channelId, await this.memory.context(channelId, limit), holdsManifestGrant(authorization));
38949
39212
  } catch {
38950
39213
  return { recent: [], facts: [] };
38951
39214
  }
38952
39215
  }
39216
+ /**
39217
+ * Task 6 (spec §2.2 A-1, N1): call-buffer entries as runtime-frame turns. Under the voice fence (a pin is loaded) the
39218
+ * fast model's own lines (origin "fast_path") are dropped, as fenceMemory drops them from store history; the fast
39219
+ * prompt keeps them. The internal origin is not carried into the frame; `seq` stays 0 (A-5).
39220
+ */
39221
+ voiceFrameTurns(entries) {
39222
+ const fenced = this.voiceFenceOn();
39223
+ return entries.filter((e) => !(fenced && e.origin === "fast_path")).map((e) => ({ seq: e.seq, role: e.role, text: e.text }));
39224
+ }
38953
39225
  /**
38954
39226
  * Owner grants plan 4 Task 16 (T-30, audit r2 H-1): the voice fence is on whenever a pin is loaded, on EVERY runtime
38955
39227
  * turn (manifest, fallback, greeting, relayed, constants-only alike). It never depends on a turn's grants.
@@ -38984,7 +39256,7 @@ var BridgeAgent = class _BridgeAgent {
38984
39256
  if (!strict && !ctx.recent.some((t) => t.role === "agent")) return ctx;
38985
39257
  const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
38986
39258
  const securecoms = (this.cfg.memoryProvider ?? "securecoms") === "securecoms";
38987
- const meta = securecoms ? this.store.memoryLineMeta(channelId, ctx.recent.filter((t) => strict || t.role === "agent").map((t) => t.seq)) : /* @__PURE__ */ new Map();
39259
+ const meta = securecoms ? this.store.memoryLineMeta(channelId, ctx.recent.filter((t) => (strict || t.role === "agent") && t.seq !== 0).map((t) => t.seq)) : /* @__PURE__ */ new Map();
38988
39260
  const fence = this.fenceContext(channelId);
38989
39261
  const recent = ctx.recent.filter((t) => {
38990
39262
  const m = meta.get(t.seq);
@@ -39003,7 +39275,7 @@ var BridgeAgent = class _BridgeAgent {
39003
39275
  if (recent.length === 0) return false;
39004
39276
  const ownerId = this.cfg.ownerGrantPin?.pin?.ownerIdentityId ?? "";
39005
39277
  const securecoms = (this.cfg.memoryProvider ?? "securecoms") === "securecoms";
39006
- const meta = securecoms ? this.store.memoryLineMeta(m.channel_id, recent.filter((t) => t.role !== "agent").map((t) => t.seq)) : /* @__PURE__ */ new Map();
39278
+ const meta = securecoms ? this.store.memoryLineMeta(m.channel_id, recent.filter((t) => t.role !== "agent" && t.seq !== 0).map((t) => t.seq)) : /* @__PURE__ */ new Map();
39007
39279
  return recent.some((t) => t.role !== "agent" && isRelayClassPeer({ peerId: t.peerId, modality: meta.get(t.seq)?.modality }, ownerId));
39008
39280
  }
39009
39281
  /**
@@ -39299,9 +39571,18 @@ var BridgeAgent = class _BridgeAgent {
39299
39571
  writeServiceLog("warn", "memory.recall_prepare_failed", {}, e);
39300
39572
  }
39301
39573
  }
39574
+ /**
39575
+ * Voice fast-path privacy, Task 7 (spec §4): the frame-keyed delivery check. A voice-scoped frame gets no notes, no
39576
+ * recall and no unified items, whatever its authorization says. Keys on the frame's own marks, NEVER on the call
39577
+ * buffer's id set (a reset evicts ids; the staged frame must stay scoped).
39578
+ */
39579
+ frameIsVoiceScoped(m) {
39580
+ return m.voice_scoped === true || m.transcribed === true || m.speaker !== void 0;
39581
+ }
39302
39582
  /** ws flush (synchronous). Q1 to Q3 and Q6 again at delivery, on the ORIGINAL message; Q4 lives in withConsumerRecall. */
39303
39583
  recallForDelivery(m) {
39304
39584
  if (this.unifiedOn) return void 0;
39585
+ if (this.frameIsVoiceScoped(m)) return void 0;
39305
39586
  if (!this.recallProviderOn() || !this.notesReadable(m.authorization)) return void 0;
39306
39587
  return this.recall.deliverable(m.message_id, m.channel_id);
39307
39588
  }
@@ -39534,6 +39815,7 @@ var BridgeAgent = class _BridgeAgent {
39534
39815
  /** ws flush (synchronous). The predicate and the owner scope again at delivery, on the ORIGINAL message (H-4, Review Focus 3). */
39535
39816
  unifiedForDelivery(m) {
39536
39817
  if (!this.unifiedProviderOn()) return void 0;
39818
+ if (this.frameIsVoiceScoped(m)) return void 0;
39537
39819
  const a = m.authorization;
39538
39820
  if (!(a?.memory_write === true || a?.relay_speaker === "owner")) return void 0;
39539
39821
  const scopeId = this.ownerScopeNow(m.channel_id);
@@ -39769,6 +40051,7 @@ var BridgeAgent = class _BridgeAgent {
39769
40051
  notesForDelivery(m) {
39770
40052
  if (this.unifiedOn) return { generation: this.notes.appliedGeneration() };
39771
40053
  const generation = this.notes.appliedGeneration();
40054
+ if (this.frameIsVoiceScoped(m)) return { generation };
39772
40055
  if (!this.notesReadable(m.authorization)) return { generation };
39773
40056
  const d = this.notes.deliverable(m.channel_id);
39774
40057
  if (d.miss) {
@@ -40128,7 +40411,7 @@ var BridgeAgent = class _BridgeAgent {
40128
40411
  * deferring fast reply was actually published — F3).
40129
40412
  */
40130
40413
  async speakAndSettle(turn, plan, held, heldAfterAck, fastText, guidance, fastSpeaker, greeting) {
40131
- const outcome = plan.kind === "ack" ? await this.sendFastLine(turn.channelId, turn.messageId, plan.line) ? "deferred" : "failed" : await this.runFastReply(turn, fastText, guidance, fastSpeaker, greeting);
40414
+ const outcome = plan.kind === "ack" ? await this.sendFastLine(turn, plan.line) ? "deferred" : "failed" : await this.runFastReply(turn, fastText, guidance, fastSpeaker, greeting);
40132
40415
  if (outcome === "answered") await this.settleHeldRuntimeMessage(held, false);
40133
40416
  else await this.settleHeldRuntimeMessage(outcome === "deferred" ? heldAfterAck : held, true);
40134
40417
  }
@@ -40147,19 +40430,29 @@ var BridgeAgent = class _BridgeAgent {
40147
40430
  async settleFastReplies() {
40148
40431
  while (this.fastReplyChains.size) await Promise.all([...this.fastReplyChains.values()]);
40149
40432
  }
40150
- /** Send a line the voice fast path speaks: its memory record is "voice" (H1). */
40151
- async sendSpoken(channelId, text, messageId) {
40433
+ /**
40434
+ * Send a line the voice fast path speaks: its memory record is "voice" (H1).
40435
+ * Task 4b (spec §2.1 agent appends (1), A-1, A-2): once published, the line enters the call buffer as "fast_path",
40436
+ * unless the turn it answers failed owner verification (its id, or any fragment of a coalesced turn). A line answering
40437
+ * a turn the ingest did not find voice-scoped is not appended either (defense in depth: the fast path fires only on
40438
+ * voice-scoped turns, Task 5).
40439
+ */
40440
+ async sendSpoken(channelId, text, messageId, turn) {
40152
40441
  this.spokenReplyIds.add(messageId);
40153
40442
  try {
40154
40443
  await this.send(channelId, text, messageId);
40155
40444
  } finally {
40156
40445
  this.spokenReplyIds.delete(messageId);
40157
40446
  }
40447
+ if (turn.voiceScoped === true && turn.anyFragmentVerifyFailed !== true && !this.isOwnerVerifyFailed(turn.messageId)) {
40448
+ this.callBuffer.appendAgent(channelId, text, "fast_path");
40449
+ }
40158
40450
  }
40159
40451
  /** Speak a fixed acknowledgement line; failures are counted, never thrown. */
40160
- async sendFastLine(channelId, messageId, line) {
40452
+ async sendFastLine(turn, line) {
40453
+ const { channelId, messageId } = turn;
40161
40454
  try {
40162
- await this.sendSpoken(channelId, line, fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, messageId));
40455
+ await this.sendSpoken(channelId, line, fastReplyId(this.replyIdKey, this.cfg.identityId, channelId, messageId), turn);
40163
40456
  this.fastPathSent++;
40164
40457
  return true;
40165
40458
  } catch (e) {
@@ -40173,8 +40466,8 @@ var BridgeAgent = class _BridgeAgent {
40173
40466
  * the fast model's reply — or, in `ack_only`, the greeting template — is
40174
40467
  * spoken, and the outcome decides whether the runtime answers next.
40175
40468
  *
40176
- * MUST NEVER throw: every failure path (memory lookup, the LLM call,
40177
- * normalization, or the send itself) is caught, counted in
40469
+ * MUST NEVER throw: every failure path (the LLM call, normalization,
40470
+ * or the send itself) is caught, counted in
40178
40471
  * `fastPathFailed`, and logged WITHOUT the turn text or the model's reply.
40179
40472
  * The `fastReplyId(...)` send id is deterministic, so a duplicate that
40180
40473
  * slipped past the in-memory dedupe (e.g. a restart) resolves to the same
@@ -40188,20 +40481,18 @@ var BridgeAgent = class _BridgeAgent {
40188
40481
  try {
40189
40482
  if ((this.cfg.voiceFastPathMode ?? "full") === "ack_only") {
40190
40483
  if (!isGreeting) return "failed";
40191
- const ok = await this.sendFastLine(m.channelId, m.messageId, ackOnlyGreeting(this.voiceAgentName(), speaker?.displayName));
40484
+ const ok = await this.sendFastLine(m, ackOnlyGreeting(this.voiceAgentName(), speaker?.displayName));
40192
40485
  return ok ? "answered" : "failed";
40193
40486
  }
40194
40487
  const fastLlm = this.fastLlm;
40195
40488
  if (!fastLlm || !this.cfg.voice) return "failed";
40196
- let memory;
40197
- if (!this.mayReadHistoryForTurn(m.messageId, m.authorization)) {
40198
- memory = { recent: [], facts: [] };
40199
- } else {
40200
- try {
40201
- memory = await this.memory.context(m.channelId, FAST_PATH_MEMORY_TURNS);
40202
- } catch {
40203
- memory = { recent: [], facts: [] };
40204
- }
40489
+ let memory = { recent: [], facts: [] };
40490
+ const snapshot = m.voiceSnapshot;
40491
+ if (m.voiceScoped === true && snapshot !== void 0 && m.anyFragmentVerifyFailed !== true && this.mayReadHistoryForTurn(m.messageId, m.authorization)) {
40492
+ memory = {
40493
+ recent: [...snapshot.recent, ...this.callBuffer.agentAfter(m.channelId, { seq: snapshot.endSeq, generation: snapshot.generation })].slice(-FAST_PATH_MEMORY_TURNS),
40494
+ facts: []
40495
+ };
40205
40496
  }
40206
40497
  const messages = buildFastMessages({
40207
40498
  persona: this.cfg.voice.persona,
@@ -40224,7 +40515,7 @@ var BridgeAgent = class _BridgeAgent {
40224
40515
  const refusal = !isGreeting && isRefusal(split3.reply);
40225
40516
  const defer = split3.defer || refusal;
40226
40517
  const reply = refusal ? deferAck(m.messageId) : split3.reply;
40227
- await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId));
40518
+ await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId), m);
40228
40519
  this.fastPathSent++;
40229
40520
  if (defer) this.fastPathDeferred++;
40230
40521
  if (refusal) this.fastPathRefusalDeferred++;
@@ -40288,6 +40579,15 @@ var BridgeAgent = class _BridgeAgent {
40288
40579
  async memoryAtRelease(message) {
40289
40580
  const prior = message.memory;
40290
40581
  if (!prior || !this.mayReadHistoryForTurn(message.message_id, message.authorization)) return prior;
40582
+ if (this.frameIsVoiceScoped(message)) {
40583
+ const tails = this.voiceCoalescedTails.get(message.message_id) ?? [];
40584
+ if (tails.some((id) => this.isOwnerVerifyFailed(id))) return prior;
40585
+ const noted = this.callBuffer.seqNoted(message.message_id);
40586
+ if (noted === void 0) return prior;
40587
+ const spoken2 = this.voiceFrameTurns(this.callBuffer.agentAfter(message.channel_id, noted));
40588
+ if (spoken2.length === 0) return prior;
40589
+ return { recent: [...prior.recent, ...spoken2].slice(-_BridgeAgent.TURN_MEMORY_LIMIT), facts: [] };
40590
+ }
40291
40591
  let fresh;
40292
40592
  try {
40293
40593
  fresh = await this.memory.context(message.channel_id, _BridgeAgent.TURN_MEMORY_LIMIT);
@@ -40799,9 +41099,9 @@ var BridgeAgent = class _BridgeAgent {
40799
41099
  });
40800
41100
  }
40801
41101
  /** Classify one inbound turn at the agent's CURRENT epoch for its channel. */
40802
- async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId) {
41102
+ async authorizeTurn(channelId, messageId, senderId, ownerVerdict, relaySpeakerId, voiceScoped) {
40803
41103
  const held = await this.heldPosition(channelId);
40804
- const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict });
41104
+ const auth = this.authorizationFor(channelId, senderId, relaySpeakerId, held.epoch, held.groupId, { ownerVerdict, voiceScoped });
40805
41105
  if (!(auth.audience === "team_room" && auth.tool_grants.length > 0)) return auth;
40806
41106
  if (this.roomGrantEpochs.size >= ROOM_GRANT_TRACK_MAX || held.epoch === null || held.groupId === null) {
40807
41107
  writeServiceLog("warn", "room_grant.untracked");
@@ -40816,17 +41116,22 @@ var BridgeAgent = class _BridgeAgent {
40816
41116
  * guess. The returned object and grant array are frozen before they cross the
40817
41117
  * daemon boundary so one consumer cannot widen a later execution check.
40818
41118
  *
40819
- * Grants (architect ruling 2026-10-02): OWNER_DM_DEFAULT_READ_GRANTS only when
40820
- * the kill switch is on, the channel is a DM, and EITHER
41119
+ * Grants (architect ruling 2026-10-02; P-5 and G-1, spec §9): an owner-DM
41120
+ * constant only when the kill switch is on, the channel is a DM, and EITHER
40821
41121
  * (a) audience is owner_dm AND the sender is the channel's pinned DM
40822
41122
  * counterpart (`channelOwner`, never "anomalous"). A server-marked
40823
41123
  * `is_agent_owner` on a member who is NOT the DM's single human
40824
41124
  * counterpart gets nothing; on the counterpart it is trusted (see
40825
- * LIMIT below); OR
41125
+ * LIMIT below). OWNER_DM_DEFAULT_READ_GRANTS on a typed turn;
41126
+ * RELAYED_VOICE_GRANTS and no memory_write when `opts.voiceScoped`
41127
+ * (G-1: the ingest predicate isVoiceScopedTurn, spec §1); OR
40826
41128
  * (b) the sender is this channel's server-marked relay (`is_relay`) and the
40827
41129
  * relayed `speaker_identity_id` (passed by the caller only for relay
40828
41130
  * turns) is that same pinned owner, whose roster row is the agent's
40829
- * owner. A speaker claim from any non-relay sender gets nothing.
41131
+ * owner: RELAYED_VOICE_GRANTS. A speaker claim from any non-relay
41132
+ * sender gets nothing.
41133
+ * RELAYED_VOICE_GRANTS is never manifest-eligible: effectiveToolGrants gives
41134
+ * the constant by reference, or the fallback when a good manifest is held.
40830
41135
  * Both need the roster fetched at `heldEpoch` (the agent's current MLS
40831
41136
  * epoch for the channel), or the DM marked epoch-fresh at this ingest
40832
41137
  * cycle's refresh (audit M-B4).
@@ -40870,9 +41175,11 @@ var BridgeAgent = class _BridgeAgent {
40870
41175
  const ownerDirect = audience === "owner_dm" && senderId === ownerId;
40871
41176
  const owner = roster?.get(ownerId);
40872
41177
  const ownerRelayed = relaySpeakerId !== void 0 && sender.is_relay && relaySpeakerId === ownerId && owner !== void 0 && !owner.is_agent && owner.is_agent_owner;
40873
- if (ownerDirect || ownerRelayed) tool_grants = OWNER_DM_DEFAULT_READ_GRANTS;
41178
+ if (ownerDirect) tool_grants = opts.voiceScoped === true ? RELAYED_VOICE_GRANTS : OWNER_DM_DEFAULT_READ_GRANTS;
41179
+ if (ownerRelayed) tool_grants = RELAYED_VOICE_GRANTS;
40874
41180
  if (ownerRelayed) relaySpeaker = "owner";
40875
- if (ownerDirect && relaySpeakerId === void 0 && (this.cfg.memoryProvider ?? "securecoms") === "securecoms" && // Final review M-4: the live payload flag too, so with notes off the
41181
+ if (ownerDirect && relaySpeakerId === void 0 && // G-1: never on a voice-scoped turn.
41182
+ opts.voiceScoped !== true && (this.cfg.memoryProvider ?? "securecoms") === "securecoms" && // Final review M-4: the live payload flag too, so with notes off the
40876
41183
  // runtime does not offer memory_note at all. Not memoryConfigConfirmed:
40877
41184
  // a turn the boot drain pulls before the boot refetch keeps its mark,
40878
41185
  // and the read and write gates (notesProviderOn) hold it until then.
@@ -40889,11 +41196,11 @@ var BridgeAgent = class _BridgeAgent {
40889
41196
  const binding = this.manifestBinding;
40890
41197
  const boundNow = binding !== null && binding.state === "bound" && binding.counter === this.toolGrantCounter;
40891
41198
  const signedTargets = boundNow ? binding.signedTargets : { endpoints: 0, databases: 0 };
40892
- if (tool_grants === OWNER_DM_DEFAULT_READ_GRANTS) {
40893
- const eligible = relaySpeakerId === void 0 && opts.ownerVerdict?.kind === "verified" && manifest?.owner_dm_channel_id === channelId && boundNow && this.manifestConsumer !== null;
41199
+ if (tool_grants === OWNER_DM_DEFAULT_READ_GRANTS || tool_grants === RELAYED_VOICE_GRANTS) {
41200
+ const eligible = relaySpeakerId === void 0 && opts.ownerVerdict?.kind === "verified" && manifest?.owner_dm_channel_id === channelId && boundNow && this.manifestConsumer !== null && tool_grants !== RELAYED_VOICE_GRANTS;
40894
41201
  const r = effectiveToolGrants({
40895
41202
  audience: "owner_dm",
40896
- base: OWNER_DM_DEFAULT_READ_GRANTS,
41203
+ base: tool_grants,
40897
41204
  manifest,
40898
41205
  eligible,
40899
41206
  manifestSwitchOn: this.ownerDmManifestGrants,
@@ -41615,6 +41922,7 @@ var BridgeAgent = class _BridgeAgent {
41615
41922
  const acked = [];
41616
41923
  for (const m of entries) {
41617
41924
  try {
41925
+ if (m.voiceMode === false) this.callBuffer.reset(m.channelId);
41618
41926
  const encId = m.envelope.enclave_id;
41619
41927
  if (encId && !this.channelEnclaveRecorded.has(m.channelId)) {
41620
41928
  this.channelEnclaveRecorded.add(m.channelId);
@@ -41772,6 +42080,7 @@ var BridgeAgent = class _BridgeAgent {
41772
42080
  continue;
41773
42081
  }
41774
42082
  if (typeof parsed.text === "string") {
42083
+ if (parsed.text.trim() === VOICE_CALL_START_MARKER) this.callBuffer.reset(m.channelId);
41775
42084
  const meta = this.freshMeta(m.channelId);
41776
42085
  const senderType = m.envelope.sender_type;
41777
42086
  const senderIsAgent = senderType === "agent" ? true : senderType === "human" ? false : void 0;
@@ -41813,23 +42122,39 @@ var BridgeAgent = class _BridgeAgent {
41813
42122
  const senderName = this.freshRoster(m.channelId)?.get(
41814
42123
  m.envelope.sender_id
41815
42124
  )?.name;
42125
+ const voiceScoped = isVoiceScopedTurn({
42126
+ speaker,
42127
+ transcribed: parsed.transcribed,
42128
+ senderIsRelay: this.freshRoster(m.channelId)?.get(m.envelope.sender_id)?.is_relay === true,
42129
+ text: parsed.text
42130
+ });
41816
42131
  const ownerVerdict = await this.verifyOwnerTurn(m);
41817
42132
  const turnAuthorization = await this.authorizeTurn(
41818
42133
  m.channelId,
41819
42134
  m.messageId,
41820
42135
  m.envelope.sender_id,
41821
42136
  ownerVerdict,
41822
- speaker?.identityId
42137
+ speaker?.identityId,
42138
+ voiceScoped
41823
42139
  );
41824
42140
  await this.prepareNotesForTurn(m.channelId, turnAuthorization);
41825
- const runtimeMemory = this.memoryForTurn(m.channelId, m.messageId, turnAuthorization);
41826
42141
  const voiceGuidance = m.voiceMode ? selectVoiceGuidance(meta?.is_dm, m.voiceDefaultResponder) : void 0;
41827
42142
  const humanTurn = senderIsAgent !== true || speaker !== void 0;
41828
- const fastPathEligible = Boolean(m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
42143
+ const fastPathEligible = Boolean(voiceScoped && m.voiceMode && humanTurn && this.fastLlm && voiceGuidance);
41829
42144
  const turnText = parsed.text;
41830
42145
  const isCallStart = turnText.trim() === VOICE_CALL_START_MARKER;
41831
42146
  const windowEligible = voiceWindowEligible({ voiceMode: m.voiceMode, speaker, transcribed: parsed.transcribed, voiceGuidance, isCallStart });
41832
42147
  const coalescible = this.voiceCoalesceWindowMs > 0 && windowEligible;
42148
+ const callPeer = admitsToCallBuffer({ speaker, transcribed: parsed.transcribed });
42149
+ let voiceSnapshot;
42150
+ if (voiceScoped) {
42151
+ this.callBuffer.markVoiceScoped(m.channelId, m.messageId);
42152
+ if (!coalescible) {
42153
+ voiceSnapshot = this.takeVoiceSnapshot(m.channelId, m.messageId);
42154
+ if (callPeer && !this.isOwnerVerifyFailed(m.messageId)) this.callBuffer.appendPeer(m.channelId, parsed.text, m.messageId);
42155
+ }
42156
+ }
42157
+ const runtimeMemory = this.memoryForTurn(m.channelId, m.messageId, turnAuthorization, void 0, voiceScoped ? { snapshot: voiceSnapshot } : void 0);
41833
42158
  if (!coalescible) {
41834
42159
  const kind2 = m.voiceMode === true || parsed.transcribed === true ? "voice" : "text";
41835
42160
  if (this.unifiedOn) await this.prepareUnifiedForTurn({ channelId: m.channelId, messageId: m.messageId, text: parsed.text, authorization: turnAuthorization, kind: kind2 });
@@ -41845,7 +42170,9 @@ var BridgeAgent = class _BridgeAgent {
41845
42170
  const fastGreeting = isCallStart && fastPathFiring;
41846
42171
  const runtimeVoice = (ackSent) => m.voiceMode ? {
41847
42172
  voice: true,
41848
- guidance: ackSent ? `${voiceGuidance} ${FAST_PATH_SPOKEN_GUIDANCE}` : voiceGuidance
42173
+ // Task 6 (OQ-3): a voice-scoped frame's guidance ends with VOICE_CALL_CLAUSE, on all three frames
42174
+ // (held, coalesced after-ack, after-ack); a typed turn during a call keeps its guidance.
42175
+ guidance: `${ackSent ? `${voiceGuidance} ${FAST_PATH_SPOKEN_GUIDANCE}` : voiceGuidance}${voiceScoped ? ` ${VOICE_CALL_CLAUSE}` : ""}`
41849
42176
  } : {};
41850
42177
  const runtimeEntry = (ackSent, memory) => ({
41851
42178
  channel_id: m.channelId,
@@ -41866,6 +42193,7 @@ var BridgeAgent = class _BridgeAgent {
41866
42193
  // identify: the envelope's own claim (relay or on-device
41867
42194
  // transcript) or a relay-attributed turn.
41868
42195
  ...parsed.transcribed === true || speaker !== void 0 ? { transcribed: true } : {},
42196
+ ...voiceScoped ? { voice_scoped: true } : {},
41869
42197
  ...runtimeVoice(ackSent)
41870
42198
  });
41871
42199
  if (coalescible) {
@@ -41895,6 +42223,7 @@ var BridgeAgent = class _BridgeAgent {
41895
42223
  acked.push(m.queueId);
41896
42224
  continue;
41897
42225
  }
42226
+ const bufferGeneration = this.callBuffer.generation(m.channelId);
41898
42227
  const held = runtimeEntry(false, await runtimeMemory);
41899
42228
  if (!await this.stageHeldRuntimeMessage(held)) continue;
41900
42229
  this.noteVoiceCoalesced(m.messageId);
@@ -41905,7 +42234,8 @@ var BridgeAgent = class _BridgeAgent {
41905
42234
  closed: false,
41906
42235
  pending: Promise.resolve(),
41907
42236
  purgeEpoch: purgeEpochAtReceipt.get(m.channelId),
41908
- itemCapture: itemCaptureAtReceipt.get(m.channelId)
42237
+ itemCapture: itemCaptureAtReceipt.get(m.channelId),
42238
+ bufferGeneration
41909
42239
  };
41910
42240
  this.voiceCoalescing.set(windowKey, win);
41911
42241
  const openedAt = Date.now();
@@ -41921,9 +42251,18 @@ var BridgeAgent = class _BridgeAgent {
41921
42251
  const [firstId, ...tailIds] = win.ids;
41922
42252
  if (tailIds.length) this.voiceCoalescedTails.set(firstId, tailIds);
41923
42253
  const text = win.parts.join(" ");
42254
+ const sameCall = this.callBuffer.generation(channelId) === win.bufferGeneration;
42255
+ const anyFragmentVerifyFailed = win.ids.some((id) => this.isOwnerVerifyFailed(id));
42256
+ let windowSnapshot;
42257
+ if (sameCall) {
42258
+ this.callBuffer.markVoiceScoped(channelId, firstId);
42259
+ windowSnapshot = this.takeVoiceSnapshot(channelId, firstId);
42260
+ }
42261
+ if (callPeer && sameCall && !anyFragmentVerifyFailed) this.callBuffer.appendPeer(channelId, text, firstId);
41924
42262
  if (this.unifiedOn) await this.prepareUnifiedForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
41925
42263
  else await this.prepareRecallForTurn({ channelId, messageId: firstId, text, authorization: turnAuthorization, kind: "voice" });
41926
- const merged = { ...held, text };
42264
+ const windowMemory = await this.memoryForTurn(channelId, firstId, turnAuthorization, void 0, { snapshot: anyFragmentVerifyFailed ? void 0 : windowSnapshot });
42265
+ const merged = { ...held, text, memory: windowMemory };
41927
42266
  if (this.unifiedOn) {
41928
42267
  const snap = win.itemCapture ?? { ...this.store.itemCapture(channelId), purgeEpoch: -1 };
41929
42268
  const work = this.tapUnifiedVoice(merged, { stamp: this.ingestGeneration.get(firstId), capture: { ...snap, purgeEpoch: win.purgeEpoch ?? -1 } });
@@ -41951,7 +42290,7 @@ var BridgeAgent = class _BridgeAgent {
41951
42290
  const release = this.reserveFastReply(channelId);
41952
42291
  try {
41953
42292
  await this.speakAndSettle(
41954
- { channelId, messageId: firstId, senderId, authorization: turnAuthorization },
42293
+ { channelId, messageId: firstId, senderId, authorization: turnAuthorization, voiceScoped: voiceScoped && sameCall, voiceSnapshot: windowSnapshot, anyFragmentVerifyFailed },
41955
42294
  plan,
41956
42295
  merged,
41957
42296
  { ...merged, ...runtimeVoice(true) },
@@ -41983,7 +42322,9 @@ var BridgeAgent = class _BridgeAgent {
41983
42322
  channelId: m.channelId,
41984
42323
  messageId: m.messageId,
41985
42324
  senderId: m.envelope.sender_id,
41986
- authorization: turnAuthorization
42325
+ authorization: turnAuthorization,
42326
+ voiceScoped,
42327
+ voiceSnapshot
41987
42328
  };
41988
42329
  const guidanceForFast = voiceGuidance;
41989
42330
  const greeting = fastGreeting;
@@ -42808,6 +43149,115 @@ ${channelId}`;
42808
43149
  manifestConsumerAttached() {
42809
43150
  return this.manifestConsumer !== null;
42810
43151
  }
43152
+ /**
43153
+ * D28 (B-4): every socket that said hello, with whether its latest hello declared search_providers_v2 on the
43154
+ * manifest_grants_v1 predicate (ws-server.ts). The agent value is the AND over all of them, absent (false) with
43155
+ * none: unlike manifestConsumer, never newest wins. The per-delivery filter judges each socket by its own hello.
43156
+ */
43157
+ searchProvidersSockets = /* @__PURE__ */ new Map();
43158
+ /**
43159
+ * The D28 value the files were last recomputed for; null until the first socket observation, so that observation
43160
+ * always recomputes (final review I-1), even when it is non-capable.
43161
+ */
43162
+ searchProvidersState = null;
43163
+ searchCapabilityRefetchTimer = null;
43164
+ /** When the current burst's first change happened (valid while the timer is set). */
43165
+ searchCapabilityBurstStart = 0;
43166
+ /** The D28 value in the last report posted (null until one is); a change needs a refetch to reach the web. */
43167
+ lastReportedSearchProviders = null;
43168
+ noteSearchProvidersSocket(token, declared) {
43169
+ this.searchProvidersSockets.set(token, declared);
43170
+ this.onSearchProvidersMaybeChanged();
43171
+ }
43172
+ clearSearchProvidersSocket(token) {
43173
+ if (this.searchProvidersSockets.delete(token)) this.onSearchProvidersMaybeChanged();
43174
+ }
43175
+ /** D28: every attached runtime socket declared search_providers_v2; no socket attached means absent (false). */
43176
+ searchProvidersCapable() {
43177
+ if (this.searchProvidersSockets.size === 0) return false;
43178
+ for (const v of this.searchProvidersSockets.values()) if (!v) return false;
43179
+ return true;
43180
+ }
43181
+ /**
43182
+ * D19 item 3 (B-6): the search provider signed in the stored verified manifest in force (a pin loaded and a good
43183
+ * manifest), never the server payload; undefined with no pin, no manifest, or no signed search block. The
43184
+ * per-delivery filter (consumer-grants.ts withSearchCapability) and the S-28 report reason both read this.
43185
+ */
43186
+ signedSearchProvider() {
43187
+ const pin = this.cfg.ownerGrantPin?.pin ?? null;
43188
+ return pin !== null ? this.toolGrantManifest?.config.search?.provider : void 0;
43189
+ }
43190
+ /** D19 item 4, D34: the in-force signed provider is not brave, and the attached runtime(s) lack the capability. */
43191
+ searchProviderUnsupported(capable) {
43192
+ if (capable) return false;
43193
+ const provider = this.signedSearchProvider();
43194
+ return provider !== void 0 && provider !== "brave";
43195
+ }
43196
+ /**
43197
+ * D35 (P-4): on a capability change, rewrite manifest-policy.json at once from the stored manifest, the kill
43198
+ * switches and the new capability, then a debounced refetch (one per burst) only when needed.
43199
+ */
43200
+ onSearchProvidersMaybeChanged() {
43201
+ const capable = this.searchProvidersCapable();
43202
+ const prev = this.searchProvidersState;
43203
+ if (capable === prev) return;
43204
+ this.searchProvidersState = capable;
43205
+ this.recomputeManifestPolicy(capable);
43206
+ if (this.closingDown) return;
43207
+ if (prev === null && !capable) return;
43208
+ const now = Date.now();
43209
+ if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
43210
+ else this.searchCapabilityBurstStart = now;
43211
+ const delay = Math.max(0, Math.min(this.searchCapabilityRefetchDebounceMs, this.searchCapabilityBurstStart + this.searchCapabilityRefetchMaxWaitMs - now));
43212
+ this.searchCapabilityRefetchTimer = setTimeout(() => {
43213
+ this.searchCapabilityRefetchTimer = null;
43214
+ if (this.searchCapabilityRefetchNeeded()) void this.refetchRuntimeConfig();
43215
+ }, delay);
43216
+ this.searchCapabilityRefetchTimer.unref?.();
43217
+ }
43218
+ /**
43219
+ * A refetch re-renders skills.json (owner_dm_search_cap counts web_extract only with the capability, D39) and posts
43220
+ * the report. Needed under a manifest in force, or when the reported value is stale. Never before the boot fetch.
43221
+ */
43222
+ searchCapabilityRefetchNeeded() {
43223
+ if (!this.bootConfigSettled || this.closingDown) return false;
43224
+ const inForce = (this.cfg.ownerGrantPin?.pin ?? null) !== null && this.toolGrantManifest !== null;
43225
+ return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable();
43226
+ }
43227
+ /**
43228
+ * D35: the local recompute. Widening (web_extract back in) only while the binding for the in-force manifest stands,
43229
+ * so a policy is never written ahead of its skills.json. Narrowing never waits: with no binding in force, a file that
43230
+ * lists web_extract loses it at once, so an older runtime never reads it as unreadable (which drops every grant).
43231
+ * Content-free; a write failure is logged and the debounced refetch tries again.
43232
+ */
43233
+ recomputeManifestPolicy(capable) {
43234
+ const pin = this.cfg.ownerGrantPin?.pin ?? null;
43235
+ const manifest = pin !== null ? this.toolGrantManifest : null;
43236
+ try {
43237
+ if (manifest !== null && this.manifestBinding !== null && this.manifestBinding.counter === manifest.counter) {
43238
+ this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, capable));
43239
+ return;
43240
+ }
43241
+ if (capable) return;
43242
+ const path = join19(runtimeConfigDir(this.home, this.cfg.identityId), MANIFEST_POLICY_FILE);
43243
+ if (!existsSync5(path)) return;
43244
+ let doc;
43245
+ try {
43246
+ doc = JSON.parse(readFileSync11(path, "utf8"));
43247
+ } catch {
43248
+ return;
43249
+ }
43250
+ const tools = doc?.manifest_tools;
43251
+ if (!Array.isArray(tools) || !tools.includes("web_extract")) return;
43252
+ this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, {
43253
+ v: 1,
43254
+ manifest_tools: tools.filter((t) => typeof t === "string" && t !== "web_extract"),
43255
+ secret_path_mode: "enforce"
43256
+ });
43257
+ } catch (e) {
43258
+ writeServiceLog("warn", "authz.search_capability", { state: ServiceLogEnum.of("policy_write_failed", ["policy_write_failed"]) }, e);
43259
+ }
43260
+ }
42811
43261
  noteConsumerCapability(state) {
42812
43262
  this.lastConsumerCapability = state;
42813
43263
  if (state !== "ok") {
@@ -42999,6 +43449,8 @@ ${channelId}`;
42999
43449
  this.supersedeRecheckTimers.clear();
43000
43450
  if (this.redeliveryTimer !== null) clearTimeout(this.redeliveryTimer);
43001
43451
  this.redeliveryTimer = null;
43452
+ if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
43453
+ this.searchCapabilityRefetchTimer = null;
43002
43454
  this.pingHandle?.close();
43003
43455
  if (this.safetyTimer) clearInterval(this.safetyTimer);
43004
43456
  if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
@@ -43109,11 +43561,11 @@ ${channelId}`;
43109
43561
 
43110
43562
  // src/auth.ts
43111
43563
  import { randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
43112
- import { existsSync as existsSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync2 } from "node:fs";
43564
+ import { existsSync as existsSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync2 } from "node:fs";
43113
43565
  function resolveToken(opts) {
43114
43566
  if (opts.token) return opts.token;
43115
43567
  const f = opts.tokenFile;
43116
- if (f && existsSync6(f)) return readFileSync11(f, "utf8").trim();
43568
+ if (f && existsSync6(f)) return readFileSync12(f, "utf8").trim();
43117
43569
  const tok = randomBytes11(32).toString("base64url");
43118
43570
  if (f) writeFileSync2(f, tok, { mode: 384 });
43119
43571
  return tok;
@@ -43214,6 +43666,7 @@ function withConsumerGrants(m, perToolConsumer) {
43214
43666
  return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
43215
43667
  }
43216
43668
  var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
43669
+ var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
43217
43670
  var CONSTANT_TOOL_NAMES = Object.freeze({
43218
43671
  owner_dm: Object.freeze(["current_time", "web_search"]),
43219
43672
  team_room: Object.freeze(["current_time"])
@@ -43236,6 +43689,15 @@ function withHarnessGrants(m, withheld) {
43236
43689
  if (!a || a.tool_grants.length === 0) return m;
43237
43690
  return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
43238
43691
  }
43692
+ function withSearchCapability(m, consumerHasFeature, signedProvider) {
43693
+ if (consumerHasFeature) return m;
43694
+ const a = m.authorization;
43695
+ if (!a) return m;
43696
+ const stripSearch = signedProvider !== void 0 && signedProvider !== "brave";
43697
+ const kept = a.tool_grants.filter((g) => g.tool !== "web_extract" && !(stripSearch && g.tool === "web_search"));
43698
+ if (kept.length === a.tool_grants.length) return m;
43699
+ return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
43700
+ }
43239
43701
 
43240
43702
  // src/http-server.ts
43241
43703
  function writeSse(res, msgs, onWriteError) {
@@ -43258,7 +43720,8 @@ data: ${JSON.stringify(msgs[i])}
43258
43720
  }
43259
43721
  function deliverSseBatch(res, batch, agent, onWriteError) {
43260
43722
  const withheld = agent.harnessGrantsWithheld();
43261
- const left3 = writeSse(res, batch.map((m) => withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false)), onWriteError);
43723
+ const provider = agent.signedSearchProvider();
43724
+ const left3 = writeSse(res, batch.map((m) => withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider)), onWriteError);
43262
43725
  const leftIds = new Set(left3.map((message) => message.message_id));
43263
43726
  agent.ackRuntimeMessages(batch.filter((message) => !leftIds.has(message.message_id)).map((message) => message.message_id));
43264
43727
  if (left3.length) agent.requeueFront(batch.filter((message) => leftIds.has(message.message_id)));
@@ -43529,7 +43992,10 @@ function createHttpServer(agent, opts) {
43529
43992
  if (method === "GET" && path === "/v1/messages") {
43530
43993
  const channel = url.searchParams.get("channel_id") ?? void 0;
43531
43994
  const wait = Math.max(0, Math.min(50, Number(url.searchParams.get("wait") ?? 0)));
43532
- return json(res, 200, (await agent.drain(channel, wait)).map((m) => withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), agent.harnessGrantsWithheld()), false)));
43995
+ const drained = await agent.drain(channel, wait);
43996
+ const withheld = agent.harnessGrantsWithheld();
43997
+ const provider = agent.signedSearchProvider();
43998
+ return json(res, 200, drained.map((m) => withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider)));
43533
43999
  }
43534
44000
  if (method === "GET" && path === "/v1/events") {
43535
44001
  const channel = url.searchParams.get("channel_id") ?? void 0;
@@ -43703,6 +44169,7 @@ function attachAgentWebSocket(server, agent, opts) {
43703
44169
  consumer: "unknown",
43704
44170
  perToolGrants: false,
43705
44171
  manifestGrants: false,
44172
+ searchProviders: false,
43706
44173
  // helloWaitMs 0 keeps today's synchronous initial flush exactly (the opt-out the existing tests use).
43707
44174
  deliveryOpen: helloWaitMs <= 0,
43708
44175
  deliveryToken: /* @__PURE__ */ new Map(),
@@ -43756,9 +44223,13 @@ function attachAgentWebSocket(server, agent, opts) {
43756
44223
  continue;
43757
44224
  }
43758
44225
  const out = withConsumerNotes(
43759
- withManifestCapability(
43760
- withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
43761
- session.manifestGrants
44226
+ withSearchCapability(
44227
+ withManifestCapability(
44228
+ withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
44229
+ session.manifestGrants
44230
+ ),
44231
+ session.searchProviders,
44232
+ agent.signedSearchProvider()
43762
44233
  ),
43763
44234
  batch[i],
43764
44235
  session,
@@ -43794,6 +44265,7 @@ function attachAgentWebSocket(server, agent, opts) {
43794
44265
  const cleanup = () => {
43795
44266
  if (cleaned) return;
43796
44267
  cleaned = true;
44268
+ session.closed = true;
43797
44269
  if (helloTimer) clearTimeout(helloTimer);
43798
44270
  agent.noteSessionClose();
43799
44271
  unsubscribe();
@@ -43814,6 +44286,7 @@ function attachAgentWebSocket(server, agent, opts) {
43814
44286
  if (session.harness) agent.clearHarnessReport(session.harness);
43815
44287
  if (session.perToolGrants) agent.clearPerToolConsumer(session);
43816
44288
  if (session.manifestGrants) agent.clearManifestConsumer(session);
44289
+ agent.clearSearchProvidersSocket(session);
43817
44290
  };
43818
44291
  current = { sock, cleanup, sweepStale };
43819
44292
  sock.on("close", cleanup);
@@ -43889,6 +44362,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
43889
44362
  return;
43890
44363
  }
43891
44364
  if (m.type === "hello") {
44365
+ if (session.closed) return;
43892
44366
  const report = m.harness === void 0 ? null : parseHarnessReport(m.harness);
43893
44367
  if (report) {
43894
44368
  session.harness = report;
@@ -43912,6 +44386,8 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
43912
44386
  agent.clearManifestConsumer(session);
43913
44387
  }
43914
44388
  agent.noteConsumerCapability(manifest ? "ok" : session.harnessClaimed ? "harness_consumer" : "missing");
44389
+ session.searchProviders = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(SEARCH_PROVIDERS_FEATURE);
44390
+ agent.noteSearchProvidersSocket(session, session.searchProviders);
43915
44391
  if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
43916
44392
  agent.noteNotesConsumer();
43917
44393
  }
@@ -43919,7 +44395,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
43919
44395
  if (session.recallConsumer) agent.noteRecallConsumer();
43920
44396
  session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
43921
44397
  if (session.unifiedConsumer) agent.noteUnifiedConsumer();
43922
- sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE] });
44398
+ sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE, SEARCH_PROVIDERS_FEATURE] });
43923
44399
  if (!session.deliveryOpen) {
43924
44400
  session.deliveryOpen = true;
43925
44401
  flush();
@@ -43971,10 +44447,13 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
43971
44447
  sendJson(sock, wsErrorFrame("invalid_request"));
43972
44448
  return;
43973
44449
  }
44450
+ const record = typeof m.in_reply_to === "string" ? inflight.get(m.in_reply_to) : void 0;
44451
+ const callBufferReply = session.consumer === "runtime" && session.perToolGrants === true && session.harnessClaimed !== true && typeof m.in_reply_to === "string" && record !== void 0 && session.deliveryToken.get(m.in_reply_to) !== void 0 && record.channel_id === m.channel_id && !agent.isRuntimeMessageRetired(m.in_reply_to);
43974
44452
  const sendOpts = {
43975
44453
  agentTurn: isAgentTurn(agent, m),
43976
44454
  ...typeof m.in_reply_to === "string" ? { inReplyTo: m.in_reply_to } : {},
43977
- ...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {}
44455
+ ...typeof m.idempotency_key === "string" ? { idempotencyKey: m.idempotency_key } : {},
44456
+ ...callBufferReply ? { callBufferReply: true } : {}
43978
44457
  };
43979
44458
  if (m.a2a && typeof m.a2a === "object") {
43980
44459
  const a = m.a2a;
@@ -44145,7 +44624,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
44145
44624
  var LOWER_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
44146
44625
 
44147
44626
  // src/bin.ts
44148
- import { readFileSync as readFileSync16, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5, rmSync as rmSync8, existsSync as existsSync7, realpathSync as realpathSync3, readdirSync as readdirSync4, statSync as statSync8, chmodSync as chmodSync6 } from "node:fs";
44627
+ import { readFileSync as readFileSync17, mkdirSync as mkdirSync8, writeFileSync as writeFileSync5, rmSync as rmSync8, existsSync as existsSync7, realpathSync as realpathSync3, readdirSync as readdirSync4, statSync as statSync8, chmodSync as chmodSync6 } from "node:fs";
44149
44628
  import { join as join25, dirname as dirname13, isAbsolute as isAbsolute3, resolve as resolve3 } from "node:path";
44150
44629
  import { fileURLToPath as fileURLToPath4 } from "node:url";
44151
44630
  import { homedir as homedir5, userInfo } from "node:os";
@@ -44358,7 +44837,8 @@ function installService(input, deps) {
44358
44837
  deps.writeFile(cfg.unitPath, renderSystemdUnit(cfg));
44359
44838
  deps.run("systemctl", ["--user", "daemon-reload"]);
44360
44839
  deps.run("loginctl", ["enable-linger", deps.user]);
44361
- requireOk(deps.run("systemctl", ["--user", "enable", "--now", cfg.unitName]), "systemctl --user enable --now");
44840
+ requireOk(deps.run("systemctl", ["--user", "enable", cfg.unitName]), "systemctl --user enable");
44841
+ requireOk(deps.run("systemctl", ["--user", "restart", cfg.unitName]), "systemctl --user restart");
44362
44842
  return { label: cfg.unitName, filePath: cfg.unitPath, logHint: `journalctl --user -u ${cfg.unitName} -f`, tokenHint: tokenHint(input) };
44363
44843
  }
44364
44844
  throw new ServiceUnsupportedError(`Service install isn't supported on ${deps.platform} yet. Run the agent in a terminal, or use a process manager.`);
@@ -45107,7 +45587,7 @@ function renderStatusTable(rows) {
45107
45587
  import {
45108
45588
  chmodSync as chmodSync5,
45109
45589
  mkdirSync as mkdirSync6,
45110
- readFileSync as readFileSync12,
45590
+ readFileSync as readFileSync13,
45111
45591
  renameSync as renameSync4,
45112
45592
  unlinkSync as unlinkSync2,
45113
45593
  writeFileSync as writeFileSync3
@@ -45119,7 +45599,7 @@ var REAL_FS2 = {
45119
45599
  chmod: (p, mode) => chmodSync5(p, mode),
45120
45600
  writeFile: (p, data, mode) => writeFileSync3(p, data, { mode }),
45121
45601
  rename: (a, b) => renameSync4(a, b),
45122
- readFile: (p) => readFileSync12(p, "utf8"),
45602
+ readFile: (p) => readFileSync13(p, "utf8"),
45123
45603
  unlink: (p) => unlinkSync2(p)
45124
45604
  };
45125
45605
  function securecomsDir(home) {
@@ -45168,7 +45648,7 @@ function removeConnectionFileIfOwned(home, identityId, instanceId, deps = REAL_F
45168
45648
 
45169
45649
  // src/service/instance-lock.ts
45170
45650
  import { execFileSync } from "node:child_process";
45171
- import { closeSync as closeSync4, fsyncSync, mkdirSync as mkdirSync7, openSync as openSync4, readFileSync as readFileSync13, statSync as statSync6, unlinkSync as unlinkSync3, utimesSync, writeSync as writeSync2 } from "node:fs";
45651
+ import { closeSync as closeSync4, fsyncSync, mkdirSync as mkdirSync7, openSync as openSync4, readFileSync as readFileSync14, statSync as statSync6, unlinkSync as unlinkSync3, utimesSync, writeSync as writeSync2 } from "node:fs";
45172
45652
  import { hostname, uptime as uptime2 } from "node:os";
45173
45653
  import { dirname as dirname11 } from "node:path";
45174
45654
  var LOCK_PARTIAL_WRITE_GRACE_MS = 1e4;
@@ -45182,7 +45662,7 @@ function errno(error) {
45182
45662
  }
45183
45663
  function readTrimmed(path) {
45184
45664
  try {
45185
- const v = readFileSync13(path, "utf8").trim();
45665
+ const v = readFileSync14(path, "utf8").trim();
45186
45666
  return v === "" ? void 0 : v;
45187
45667
  } catch {
45188
45668
  return void 0;
@@ -45297,7 +45777,7 @@ var InstanceLock = class {
45297
45777
  if (!this.held) return false;
45298
45778
  let current;
45299
45779
  try {
45300
- current = readFileSync13(this.path, "utf8");
45780
+ current = readFileSync14(this.path, "utf8");
45301
45781
  } catch {
45302
45782
  current = void 0;
45303
45783
  }
@@ -45315,7 +45795,7 @@ var InstanceLock = class {
45315
45795
  let ageMs;
45316
45796
  this.env.beforeLockRead?.();
45317
45797
  try {
45318
- raw = readFileSync13(this.path, "utf8");
45798
+ raw = readFileSync14(this.path, "utf8");
45319
45799
  ageMs = Date.now() - statSync6(this.path).mtimeMs;
45320
45800
  } catch (error) {
45321
45801
  if (errno(error) === "ENOENT") {
@@ -45356,7 +45836,7 @@ var InstanceLock = class {
45356
45836
  const written = this.written;
45357
45837
  this.drop();
45358
45838
  try {
45359
- if (written !== void 0 && readFileSync13(this.path, "utf8") === written) unlinkSync3(this.path);
45839
+ if (written !== void 0 && readFileSync14(this.path, "utf8") === written) unlinkSync3(this.path);
45360
45840
  } catch {
45361
45841
  }
45362
45842
  }
@@ -45370,7 +45850,7 @@ var InstanceLock = class {
45370
45850
  took() {
45371
45851
  this.held = true;
45372
45852
  try {
45373
- this.written = readFileSync13(this.path, "utf8");
45853
+ this.written = readFileSync14(this.path, "utf8");
45374
45854
  } catch {
45375
45855
  this.written = void 0;
45376
45856
  }
@@ -45452,7 +45932,7 @@ var InstanceLock = class {
45452
45932
  this.env.beforeTakeoverReread?.();
45453
45933
  let current;
45454
45934
  try {
45455
- current = readFileSync13(this.path, "utf8");
45935
+ current = readFileSync14(this.path, "utf8");
45456
45936
  } catch {
45457
45937
  current = void 0;
45458
45938
  }
@@ -45466,7 +45946,7 @@ var InstanceLock = class {
45466
45946
  if (created !== "created") {
45467
45947
  if (created === "failed") return "failed";
45468
45948
  try {
45469
- this.holderPid = parseBody(readFileSync13(this.path, "utf8"))?.pid;
45949
+ this.holderPid = parseBody(readFileSync14(this.path, "utf8"))?.pid;
45470
45950
  } catch {
45471
45951
  }
45472
45952
  return "held";
@@ -45492,7 +45972,7 @@ import {
45492
45972
  fsyncSync as fsyncSync2,
45493
45973
  lstatSync as lstatSync8,
45494
45974
  openSync as openSync5,
45495
- readFileSync as readFileSync14,
45975
+ readFileSync as readFileSync15,
45496
45976
  realpathSync as realpathSync2,
45497
45977
  renameSync as renameSync5,
45498
45978
  unlinkSync as unlinkSync4,
@@ -45529,7 +46009,7 @@ function isSymlink(path) {
45529
46009
  }
45530
46010
  }
45531
46011
  function readJson(path) {
45532
- return JSON.parse(readFileSync14(path, "utf8"));
46012
+ return JSON.parse(readFileSync15(path, "utf8"));
45533
46013
  }
45534
46014
  function publicKeyOf(v) {
45535
46015
  const r = parsePinObject(v);
@@ -45773,7 +46253,7 @@ function realResolveDeps() {
45773
46253
  }
45774
46254
  function realPrepDeps() {
45775
46255
  return {
45776
- readFile: (p) => readFileSync16(p, "utf8"),
46256
+ readFile: (p) => readFileSync17(p, "utf8"),
45777
46257
  writeFileMode: (p, data, mode) => writeFileSync5(p, data, { mode }),
45778
46258
  mkdirp: (p, mode) => {
45779
46259
  mkdirSync8(p, { recursive: true, mode: mode ?? 493 });
@@ -45826,7 +46306,7 @@ function realReadFleet() {
45826
46306
  return [];
45827
46307
  }
45828
46308
  },
45829
- readFile: (p) => readFileSync16(p, "utf8")
46309
+ readFile: (p) => readFileSync17(p, "utf8")
45830
46310
  });
45831
46311
  }
45832
46312
  function runStatus() {
@@ -45851,7 +46331,7 @@ function realCopyConnector(srcDir, dstDir, harness) {
45851
46331
  }
45852
46332
  if (name.includes(".test.")) continue;
45853
46333
  if (!allowedExt.some((ext) => name.endsWith(ext))) continue;
45854
- writeFileSync5(join25(dstDir, name), readFileSync16(srcPath));
46334
+ writeFileSync5(join25(dstDir, name), readFileSync17(srcPath));
45855
46335
  }
45856
46336
  }
45857
46337
  function realWriteBinding(dstDir, identityId) {
@@ -45860,7 +46340,7 @@ function realWriteBinding(dstDir, identityId) {
45860
46340
  }
45861
46341
  function realReadBinding(dstDir) {
45862
46342
  try {
45863
- const parsed = JSON.parse(readFileSync16(join25(dstDir, "securecoms.binding.json"), "utf8"));
46343
+ const parsed = JSON.parse(readFileSync17(join25(dstDir, "securecoms.binding.json"), "utf8"));
45864
46344
  return parsed.identity_id ?? null;
45865
46345
  } catch {
45866
46346
  return null;
@@ -45878,7 +46358,7 @@ function realStatDir(p) {
45878
46358
  }
45879
46359
  function realReadHermesConfig(p) {
45880
46360
  try {
45881
- return readFileSync16(p, "utf8");
46361
+ return readFileSync17(p, "utf8");
45882
46362
  } catch {
45883
46363
  return null;
45884
46364
  }
@@ -46268,7 +46748,7 @@ async function runMode(deps) {
46268
46748
  served = await deps.serve(agent, {
46269
46749
  token,
46270
46750
  allowOrigins: daemon.allowOrigins,
46271
- readTls: () => daemon.tls ? { cert: readFileSync16(daemon.tlsCert, "utf8"), key: readFileSync16(daemon.tlsKey, "utf8") } : void 0
46751
+ readTls: () => daemon.tls ? { cert: readFileSync17(daemon.tlsCert, "utf8"), key: readFileSync17(daemon.tlsKey, "utf8") } : void 0
46272
46752
  });
46273
46753
  } catch (e) {
46274
46754
  if (!(e instanceof SessionSupersededError)) throw e;