@securecoms/agent-bridge 0.30.1 → 0.30.3

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
@@ -17457,7 +17457,13 @@ var CODES = [
17457
17457
  // lookup missed and before any write, so a retry can never land: the
17458
17458
  // pending commit is dropped instead of being resent forever. The same pair
17459
17459
  // answers an application send into a deleted channel (#325 F5).
17460
- { status: 409, code: "channel_unavailable" }
17460
+ { status: 409, code: "channel_unavailable" },
17461
+ // ADR-0023 B.1: an Add or Welcome into a humans-only enclave's group naming
17462
+ // a device that is not provably a person's. Refused after the replay lookup
17463
+ // missed and before any write, so nothing landed. Definite: the committer's
17464
+ // work cannot name an agent in a humans-only enclave (admission refuses
17465
+ // one), so dropping the commit starts no resend loop.
17466
+ { status: 403, code: "humans_only_enclave" }
17461
17467
  ];
17462
17468
  var DISTRIBUTE_DEFINITE_CODES = Object.freeze(CODES.map((c) => Object.freeze({ ...c })));
17463
17469
  var DEFINITE_KEYS = new Set(DISTRIBUTE_DEFINITE_CODES.map((c) => `${c.status} ${c.code}`));
@@ -29095,7 +29101,7 @@ function loadPackageMeta(importMetaUrl) {
29095
29101
  }
29096
29102
 
29097
29103
  // src/bridge-agent.ts
29098
- import { existsSync as existsSync5, promises as fsp, statSync as statSync4 } from "node:fs";
29104
+ import { existsSync as existsSync5, promises as fsp, readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
29099
29105
  import { join as join17, dirname as dirname5 } from "node:path";
29100
29106
  import { fileURLToPath as fileURLToPath2 } from "node:url";
29101
29107
 
@@ -34026,6 +34032,15 @@ import {
34026
34032
  writeFileSync
34027
34033
  } from "node:fs";
34028
34034
  import { basename as basename4, isAbsolute, join as join15 } from "node:path";
34035
+
34036
+ // src/grants/search-providers.ts
34037
+ var SEARCH_PROVIDERS = /* @__PURE__ */ new Set(["brave", "tavily", "parallel", "exa", "firecrawl"]);
34038
+ var OWNER_DM_SEARCH_DAILY_CAP_MAX = 2e3;
34039
+ function isSearchProvider(value) {
34040
+ return typeof value === "string" && SEARCH_PROVIDERS.has(value);
34041
+ }
34042
+
34043
+ // src/service/runtime-config-dir.ts
34029
34044
  var MEMORY_FILE = "MEMORY.md";
34030
34045
  var REAL_FS = {
34031
34046
  mkdir: (p, mode) => mkdirSync5(p, { recursive: true, mode }),
@@ -34208,10 +34223,14 @@ function readLocalReport(home, identityId) {
34208
34223
  const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
34209
34224
  Object.entries(raw).filter(([, v]) => typeof v === "string")
34210
34225
  ) : {};
34226
+ const capRaw = doc.owner_dm_search_cap_effective;
34227
+ const ownerDmCap = typeof capRaw === "number" && Number.isInteger(capRaw) && capRaw >= 0 && capRaw <= OWNER_DM_SEARCH_DAILY_CAP_MAX ? capRaw : void 0;
34211
34228
  return {
34212
34229
  persona,
34213
34230
  has_search_key: hasSearchKey,
34214
34231
  has_llm_key: hasLlmKey,
34232
+ has_extract_provider: doc.has_extract_provider === true,
34233
+ ...ownerDmCap !== void 0 ? { owner_dm_search_cap_effective: ownerDmCap } : {},
34215
34234
  legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source),
34216
34235
  secret_paths: parseSecretPathsReport(doc.secret_paths)
34217
34236
  };
@@ -34410,7 +34429,8 @@ function loadOwnerGrantPin(opts) {
34410
34429
  // src/grants/manifest.ts
34411
34430
  var TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
34412
34431
  var MANIFEST_KNOWN_TOOLS = Object.freeze({
34413
- owner_dm: Object.freeze(["web_search", "fetch_url", "http_request", "sql_query"]),
34432
+ owner_dm: Object.freeze(["web_search", "web_extract", "fetch_url", "http_request", "sql_query"]),
34433
+ // web_extract: owner DM only, never team_room (spec §5.4, D5)
34414
34434
  team_room: Object.freeze(["web_search"])
34415
34435
  });
34416
34436
  function manifestSignedBytes(manifest) {
@@ -34485,7 +34505,11 @@ function parseConfig(v) {
34485
34505
  }
34486
34506
  if ("search" in v) {
34487
34507
  const sr = v.search;
34488
- if (!isPlainObject(sr) || !exactKeys(sr, ["provider"]) || sr.provider !== "brave" && sr.provider !== "tavily") return null;
34508
+ if (!isPlainObject(sr) || !exactKeys(sr, ["provider"], ["owner_dm_daily_cap"]) || !isSearchProvider(sr.provider)) return null;
34509
+ if ("owner_dm_daily_cap" in sr) {
34510
+ const cap = sr.owner_dm_daily_cap;
34511
+ if (typeof cap !== "number" || !Number.isSafeInteger(cap) || cap < 0 || cap > OWNER_DM_SEARCH_DAILY_CAP_MAX) return null;
34512
+ }
34489
34513
  }
34490
34514
  const mem = v.memory;
34491
34515
  if (!isPlainObject(mem) || !exactKeys(mem, ["mode"], ["recall_days"])) return null;
@@ -34495,7 +34519,12 @@ function parseConfig(v) {
34495
34519
  return Object.freeze({
34496
34520
  persona_sha256: v.persona_sha256,
34497
34521
  ...isPlainObject(v.llm) ? { llm: Object.freeze({ provider: v.llm.provider, model: v.llm.model }) } : {},
34498
- ...isPlainObject(v.search) ? { search: Object.freeze({ provider: v.search.provider }) } : {},
34522
+ ...isPlainObject(v.search) ? {
34523
+ search: Object.freeze({
34524
+ provider: v.search.provider,
34525
+ ..."owner_dm_daily_cap" in v.search ? { owner_dm_daily_cap: v.search.owner_dm_daily_cap } : {}
34526
+ })
34527
+ } : {},
34499
34528
  ...isStringArray(v.tools_allow) ? { tools_allow: Object.freeze([...v.tools_allow]) } : {},
34500
34529
  flags: Object.freeze([...v.flags]),
34501
34530
  memory: Object.freeze({ mode: mem.mode, ..."recall_days" in mem ? { recall_days: mem.recall_days } : {} }),
@@ -34726,7 +34755,20 @@ function bindTargets(raw, signed, equals) {
34726
34755
  }
34727
34756
  return { written, mismatches };
34728
34757
  }
34729
- function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true }) {
34758
+ function signedManifestTools(manifest, audience, searchProvidersCapable) {
34759
+ return MANIFEST_KNOWN_TOOLS[audience].filter((t) => (manifest.grants[audience] ?? []).includes(t) && (t !== "web_extract" || searchProvidersCapable));
34760
+ }
34761
+ function manifestPolicyFor(manifest, switches, searchProvidersCapable) {
34762
+ return {
34763
+ v: 1,
34764
+ manifest_tools: [.../* @__PURE__ */ new Set([
34765
+ ...switches.ownerDm ? signedManifestTools(manifest, "owner_dm", searchProvidersCapable) : [],
34766
+ ...switches.room ? signedManifestTools(manifest, "team_room", searchProvidersCapable) : []
34767
+ ])].sort(),
34768
+ secret_path_mode: "enforce"
34769
+ };
34770
+ }
34771
+ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true }, searchProvidersCapable = false) {
34730
34772
  const mismatches = /* @__PURE__ */ new Set();
34731
34773
  const cfg = manifest.config;
34732
34774
  const out = {};
@@ -34755,6 +34797,7 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
34755
34797
  for (const name of Object.keys(files)) {
34756
34798
  if (name !== LLM_SOURCE && !PERSONA_FILES.includes(name)) mismatches.add("files");
34757
34799
  }
34800
+ const ownerDmTools = signedManifestTools(manifest, "owner_dm", searchProvidersCapable);
34758
34801
  const sj = isObj2(payload.skills_json) ? payload.skills_json : void 0;
34759
34802
  const eps = bindTargets(sj?.endpoints, manifest.endpoints, endpointEquals);
34760
34803
  const dbs = bindTargets(sj?.databases, manifest.databases, databaseEquals);
@@ -34772,14 +34815,20 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
34772
34815
  appliedSearch = searchBlock;
34773
34816
  } else {
34774
34817
  mismatches.add("search");
34775
- searchBlock = (prior.search?.provider ?? void 0) === cfg.search?.provider ? prior.search : void 0;
34818
+ const usablePrior = cfg.search !== void 0 && prior.search?.provider === cfg.search.provider ? prior.search : void 0;
34819
+ searchBlock = cfg.search === void 0 ? void 0 : usablePrior ?? Object.freeze({ provider: cfg.search.provider });
34776
34820
  }
34777
34821
  const skillsDoc = {
34778
34822
  endpoints: eps.written,
34779
34823
  databases: dbs.written,
34780
34824
  flags: [...cfg.flags],
34781
34825
  ...deny !== void 0 || cfg.tools_allow !== void 0 ? { tools: { ...deny !== void 0 ? { deny } : {}, ...cfg.tools_allow !== void 0 ? { allow: [...cfg.tools_allow] } : {} } } : {},
34782
- ...searchBlock ? { search: searchBlock } : {}
34826
+ ...searchBlock ? { search: searchBlock } : {},
34827
+ // D31: the marker the runtime keys D24 (iii) on; written under every manifest, true when config.search is signed.
34828
+ search_signed: cfg.search !== void 0,
34829
+ // D36, D39: the signed owner-DM cap, only when the owner-DM switch is on and the manifest grants an owner-DM tool;
34830
+ // otherwise none, so the runtime clamps a signed raise to the default. Never read from the payload.
34831
+ ...cfg.search?.owner_dm_daily_cap !== void 0 && switches.ownerDm && ownerDmTools.length > 0 ? { owner_dm_search_cap: cfg.search.owner_dm_daily_cap } : {}
34783
34832
  };
34784
34833
  out["skills.json"] = JSON.stringify(skillsDoc, null, 2) + "\n";
34785
34834
  const signedMemory = { mode: cfg.memory.mode, ...cfg.memory.recall_days !== void 0 ? { recall_days: cfg.memory.recall_days } : {} };
@@ -34810,12 +34859,7 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
34810
34859
  mismatches.add("legacy_assign");
34811
34860
  legacyDecisions = decisions.filter((d) => isObj2(d) && d.action === "delete");
34812
34861
  }
34813
- const signedTools = (a) => MANIFEST_KNOWN_TOOLS[a].filter((t) => (manifest.grants[a] ?? []).includes(t));
34814
- const policy = {
34815
- v: 1,
34816
- manifest_tools: [.../* @__PURE__ */ new Set([...switches.ownerDm ? signedTools("owner_dm") : [], ...switches.room ? signedTools("team_room") : []])].sort(),
34817
- secret_path_mode: "enforce"
34818
- };
34862
+ const policy = manifestPolicyFor(manifest, switches, searchProvidersCapable);
34819
34863
  return {
34820
34864
  mismatches: CONFIG_BINDING_FIELDS.filter((f) => mismatches.has(f)).sort(),
34821
34865
  targetMismatches: { endpoint: eps.mismatches, database: dbs.mismatches },
@@ -34954,6 +34998,8 @@ var TOOL_ROOM_CLASSES = Object.freeze({
34954
34998
  current_time: Object.freeze(["room_safe_read"]),
34955
34999
  echo: Object.freeze(["room_safe_read"]),
34956
35000
  web_search: Object.freeze(["owner_credential"]),
35001
+ web_extract: Object.freeze(["owner_credential", "egress"]),
35002
+ // D5: spends the owner's key and sends URLs out
34957
35003
  fetch_url: Object.freeze(["egress"]),
34958
35004
  sql_query: Object.freeze(["owner_credential"]),
34959
35005
  http_request: Object.freeze(["owner_credential", "egress", "side_effect"])
@@ -35375,6 +35421,8 @@ function hexToBytes4(h) {
35375
35421
  return new Uint8Array(Buffer.from(h, "hex"));
35376
35422
  }
35377
35423
  var BOOT_DRAIN_WAIT_MS_DEFAULT = 6e4;
35424
+ var SEARCH_CAPABILITY_REFETCH_DEBOUNCE_MS = 1e3;
35425
+ var SEARCH_CAPABILITY_REFETCH_MAX_WAIT_MS = 1e4;
35378
35426
  var BootDrainRotationRefusedError = class extends Error {
35379
35427
  constructor() {
35380
35428
  super("key-package rotation refused: the boot drain has not settled");
@@ -35570,6 +35618,8 @@ var BridgeAgent = class _BridgeAgent {
35570
35618
  this.actionClock = deps.actionThrottleNow ?? Date.now;
35571
35619
  this.rng = deps.rng ?? Math.random;
35572
35620
  this.bootDrainWaitMs = deps.bootDrainWaitMs ?? BOOT_DRAIN_WAIT_MS_DEFAULT;
35621
+ this.searchCapabilityRefetchDebounceMs = deps.searchCapabilityRefetchDebounceMs ?? SEARCH_CAPABILITY_REFETCH_DEBOUNCE_MS;
35622
+ this.searchCapabilityRefetchMaxWaitMs = deps.searchCapabilityRefetchMaxWaitMs ?? SEARCH_CAPABILITY_REFETCH_MAX_WAIT_MS;
35573
35623
  this.replySleep = deps.replySleep ?? ((ms) => new Promise((resolve) => {
35574
35624
  const t = setTimeout(resolve, ms);
35575
35625
  t.unref?.();
@@ -36008,6 +36058,8 @@ var BridgeAgent = class _BridgeAgent {
36008
36058
  bootSweepTimer;
36009
36059
  rng;
36010
36060
  bootDrainWaitMs;
36061
+ searchCapabilityRefetchDebounceMs;
36062
+ searchCapabilityRefetchMaxWaitMs;
36011
36063
  /** Resolved (and cleared) each time an ingest cycle settles: see start(). */
36012
36064
  ingestSettledWaiters = [];
36013
36065
  /** The boot key-package publish, when start() had to defer it. Test seam. */
@@ -36453,9 +36505,14 @@ var BridgeAgent = class _BridgeAgent {
36453
36505
  }
36454
36506
  const skillsJson = payload.skills_json ?? {};
36455
36507
  if (version && inForce === null) {
36508
+ let asDelivered = skillsJson;
36509
+ if (skillsJson !== null && typeof skillsJson === "object" && !Array.isArray(skillsJson)) {
36510
+ const { owner_dm_search_cap: _cap, search_signed: _signed, ...rest } = skillsJson;
36511
+ asDelivered = rest;
36512
+ }
36456
36513
  this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, {
36457
36514
  ...payload.files ?? {},
36458
- "skills.json": `${JSON.stringify(skillsJson, null, 2)}
36515
+ "skills.json": `${JSON.stringify(asDelivered, null, 2)}
36459
36516
  `
36460
36517
  });
36461
36518
  }
@@ -36510,7 +36567,7 @@ var BridgeAgent = class _BridgeAgent {
36510
36567
  }
36511
36568
  let bound;
36512
36569
  try {
36513
- bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants });
36570
+ bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.searchProvidersCapable());
36514
36571
  } catch (e) {
36515
36572
  bound = null;
36516
36573
  return this.bindFailed(manifest, payload.memory, e);
@@ -36529,9 +36586,9 @@ var BridgeAgent = class _BridgeAgent {
36529
36586
  return true;
36530
36587
  }
36531
36588
  try {
36532
- this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, bound.policy);
36533
36589
  this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, bound.files);
36534
36590
  if (manifest.config.llm === void 0) this.grantWriters.removeManagedLlmSource(this.home, this.cfg.identityId);
36591
+ this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.searchProvidersCapable()));
36535
36592
  } catch (e) {
36536
36593
  this.manifestBinding = null;
36537
36594
  throw e;
@@ -36584,6 +36641,7 @@ var BridgeAgent = class _BridgeAgent {
36584
36641
  /** The report half of refetchRuntimeConfigOnce (split out for the plan 4 Task 11 bind_failed path). */
36585
36642
  async postRuntimeConfigReport(base, version, skillsJson) {
36586
36643
  const local = readLocalReport(this.home, this.cfg.identityId);
36644
+ const reportedSearchProviders = this.searchProvidersCapable();
36587
36645
  const report = {
36588
36646
  applied_version: version,
36589
36647
  paths: checkConfiguredPaths(skillsJson),
@@ -36598,6 +36656,13 @@ var BridgeAgent = class _BridgeAgent {
36598
36656
  // to the backend. The backend needs it to generate a truthful tool
36599
36657
  // paragraph, so it rides along here.
36600
36658
  has_search_key: local?.has_search_key ?? false,
36659
+ // L-2 (spec §7.9): only the runtime knows whether an extract-capable provider key is readable; never derived
36660
+ // from has_search_key. An older runtime omits it: false.
36661
+ // Final review I-2 (controller ruling): AND the current search_providers_v2, so the web never shows extract while
36662
+ // the bridge withholds web_extract from the runtime.
36663
+ has_extract_provider: (local?.has_extract_provider ?? false) && reportedSearchProviders,
36664
+ // D39: the owner-DM daily cap the runtime enforces, passed through when it reported a valid one; never the signed value.
36665
+ ...local?.owner_dm_search_cap_effective !== void 0 ? { owner_dm_search_cap_effective: local.owner_dm_search_cap_effective } : {},
36601
36666
  // Same idea, for the BYOK LLM key file: only the runtime can see
36602
36667
  // whether one is configured in its own environment.
36603
36668
  has_llm_key: local?.has_llm_key ?? false,
@@ -36637,7 +36702,11 @@ var BridgeAgent = class _BridgeAgent {
36637
36702
  // Plan 4 Task 15 (OQ-5): sorted, unique enums; capability is the latest hello's class.
36638
36703
  config_binding_mismatches: [...this.configBindingMismatches],
36639
36704
  target_mismatches: ["database", "endpoint"].filter((k) => this.targetMismatches[k] > 0),
36640
- capability: this.lastConsumerCapability
36705
+ capability: this.lastConsumerCapability,
36706
+ // D28 (§11.1 step 3): a separate key, never a value in the capability enum. False with no runtime attached.
36707
+ search_providers_v2: reportedSearchProviders,
36708
+ // D19 item 4: content-free; the signed provider is one the attached runtime(s) cannot honour.
36709
+ search_provider_unsupported: this.searchProviderUnsupported(reportedSearchProviders)
36641
36710
  }
36642
36711
  };
36643
36712
  const reportedVerifyFailures = this.ownerVerifyFailures;
@@ -36658,6 +36727,7 @@ var BridgeAgent = class _BridgeAgent {
36658
36727
  return;
36659
36728
  }
36660
36729
  this.ownerVerifyFailures = Math.max(0, this.ownerVerifyFailures - reportedVerifyFailures);
36730
+ this.lastReportedSearchProviders = reportedSearchProviders;
36661
36731
  if (version) writeServiceLog2("info", "runtime_config.applied", { version });
36662
36732
  else writeServiceLog2("info", "runtime_config.legacy_memory_reported");
36663
36733
  }
@@ -36727,6 +36797,7 @@ var BridgeAgent = class _BridgeAgent {
36727
36797
  this.startedAt = Date.now();
36728
36798
  await this.restoreRuntimeInbox();
36729
36799
  await this.loadToolGrantState();
36800
+ this.recomputeManifestPolicy(false);
36730
36801
  try {
36731
36802
  this.store.pruneRoomSearchCap(utcDayOf(Date.now()));
36732
36803
  } catch (e) {
@@ -42518,6 +42589,115 @@ ${channelId}`;
42518
42589
  manifestConsumerAttached() {
42519
42590
  return this.manifestConsumer !== null;
42520
42591
  }
42592
+ /**
42593
+ * D28 (B-4): every socket that said hello, with whether its latest hello declared search_providers_v2 on the
42594
+ * manifest_grants_v1 predicate (ws-server.ts). The agent value is the AND over all of them, absent (false) with
42595
+ * none: unlike manifestConsumer, never newest wins. The per-delivery filter judges each socket by its own hello.
42596
+ */
42597
+ searchProvidersSockets = /* @__PURE__ */ new Map();
42598
+ /**
42599
+ * The D28 value the files were last recomputed for; null until the first socket observation, so that observation
42600
+ * always recomputes (final review I-1), even when it is non-capable.
42601
+ */
42602
+ searchProvidersState = null;
42603
+ searchCapabilityRefetchTimer = null;
42604
+ /** When the current burst's first change happened (valid while the timer is set). */
42605
+ searchCapabilityBurstStart = 0;
42606
+ /** The D28 value in the last report posted (null until one is); a change needs a refetch to reach the web. */
42607
+ lastReportedSearchProviders = null;
42608
+ noteSearchProvidersSocket(token, declared) {
42609
+ this.searchProvidersSockets.set(token, declared);
42610
+ this.onSearchProvidersMaybeChanged();
42611
+ }
42612
+ clearSearchProvidersSocket(token) {
42613
+ if (this.searchProvidersSockets.delete(token)) this.onSearchProvidersMaybeChanged();
42614
+ }
42615
+ /** D28: every attached runtime socket declared search_providers_v2; no socket attached means absent (false). */
42616
+ searchProvidersCapable() {
42617
+ if (this.searchProvidersSockets.size === 0) return false;
42618
+ for (const v of this.searchProvidersSockets.values()) if (!v) return false;
42619
+ return true;
42620
+ }
42621
+ /**
42622
+ * D19 item 3 (B-6): the search provider signed in the stored verified manifest in force (a pin loaded and a good
42623
+ * manifest), never the server payload; undefined with no pin, no manifest, or no signed search block. The
42624
+ * per-delivery filter (consumer-grants.ts withSearchCapability) and the S-28 report reason both read this.
42625
+ */
42626
+ signedSearchProvider() {
42627
+ const pin = this.cfg.ownerGrantPin?.pin ?? null;
42628
+ return pin !== null ? this.toolGrantManifest?.config.search?.provider : void 0;
42629
+ }
42630
+ /** D19 item 4, D34: the in-force signed provider is not brave, and the attached runtime(s) lack the capability. */
42631
+ searchProviderUnsupported(capable) {
42632
+ if (capable) return false;
42633
+ const provider = this.signedSearchProvider();
42634
+ return provider !== void 0 && provider !== "brave";
42635
+ }
42636
+ /**
42637
+ * D35 (P-4): on a capability change, rewrite manifest-policy.json at once from the stored manifest, the kill
42638
+ * switches and the new capability, then a debounced refetch (one per burst) only when needed.
42639
+ */
42640
+ onSearchProvidersMaybeChanged() {
42641
+ const capable = this.searchProvidersCapable();
42642
+ const prev = this.searchProvidersState;
42643
+ if (capable === prev) return;
42644
+ this.searchProvidersState = capable;
42645
+ this.recomputeManifestPolicy(capable);
42646
+ if (this.closingDown) return;
42647
+ if (prev === null && !capable) return;
42648
+ const now = Date.now();
42649
+ if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
42650
+ else this.searchCapabilityBurstStart = now;
42651
+ const delay = Math.max(0, Math.min(this.searchCapabilityRefetchDebounceMs, this.searchCapabilityBurstStart + this.searchCapabilityRefetchMaxWaitMs - now));
42652
+ this.searchCapabilityRefetchTimer = setTimeout(() => {
42653
+ this.searchCapabilityRefetchTimer = null;
42654
+ if (this.searchCapabilityRefetchNeeded()) void this.refetchRuntimeConfig();
42655
+ }, delay);
42656
+ this.searchCapabilityRefetchTimer.unref?.();
42657
+ }
42658
+ /**
42659
+ * A refetch re-renders skills.json (owner_dm_search_cap counts web_extract only with the capability, D39) and posts
42660
+ * the report. Needed under a manifest in force, or when the reported value is stale. Never before the boot fetch.
42661
+ */
42662
+ searchCapabilityRefetchNeeded() {
42663
+ if (!this.bootConfigSettled || this.closingDown) return false;
42664
+ const inForce = (this.cfg.ownerGrantPin?.pin ?? null) !== null && this.toolGrantManifest !== null;
42665
+ return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable();
42666
+ }
42667
+ /**
42668
+ * D35: the local recompute. Widening (web_extract back in) only while the binding for the in-force manifest stands,
42669
+ * so a policy is never written ahead of its skills.json. Narrowing never waits: with no binding in force, a file that
42670
+ * lists web_extract loses it at once, so an older runtime never reads it as unreadable (which drops every grant).
42671
+ * Content-free; a write failure is logged and the debounced refetch tries again.
42672
+ */
42673
+ recomputeManifestPolicy(capable) {
42674
+ const pin = this.cfg.ownerGrantPin?.pin ?? null;
42675
+ const manifest = pin !== null ? this.toolGrantManifest : null;
42676
+ try {
42677
+ if (manifest !== null && this.manifestBinding !== null && this.manifestBinding.counter === manifest.counter) {
42678
+ this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, capable));
42679
+ return;
42680
+ }
42681
+ if (capable) return;
42682
+ const path = join17(runtimeConfigDir(this.home, this.cfg.identityId), MANIFEST_POLICY_FILE);
42683
+ if (!existsSync5(path)) return;
42684
+ let doc;
42685
+ try {
42686
+ doc = JSON.parse(readFileSync10(path, "utf8"));
42687
+ } catch {
42688
+ return;
42689
+ }
42690
+ const tools = doc?.manifest_tools;
42691
+ if (!Array.isArray(tools) || !tools.includes("web_extract")) return;
42692
+ this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, {
42693
+ v: 1,
42694
+ manifest_tools: tools.filter((t) => typeof t === "string" && t !== "web_extract"),
42695
+ secret_path_mode: "enforce"
42696
+ });
42697
+ } catch (e) {
42698
+ writeServiceLog2("warn", "authz.search_capability", { state: ServiceLogEnum.of("policy_write_failed", ["policy_write_failed"]) }, e);
42699
+ }
42700
+ }
42521
42701
  noteConsumerCapability(state) {
42522
42702
  this.lastConsumerCapability = state;
42523
42703
  if (state !== "ok") {
@@ -42709,6 +42889,8 @@ ${channelId}`;
42709
42889
  this.supersedeRecheckTimers.clear();
42710
42890
  if (this.redeliveryTimer !== null) clearTimeout(this.redeliveryTimer);
42711
42891
  this.redeliveryTimer = null;
42892
+ if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
42893
+ this.searchCapabilityRefetchTimer = null;
42712
42894
  this.pingHandle?.close();
42713
42895
  if (this.safetyTimer) clearInterval(this.safetyTimer);
42714
42896
  if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
@@ -42818,11 +43000,33 @@ ${channelId}`;
42818
43000
  };
42819
43001
 
42820
43002
  // src/config.ts
42821
- import { readFileSync as readFileSync10, statSync as statSync5 } from "node:fs";
43003
+ import { readFileSync as readFileSync11, statSync as statSync5 } from "node:fs";
42822
43004
  import { dirname as dirname6, join as join18 } from "node:path";
42823
43005
  var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
43006
+ var CONFIG_ERROR_MESSAGE = {
43007
+ credential_not_found: "credential file not found",
43008
+ credential_unreadable: "credential file could not be read",
43009
+ credential_not_json: "credential file is not valid JSON",
43010
+ credential_missing_field: "credential file missing required field",
43011
+ credential_voice_invalid: "credential file voice config invalid"
43012
+ };
43013
+ var ConfigError = class extends Error {
43014
+ constructor(reason, field, source) {
43015
+ super(CONFIG_ERROR_MESSAGE[reason] + (field ? `: ${field}` : ""));
43016
+ this.reason = reason;
43017
+ this.field = field;
43018
+ this.source = source;
43019
+ this.name = "ConfigError";
43020
+ }
43021
+ };
42824
43022
  function loadConfig(opts) {
42825
- const raw = readFileSync10(opts.credentialPath, "utf8");
43023
+ let raw;
43024
+ try {
43025
+ raw = readFileSync11(opts.credentialPath, "utf8");
43026
+ } catch (e) {
43027
+ const code = e?.code;
43028
+ throw new ConfigError(code === "ENOENT" ? "credential_not_found" : "credential_unreadable");
43029
+ }
42826
43030
  try {
42827
43031
  const mode = statSync5(opts.credentialPath).mode & 511;
42828
43032
  if (mode & 63) writeServiceLog2("warn", "config.credential_permissions_unsafe");
@@ -42832,9 +43036,9 @@ function loadConfig(opts) {
42832
43036
  try {
42833
43037
  json2 = JSON.parse(raw);
42834
43038
  } catch {
42835
- throw new Error("credential file is not valid JSON");
43039
+ throw new ConfigError("credential_not_json");
42836
43040
  }
42837
- for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new Error(`credential file missing required field: ${k}`);
43041
+ for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new ConfigError("credential_missing_field", k);
42838
43042
  const storePath = opts.storePath ?? join18(dirname6(opts.credentialPath), "bridge-state.db");
42839
43043
  const ownerGrantPin = loadOwnerGrantPin({ credentialPath: opts.credentialPath, credentialJson: json2 });
42840
43044
  writeServiceLog2(ownerGrantPin.state === "ok" || ownerGrantPin.state === "no_pin" ? "info" : "warn", "authz.pin", {
@@ -42872,8 +43076,14 @@ function loadConfig(opts) {
42872
43076
  let voice;
42873
43077
  try {
42874
43078
  voice = parseVoiceConfig(json2.voice, process.env);
42875
- } catch (e) {
42876
- throw new Error(`credential file voice config invalid: ${e.message}`);
43079
+ } catch {
43080
+ let source = "env";
43081
+ try {
43082
+ parseVoiceConfig(json2.voice, {});
43083
+ } catch {
43084
+ source = "file";
43085
+ }
43086
+ throw new ConfigError("credential_voice_invalid", void 0, source);
42877
43087
  }
42878
43088
  const modelSource = json2.memory?.model_source === "import_only" ? "import_only" : "download";
42879
43089
  const enabledRaw = json2.memory?.enabled;
@@ -42942,11 +43152,11 @@ import { z as z3 } from "zod";
42942
43152
 
42943
43153
  // src/auth.ts
42944
43154
  import { randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
42945
- import { existsSync as existsSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync2 } from "node:fs";
43155
+ import { existsSync as existsSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync2 } from "node:fs";
42946
43156
  function resolveToken(opts) {
42947
43157
  if (opts.token) return opts.token;
42948
43158
  const f = opts.tokenFile;
42949
- if (f && existsSync6(f)) return readFileSync11(f, "utf8").trim();
43159
+ if (f && existsSync6(f)) return readFileSync12(f, "utf8").trim();
42950
43160
  const tok = randomBytes11(32).toString("base64url");
42951
43161
  if (f) writeFileSync2(f, tok, { mode: 384 });
42952
43162
  return tok;
@@ -43042,6 +43252,7 @@ function withConsumerGrants(m, perToolConsumer) {
43042
43252
  return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
43043
43253
  }
43044
43254
  var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
43255
+ var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
43045
43256
  var CONSTANT_TOOL_NAMES = Object.freeze({
43046
43257
  owner_dm: Object.freeze(["current_time", "web_search"]),
43047
43258
  team_room: Object.freeze(["current_time"])
@@ -43064,6 +43275,15 @@ function withHarnessGrants(m, withheld) {
43064
43275
  if (!a || a.tool_grants.length === 0) return m;
43065
43276
  return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
43066
43277
  }
43278
+ function withSearchCapability(m, consumerHasFeature, signedProvider) {
43279
+ if (consumerHasFeature) return m;
43280
+ const a = m.authorization;
43281
+ if (!a) return m;
43282
+ const stripSearch = signedProvider !== void 0 && signedProvider !== "brave";
43283
+ const kept = a.tool_grants.filter((g) => g.tool !== "web_extract" && !(stripSearch && g.tool === "web_search"));
43284
+ if (kept.length === a.tool_grants.length) return m;
43285
+ return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
43286
+ }
43067
43287
 
43068
43288
  // src/http-server.ts
43069
43289
  function writeSse(res, msgs, onWriteError) {
@@ -43086,7 +43306,8 @@ data: ${JSON.stringify(msgs[i])}
43086
43306
  }
43087
43307
  function deliverSseBatch(res, batch, agent, onWriteError) {
43088
43308
  const withheld = agent.harnessGrantsWithheld();
43089
- const left3 = writeSse(res, batch.map((m) => withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false)), onWriteError);
43309
+ const provider = agent.signedSearchProvider();
43310
+ const left3 = writeSse(res, batch.map((m) => withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider)), onWriteError);
43090
43311
  const leftIds = new Set(left3.map((message) => message.message_id));
43091
43312
  agent.ackRuntimeMessages(batch.filter((message) => !leftIds.has(message.message_id)).map((message) => message.message_id));
43092
43313
  if (left3.length) agent.requeueFront(batch.filter((message) => leftIds.has(message.message_id)));
@@ -43357,7 +43578,10 @@ function createHttpServer(agent, opts) {
43357
43578
  if (method === "GET" && path === "/v1/messages") {
43358
43579
  const channel = url.searchParams.get("channel_id") ?? void 0;
43359
43580
  const wait = Math.max(0, Math.min(50, Number(url.searchParams.get("wait") ?? 0)));
43360
- return json(res, 200, (await agent.drain(channel, wait)).map((m) => withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), agent.harnessGrantsWithheld()), false)));
43581
+ const drained = await agent.drain(channel, wait);
43582
+ const withheld = agent.harnessGrantsWithheld();
43583
+ const provider = agent.signedSearchProvider();
43584
+ return json(res, 200, drained.map((m) => withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider)));
43361
43585
  }
43362
43586
  if (method === "GET" && path === "/v1/events") {
43363
43587
  const channel = url.searchParams.get("channel_id") ?? void 0;
@@ -43531,6 +43755,7 @@ function attachAgentWebSocket(server, agent, opts) {
43531
43755
  consumer: "unknown",
43532
43756
  perToolGrants: false,
43533
43757
  manifestGrants: false,
43758
+ searchProviders: false,
43534
43759
  // helloWaitMs 0 keeps today's synchronous initial flush exactly (the opt-out the existing tests use).
43535
43760
  deliveryOpen: helloWaitMs <= 0,
43536
43761
  deliveryToken: /* @__PURE__ */ new Map(),
@@ -43584,9 +43809,13 @@ function attachAgentWebSocket(server, agent, opts) {
43584
43809
  continue;
43585
43810
  }
43586
43811
  const out = withConsumerNotes(
43587
- withManifestCapability(
43588
- withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
43589
- session.manifestGrants
43812
+ withSearchCapability(
43813
+ withManifestCapability(
43814
+ withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
43815
+ session.manifestGrants
43816
+ ),
43817
+ session.searchProviders,
43818
+ agent.signedSearchProvider()
43590
43819
  ),
43591
43820
  batch[i],
43592
43821
  session,
@@ -43622,6 +43851,7 @@ function attachAgentWebSocket(server, agent, opts) {
43622
43851
  const cleanup = () => {
43623
43852
  if (cleaned) return;
43624
43853
  cleaned = true;
43854
+ session.closed = true;
43625
43855
  if (helloTimer) clearTimeout(helloTimer);
43626
43856
  agent.noteSessionClose();
43627
43857
  unsubscribe();
@@ -43642,6 +43872,7 @@ function attachAgentWebSocket(server, agent, opts) {
43642
43872
  if (session.harness) agent.clearHarnessReport(session.harness);
43643
43873
  if (session.perToolGrants) agent.clearPerToolConsumer(session);
43644
43874
  if (session.manifestGrants) agent.clearManifestConsumer(session);
43875
+ agent.clearSearchProvidersSocket(session);
43645
43876
  };
43646
43877
  current = { sock, cleanup, sweepStale };
43647
43878
  sock.on("close", cleanup);
@@ -43717,6 +43948,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
43717
43948
  return;
43718
43949
  }
43719
43950
  if (m.type === "hello") {
43951
+ if (session.closed) return;
43720
43952
  const report = m.harness === void 0 ? null : parseHarnessReport(m.harness);
43721
43953
  if (report) {
43722
43954
  session.harness = report;
@@ -43740,6 +43972,8 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
43740
43972
  agent.clearManifestConsumer(session);
43741
43973
  }
43742
43974
  agent.noteConsumerCapability(manifest ? "ok" : session.harnessClaimed ? "harness_consumer" : "missing");
43975
+ session.searchProviders = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(SEARCH_PROVIDERS_FEATURE);
43976
+ agent.noteSearchProvidersSocket(session, session.searchProviders);
43743
43977
  if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
43744
43978
  agent.noteNotesConsumer();
43745
43979
  }
@@ -43747,7 +43981,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
43747
43981
  if (session.recallConsumer) agent.noteRecallConsumer();
43748
43982
  session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
43749
43983
  if (session.unifiedConsumer) agent.noteUnifiedConsumer();
43750
- sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE] });
43984
+ sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE, SEARCH_PROVIDERS_FEATURE] });
43751
43985
  if (!session.deliveryOpen) {
43752
43986
  session.deliveryOpen = true;
43753
43987
  flush();
@@ -1,3 +1,4 @@
1
+ export declare const DIST_BASE = "https://nodejs.org/dist/latest-v24.x";
1
2
  export declare function parseShasums(text: string, arch: string): {
2
3
  filename: string;
3
4
  sha256: string;
@@ -14,8 +15,6 @@ export interface UpgradeDeps {
14
15
  isTTY: boolean;
15
16
  stderr: (s: string) => void;
16
17
  ask: (q: string) => Promise<string>;
17
- hasBrew: () => boolean;
18
- runBrew: () => number;
19
18
  fetchText: (url: string) => Promise<string>;
20
19
  fetchBinary: (url: string, destPath: string) => Promise<void>;
21
20
  fileSha256: (p: string) => string;