@securecoms/agent-bridge 0.30.2 → 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
@@ -29101,7 +29101,7 @@ function loadPackageMeta(importMetaUrl) {
29101
29101
  }
29102
29102
 
29103
29103
  // src/bridge-agent.ts
29104
- 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";
29105
29105
  import { join as join17, dirname as dirname5 } from "node:path";
29106
29106
  import { fileURLToPath as fileURLToPath2 } from "node:url";
29107
29107
 
@@ -34032,6 +34032,15 @@ import {
34032
34032
  writeFileSync
34033
34033
  } from "node:fs";
34034
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
34035
34044
  var MEMORY_FILE = "MEMORY.md";
34036
34045
  var REAL_FS = {
34037
34046
  mkdir: (p, mode) => mkdirSync5(p, { recursive: true, mode }),
@@ -34214,10 +34223,14 @@ function readLocalReport(home, identityId) {
34214
34223
  const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
34215
34224
  Object.entries(raw).filter(([, v]) => typeof v === "string")
34216
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;
34217
34228
  return {
34218
34229
  persona,
34219
34230
  has_search_key: hasSearchKey,
34220
34231
  has_llm_key: hasLlmKey,
34232
+ has_extract_provider: doc.has_extract_provider === true,
34233
+ ...ownerDmCap !== void 0 ? { owner_dm_search_cap_effective: ownerDmCap } : {},
34221
34234
  legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source),
34222
34235
  secret_paths: parseSecretPathsReport(doc.secret_paths)
34223
34236
  };
@@ -34416,7 +34429,8 @@ function loadOwnerGrantPin(opts) {
34416
34429
  // src/grants/manifest.ts
34417
34430
  var TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
34418
34431
  var MANIFEST_KNOWN_TOOLS = Object.freeze({
34419
- 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)
34420
34434
  team_room: Object.freeze(["web_search"])
34421
34435
  });
34422
34436
  function manifestSignedBytes(manifest) {
@@ -34491,7 +34505,11 @@ function parseConfig(v) {
34491
34505
  }
34492
34506
  if ("search" in v) {
34493
34507
  const sr = v.search;
34494
- 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
+ }
34495
34513
  }
34496
34514
  const mem = v.memory;
34497
34515
  if (!isPlainObject(mem) || !exactKeys(mem, ["mode"], ["recall_days"])) return null;
@@ -34501,7 +34519,12 @@ function parseConfig(v) {
34501
34519
  return Object.freeze({
34502
34520
  persona_sha256: v.persona_sha256,
34503
34521
  ...isPlainObject(v.llm) ? { llm: Object.freeze({ provider: v.llm.provider, model: v.llm.model }) } : {},
34504
- ...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
+ } : {},
34505
34528
  ...isStringArray(v.tools_allow) ? { tools_allow: Object.freeze([...v.tools_allow]) } : {},
34506
34529
  flags: Object.freeze([...v.flags]),
34507
34530
  memory: Object.freeze({ mode: mem.mode, ..."recall_days" in mem ? { recall_days: mem.recall_days } : {} }),
@@ -34732,7 +34755,20 @@ function bindTargets(raw, signed, equals) {
34732
34755
  }
34733
34756
  return { written, mismatches };
34734
34757
  }
34735
- 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) {
34736
34772
  const mismatches = /* @__PURE__ */ new Set();
34737
34773
  const cfg = manifest.config;
34738
34774
  const out = {};
@@ -34761,6 +34797,7 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
34761
34797
  for (const name of Object.keys(files)) {
34762
34798
  if (name !== LLM_SOURCE && !PERSONA_FILES.includes(name)) mismatches.add("files");
34763
34799
  }
34800
+ const ownerDmTools = signedManifestTools(manifest, "owner_dm", searchProvidersCapable);
34764
34801
  const sj = isObj2(payload.skills_json) ? payload.skills_json : void 0;
34765
34802
  const eps = bindTargets(sj?.endpoints, manifest.endpoints, endpointEquals);
34766
34803
  const dbs = bindTargets(sj?.databases, manifest.databases, databaseEquals);
@@ -34778,14 +34815,20 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
34778
34815
  appliedSearch = searchBlock;
34779
34816
  } else {
34780
34817
  mismatches.add("search");
34781
- 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 });
34782
34820
  }
34783
34821
  const skillsDoc = {
34784
34822
  endpoints: eps.written,
34785
34823
  databases: dbs.written,
34786
34824
  flags: [...cfg.flags],
34787
34825
  ...deny !== void 0 || cfg.tools_allow !== void 0 ? { tools: { ...deny !== void 0 ? { deny } : {}, ...cfg.tools_allow !== void 0 ? { allow: [...cfg.tools_allow] } : {} } } : {},
34788
- ...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 } : {}
34789
34832
  };
34790
34833
  out["skills.json"] = JSON.stringify(skillsDoc, null, 2) + "\n";
34791
34834
  const signedMemory = { mode: cfg.memory.mode, ...cfg.memory.recall_days !== void 0 ? { recall_days: cfg.memory.recall_days } : {} };
@@ -34816,12 +34859,7 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
34816
34859
  mismatches.add("legacy_assign");
34817
34860
  legacyDecisions = decisions.filter((d) => isObj2(d) && d.action === "delete");
34818
34861
  }
34819
- const signedTools = (a) => MANIFEST_KNOWN_TOOLS[a].filter((t) => (manifest.grants[a] ?? []).includes(t));
34820
- const policy = {
34821
- v: 1,
34822
- manifest_tools: [.../* @__PURE__ */ new Set([...switches.ownerDm ? signedTools("owner_dm") : [], ...switches.room ? signedTools("team_room") : []])].sort(),
34823
- secret_path_mode: "enforce"
34824
- };
34862
+ const policy = manifestPolicyFor(manifest, switches, searchProvidersCapable);
34825
34863
  return {
34826
34864
  mismatches: CONFIG_BINDING_FIELDS.filter((f) => mismatches.has(f)).sort(),
34827
34865
  targetMismatches: { endpoint: eps.mismatches, database: dbs.mismatches },
@@ -34960,6 +34998,8 @@ var TOOL_ROOM_CLASSES = Object.freeze({
34960
34998
  current_time: Object.freeze(["room_safe_read"]),
34961
34999
  echo: Object.freeze(["room_safe_read"]),
34962
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
34963
35003
  fetch_url: Object.freeze(["egress"]),
34964
35004
  sql_query: Object.freeze(["owner_credential"]),
34965
35005
  http_request: Object.freeze(["owner_credential", "egress", "side_effect"])
@@ -35381,6 +35421,8 @@ function hexToBytes4(h) {
35381
35421
  return new Uint8Array(Buffer.from(h, "hex"));
35382
35422
  }
35383
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;
35384
35426
  var BootDrainRotationRefusedError = class extends Error {
35385
35427
  constructor() {
35386
35428
  super("key-package rotation refused: the boot drain has not settled");
@@ -35576,6 +35618,8 @@ var BridgeAgent = class _BridgeAgent {
35576
35618
  this.actionClock = deps.actionThrottleNow ?? Date.now;
35577
35619
  this.rng = deps.rng ?? Math.random;
35578
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;
35579
35623
  this.replySleep = deps.replySleep ?? ((ms) => new Promise((resolve) => {
35580
35624
  const t = setTimeout(resolve, ms);
35581
35625
  t.unref?.();
@@ -36014,6 +36058,8 @@ var BridgeAgent = class _BridgeAgent {
36014
36058
  bootSweepTimer;
36015
36059
  rng;
36016
36060
  bootDrainWaitMs;
36061
+ searchCapabilityRefetchDebounceMs;
36062
+ searchCapabilityRefetchMaxWaitMs;
36017
36063
  /** Resolved (and cleared) each time an ingest cycle settles: see start(). */
36018
36064
  ingestSettledWaiters = [];
36019
36065
  /** The boot key-package publish, when start() had to defer it. Test seam. */
@@ -36459,9 +36505,14 @@ var BridgeAgent = class _BridgeAgent {
36459
36505
  }
36460
36506
  const skillsJson = payload.skills_json ?? {};
36461
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
+ }
36462
36513
  this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, {
36463
36514
  ...payload.files ?? {},
36464
- "skills.json": `${JSON.stringify(skillsJson, null, 2)}
36515
+ "skills.json": `${JSON.stringify(asDelivered, null, 2)}
36465
36516
  `
36466
36517
  });
36467
36518
  }
@@ -36516,7 +36567,7 @@ var BridgeAgent = class _BridgeAgent {
36516
36567
  }
36517
36568
  let bound;
36518
36569
  try {
36519
- 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());
36520
36571
  } catch (e) {
36521
36572
  bound = null;
36522
36573
  return this.bindFailed(manifest, payload.memory, e);
@@ -36535,9 +36586,9 @@ var BridgeAgent = class _BridgeAgent {
36535
36586
  return true;
36536
36587
  }
36537
36588
  try {
36538
- this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, bound.policy);
36539
36589
  this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, bound.files);
36540
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()));
36541
36592
  } catch (e) {
36542
36593
  this.manifestBinding = null;
36543
36594
  throw e;
@@ -36590,6 +36641,7 @@ var BridgeAgent = class _BridgeAgent {
36590
36641
  /** The report half of refetchRuntimeConfigOnce (split out for the plan 4 Task 11 bind_failed path). */
36591
36642
  async postRuntimeConfigReport(base, version, skillsJson) {
36592
36643
  const local = readLocalReport(this.home, this.cfg.identityId);
36644
+ const reportedSearchProviders = this.searchProvidersCapable();
36593
36645
  const report = {
36594
36646
  applied_version: version,
36595
36647
  paths: checkConfiguredPaths(skillsJson),
@@ -36604,6 +36656,13 @@ var BridgeAgent = class _BridgeAgent {
36604
36656
  // to the backend. The backend needs it to generate a truthful tool
36605
36657
  // paragraph, so it rides along here.
36606
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 } : {},
36607
36666
  // Same idea, for the BYOK LLM key file: only the runtime can see
36608
36667
  // whether one is configured in its own environment.
36609
36668
  has_llm_key: local?.has_llm_key ?? false,
@@ -36643,7 +36702,11 @@ var BridgeAgent = class _BridgeAgent {
36643
36702
  // Plan 4 Task 15 (OQ-5): sorted, unique enums; capability is the latest hello's class.
36644
36703
  config_binding_mismatches: [...this.configBindingMismatches],
36645
36704
  target_mismatches: ["database", "endpoint"].filter((k) => this.targetMismatches[k] > 0),
36646
- 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)
36647
36710
  }
36648
36711
  };
36649
36712
  const reportedVerifyFailures = this.ownerVerifyFailures;
@@ -36664,6 +36727,7 @@ var BridgeAgent = class _BridgeAgent {
36664
36727
  return;
36665
36728
  }
36666
36729
  this.ownerVerifyFailures = Math.max(0, this.ownerVerifyFailures - reportedVerifyFailures);
36730
+ this.lastReportedSearchProviders = reportedSearchProviders;
36667
36731
  if (version) writeServiceLog2("info", "runtime_config.applied", { version });
36668
36732
  else writeServiceLog2("info", "runtime_config.legacy_memory_reported");
36669
36733
  }
@@ -36733,6 +36797,7 @@ var BridgeAgent = class _BridgeAgent {
36733
36797
  this.startedAt = Date.now();
36734
36798
  await this.restoreRuntimeInbox();
36735
36799
  await this.loadToolGrantState();
36800
+ this.recomputeManifestPolicy(false);
36736
36801
  try {
36737
36802
  this.store.pruneRoomSearchCap(utcDayOf(Date.now()));
36738
36803
  } catch (e) {
@@ -42524,6 +42589,115 @@ ${channelId}`;
42524
42589
  manifestConsumerAttached() {
42525
42590
  return this.manifestConsumer !== null;
42526
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
+ }
42527
42701
  noteConsumerCapability(state) {
42528
42702
  this.lastConsumerCapability = state;
42529
42703
  if (state !== "ok") {
@@ -42715,6 +42889,8 @@ ${channelId}`;
42715
42889
  this.supersedeRecheckTimers.clear();
42716
42890
  if (this.redeliveryTimer !== null) clearTimeout(this.redeliveryTimer);
42717
42891
  this.redeliveryTimer = null;
42892
+ if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
42893
+ this.searchCapabilityRefetchTimer = null;
42718
42894
  this.pingHandle?.close();
42719
42895
  if (this.safetyTimer) clearInterval(this.safetyTimer);
42720
42896
  if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
@@ -42824,7 +43000,7 @@ ${channelId}`;
42824
43000
  };
42825
43001
 
42826
43002
  // src/config.ts
42827
- import { readFileSync as readFileSync10, statSync as statSync5 } from "node:fs";
43003
+ import { readFileSync as readFileSync11, statSync as statSync5 } from "node:fs";
42828
43004
  import { dirname as dirname6, join as join18 } from "node:path";
42829
43005
  var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
42830
43006
  var CONFIG_ERROR_MESSAGE = {
@@ -42846,7 +43022,7 @@ var ConfigError = class extends Error {
42846
43022
  function loadConfig(opts) {
42847
43023
  let raw;
42848
43024
  try {
42849
- raw = readFileSync10(opts.credentialPath, "utf8");
43025
+ raw = readFileSync11(opts.credentialPath, "utf8");
42850
43026
  } catch (e) {
42851
43027
  const code = e?.code;
42852
43028
  throw new ConfigError(code === "ENOENT" ? "credential_not_found" : "credential_unreadable");
@@ -42976,11 +43152,11 @@ import { z as z3 } from "zod";
42976
43152
 
42977
43153
  // src/auth.ts
42978
43154
  import { randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
42979
- 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";
42980
43156
  function resolveToken(opts) {
42981
43157
  if (opts.token) return opts.token;
42982
43158
  const f = opts.tokenFile;
42983
- if (f && existsSync6(f)) return readFileSync11(f, "utf8").trim();
43159
+ if (f && existsSync6(f)) return readFileSync12(f, "utf8").trim();
42984
43160
  const tok = randomBytes11(32).toString("base64url");
42985
43161
  if (f) writeFileSync2(f, tok, { mode: 384 });
42986
43162
  return tok;
@@ -43076,6 +43252,7 @@ function withConsumerGrants(m, perToolConsumer) {
43076
43252
  return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
43077
43253
  }
43078
43254
  var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
43255
+ var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
43079
43256
  var CONSTANT_TOOL_NAMES = Object.freeze({
43080
43257
  owner_dm: Object.freeze(["current_time", "web_search"]),
43081
43258
  team_room: Object.freeze(["current_time"])
@@ -43098,6 +43275,15 @@ function withHarnessGrants(m, withheld) {
43098
43275
  if (!a || a.tool_grants.length === 0) return m;
43099
43276
  return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
43100
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
+ }
43101
43287
 
43102
43288
  // src/http-server.ts
43103
43289
  function writeSse(res, msgs, onWriteError) {
@@ -43120,7 +43306,8 @@ data: ${JSON.stringify(msgs[i])}
43120
43306
  }
43121
43307
  function deliverSseBatch(res, batch, agent, onWriteError) {
43122
43308
  const withheld = agent.harnessGrantsWithheld();
43123
- 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);
43124
43311
  const leftIds = new Set(left3.map((message) => message.message_id));
43125
43312
  agent.ackRuntimeMessages(batch.filter((message) => !leftIds.has(message.message_id)).map((message) => message.message_id));
43126
43313
  if (left3.length) agent.requeueFront(batch.filter((message) => leftIds.has(message.message_id)));
@@ -43391,7 +43578,10 @@ function createHttpServer(agent, opts) {
43391
43578
  if (method === "GET" && path === "/v1/messages") {
43392
43579
  const channel = url.searchParams.get("channel_id") ?? void 0;
43393
43580
  const wait = Math.max(0, Math.min(50, Number(url.searchParams.get("wait") ?? 0)));
43394
- 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)));
43395
43585
  }
43396
43586
  if (method === "GET" && path === "/v1/events") {
43397
43587
  const channel = url.searchParams.get("channel_id") ?? void 0;
@@ -43565,6 +43755,7 @@ function attachAgentWebSocket(server, agent, opts) {
43565
43755
  consumer: "unknown",
43566
43756
  perToolGrants: false,
43567
43757
  manifestGrants: false,
43758
+ searchProviders: false,
43568
43759
  // helloWaitMs 0 keeps today's synchronous initial flush exactly (the opt-out the existing tests use).
43569
43760
  deliveryOpen: helloWaitMs <= 0,
43570
43761
  deliveryToken: /* @__PURE__ */ new Map(),
@@ -43618,9 +43809,13 @@ function attachAgentWebSocket(server, agent, opts) {
43618
43809
  continue;
43619
43810
  }
43620
43811
  const out = withConsumerNotes(
43621
- withManifestCapability(
43622
- withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
43623
- 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()
43624
43819
  ),
43625
43820
  batch[i],
43626
43821
  session,
@@ -43656,6 +43851,7 @@ function attachAgentWebSocket(server, agent, opts) {
43656
43851
  const cleanup = () => {
43657
43852
  if (cleaned) return;
43658
43853
  cleaned = true;
43854
+ session.closed = true;
43659
43855
  if (helloTimer) clearTimeout(helloTimer);
43660
43856
  agent.noteSessionClose();
43661
43857
  unsubscribe();
@@ -43676,6 +43872,7 @@ function attachAgentWebSocket(server, agent, opts) {
43676
43872
  if (session.harness) agent.clearHarnessReport(session.harness);
43677
43873
  if (session.perToolGrants) agent.clearPerToolConsumer(session);
43678
43874
  if (session.manifestGrants) agent.clearManifestConsumer(session);
43875
+ agent.clearSearchProvidersSocket(session);
43679
43876
  };
43680
43877
  current = { sock, cleanup, sweepStale };
43681
43878
  sock.on("close", cleanup);
@@ -43751,6 +43948,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
43751
43948
  return;
43752
43949
  }
43753
43950
  if (m.type === "hello") {
43951
+ if (session.closed) return;
43754
43952
  const report = m.harness === void 0 ? null : parseHarnessReport(m.harness);
43755
43953
  if (report) {
43756
43954
  session.harness = report;
@@ -43774,6 +43972,8 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
43774
43972
  agent.clearManifestConsumer(session);
43775
43973
  }
43776
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);
43777
43977
  if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
43778
43978
  agent.noteNotesConsumer();
43779
43979
  }
@@ -43781,7 +43981,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
43781
43981
  if (session.recallConsumer) agent.noteRecallConsumer();
43782
43982
  session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
43783
43983
  if (session.unifiedConsumer) agent.noteUnifiedConsumer();
43784
- 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] });
43785
43985
  if (!session.deliveryOpen) {
43786
43986
  session.deliveryOpen = true;
43787
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;
@@ -33,6 +33,7 @@ var init_resolve_node = __esm({
33
33
  // src/preflight-upgrade.cts
34
34
  var preflight_upgrade_exports = {};
35
35
  __export(preflight_upgrade_exports, {
36
+ DIST_BASE: () => DIST_BASE,
36
37
  guidedUpgrade: () => guidedUpgrade,
37
38
  parseShasums: () => parseShasums,
38
39
  promptYesNo: () => promptYesNo,
@@ -49,6 +49,10 @@ export interface LocalReport {
49
49
  persona: Record<string, string>;
50
50
  has_search_key: boolean;
51
51
  has_llm_key: boolean;
52
+ /** L-2 (spec §7.9): a readable key for an extract-capable provider. Only a real true counts; absent is false. */
53
+ has_extract_provider: boolean;
54
+ /** D39: the owner-DM daily search cap the runtime enforces; an integer in [0, 2000], otherwise undefined. */
55
+ owner_dm_search_cap_effective?: number;
52
56
  legacy_memory: LegacyMemoryReport[];
53
57
  /** §B.4 counts; undefined when the runtime reported none or the block was malformed. */
54
58
  secret_paths?: SecretPathsReport;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const BRIDGE_VERSION = "0.30.2";
1
+ export declare const BRIDGE_VERSION = "0.30.3";
@@ -51,7 +51,7 @@ export declare const MEMORY_UNIFIED_FEATURE = "memory_unified";
51
51
  * fake against this list at run time (7b gate fix: a member added here without
52
52
  * a fake made three tests throw "is not a function").
53
53
  */
54
- export declare const WS_AGENT_MEMBERS: readonly ["ackRuntimeMessages", "action", "admitRuntimeDelivery", "applyMemoryNote", "clearHarnessReport", "clearManifestConsumer", "clearPerToolConsumer", "harnessGrantsWithheld", "isRuntimeMessageRetired", "noteConsumerCapability", "noteManifestConsumer", "noteRoomCapReached", "noteNotesConsumer", "notePerToolConsumer", "noteRecallConsumer", "noteUnifiedConsumer", "noteSessionClose", "noteSessionEvicted", "noteSessionOpen", "notesAppliedGeneration", "notesForDelivery", "recallForDelivery", "unifiedFeatureOn", "unifiedForDelivery", "recordHarnessReport", "recordRuntimeReply", "reportStatusGated", "requeueFailedRuntimeDeliveries", "requeueFront", "runtimeReplyReceipt", "send", "sendA2A", "subscribe", "takeDeliverable", "wakeListeners"];
54
+ export declare const WS_AGENT_MEMBERS: readonly ["ackRuntimeMessages", "action", "admitRuntimeDelivery", "applyMemoryNote", "clearHarnessReport", "clearManifestConsumer", "clearPerToolConsumer", "clearSearchProvidersSocket", "harnessGrantsWithheld", "signedSearchProvider", "isRuntimeMessageRetired", "noteConsumerCapability", "noteManifestConsumer", "noteRoomCapReached", "noteSearchProvidersSocket", "noteNotesConsumer", "notePerToolConsumer", "noteRecallConsumer", "noteUnifiedConsumer", "noteSessionClose", "noteSessionEvicted", "noteSessionOpen", "notesAppliedGeneration", "notesForDelivery", "recallForDelivery", "unifiedFeatureOn", "unifiedForDelivery", "recordHarnessReport", "recordRuntimeReply", "reportStatusGated", "requeueFailedRuntimeDeliveries", "requeueFront", "runtimeReplyReceipt", "send", "sendA2A", "subscribe", "takeDeliverable", "wakeListeners"];
55
55
  export type WsAgent = Pick<BridgeAgent, (typeof WS_AGENT_MEMBERS)[number]>;
56
56
  export declare function attachAgentWebSocket(server: HttpServer, agent: WsAgent, opts: AgentWebSocketOptions): {
57
57
  close(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securecoms/agent-bridge",
3
- "version": "0.30.2",
3
+ "version": "0.30.3",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"