@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/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
  }
@@ -8001,8 +8001,30 @@ function ackOnlyGreeting(agentName, callerName) {
8001
8001
 
8002
8002
  // src/config.ts
8003
8003
  var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
8004
+ var CONFIG_ERROR_MESSAGE = {
8005
+ credential_not_found: "credential file not found",
8006
+ credential_unreadable: "credential file could not be read",
8007
+ credential_not_json: "credential file is not valid JSON",
8008
+ credential_missing_field: "credential file missing required field",
8009
+ credential_voice_invalid: "credential file voice config invalid"
8010
+ };
8011
+ var ConfigError = class extends Error {
8012
+ constructor(reason, field, source) {
8013
+ super(CONFIG_ERROR_MESSAGE[reason] + (field ? `: ${field}` : ""));
8014
+ this.reason = reason;
8015
+ this.field = field;
8016
+ this.source = source;
8017
+ this.name = "ConfigError";
8018
+ }
8019
+ };
8004
8020
  function loadConfig(opts) {
8005
- const raw = readFileSync2(opts.credentialPath, "utf8");
8021
+ let raw;
8022
+ try {
8023
+ raw = readFileSync2(opts.credentialPath, "utf8");
8024
+ } catch (e) {
8025
+ const code = e?.code;
8026
+ throw new ConfigError(code === "ENOENT" ? "credential_not_found" : "credential_unreadable");
8027
+ }
8006
8028
  try {
8007
8029
  const mode = statSync(opts.credentialPath).mode & 511;
8008
8030
  if (mode & 63) writeServiceLog("warn", "config.credential_permissions_unsafe");
@@ -8012,9 +8034,9 @@ function loadConfig(opts) {
8012
8034
  try {
8013
8035
  json2 = JSON.parse(raw);
8014
8036
  } catch {
8015
- throw new Error("credential file is not valid JSON");
8037
+ throw new ConfigError("credential_not_json");
8016
8038
  }
8017
- for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new Error(`credential file missing required field: ${k}`);
8039
+ for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new ConfigError("credential_missing_field", k);
8018
8040
  const storePath = opts.storePath ?? join2(dirname2(opts.credentialPath), "bridge-state.db");
8019
8041
  const ownerGrantPin = loadOwnerGrantPin({ credentialPath: opts.credentialPath, credentialJson: json2 });
8020
8042
  writeServiceLog(ownerGrantPin.state === "ok" || ownerGrantPin.state === "no_pin" ? "info" : "warn", "authz.pin", {
@@ -8052,8 +8074,14 @@ function loadConfig(opts) {
8052
8074
  let voice;
8053
8075
  try {
8054
8076
  voice = parseVoiceConfig(json2.voice, process.env);
8055
- } catch (e) {
8056
- throw new Error(`credential file voice config invalid: ${e.message}`);
8077
+ } catch {
8078
+ let source = "env";
8079
+ try {
8080
+ parseVoiceConfig(json2.voice, {});
8081
+ } catch {
8082
+ source = "file";
8083
+ }
8084
+ throw new ConfigError("credential_voice_invalid", void 0, source);
8057
8085
  }
8058
8086
  const modelSource = json2.memory?.model_source === "import_only" ? "import_only" : "download";
8059
8087
  const enabledRaw = json2.memory?.enabled;
@@ -18169,7 +18197,13 @@ var CODES = [
18169
18197
  // lookup missed and before any write, so a retry can never land: the
18170
18198
  // pending commit is dropped instead of being resent forever. The same pair
18171
18199
  // answers an application send into a deleted channel (#325 F5).
18172
- { status: 409, code: "channel_unavailable" }
18200
+ { status: 409, code: "channel_unavailable" },
18201
+ // ADR-0023 B.1: an Add or Welcome into a humans-only enclave's group naming
18202
+ // a device that is not provably a person's. Refused after the replay lookup
18203
+ // missed and before any write, so nothing landed. Definite: the committer's
18204
+ // work cannot name an agent in a humans-only enclave (admission refuses
18205
+ // one), so dropping the commit starts no resend loop.
18206
+ { status: 403, code: "humans_only_enclave" }
18173
18207
  ];
18174
18208
  var DISTRIBUTE_DEFINITE_CODES = Object.freeze(CODES.map((c) => Object.freeze({ ...c })));
18175
18209
  var DEFINITE_KEYS = new Set(DISTRIBUTE_DEFINITE_CODES.map((c) => `${c.status} ${c.code}`));
@@ -29848,7 +29882,7 @@ function loadPackageMeta(importMetaUrl) {
29848
29882
  }
29849
29883
 
29850
29884
  // src/bridge-agent.ts
29851
- import { existsSync as existsSync5, promises as fsp, statSync as statSync5 } from "node:fs";
29885
+ import { existsSync as existsSync5, promises as fsp, readFileSync as readFileSync11, statSync as statSync5 } from "node:fs";
29852
29886
  import { join as join19, dirname as dirname7 } from "node:path";
29853
29887
  import { fileURLToPath as fileURLToPath2 } from "node:url";
29854
29888
 
@@ -34363,6 +34397,15 @@ import {
34363
34397
  writeFileSync
34364
34398
  } from "node:fs";
34365
34399
  import { basename as basename4, isAbsolute, join as join18 } from "node:path";
34400
+
34401
+ // src/grants/search-providers.ts
34402
+ var SEARCH_PROVIDERS = /* @__PURE__ */ new Set(["brave", "tavily", "parallel", "exa", "firecrawl"]);
34403
+ var OWNER_DM_SEARCH_DAILY_CAP_MAX = 2e3;
34404
+ function isSearchProvider(value) {
34405
+ return typeof value === "string" && SEARCH_PROVIDERS.has(value);
34406
+ }
34407
+
34408
+ // src/service/runtime-config-dir.ts
34366
34409
  var MEMORY_FILE = "MEMORY.md";
34367
34410
  var REAL_FS = {
34368
34411
  mkdir: (p, mode) => mkdirSync5(p, { recursive: true, mode }),
@@ -34545,10 +34588,14 @@ function readLocalReport(home, identityId) {
34545
34588
  const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
34546
34589
  Object.entries(raw).filter(([, v]) => typeof v === "string")
34547
34590
  ) : {};
34591
+ const capRaw = doc.owner_dm_search_cap_effective;
34592
+ const ownerDmCap = typeof capRaw === "number" && Number.isInteger(capRaw) && capRaw >= 0 && capRaw <= OWNER_DM_SEARCH_DAILY_CAP_MAX ? capRaw : void 0;
34548
34593
  return {
34549
34594
  persona,
34550
34595
  has_search_key: hasSearchKey,
34551
34596
  has_llm_key: hasLlmKey,
34597
+ has_extract_provider: doc.has_extract_provider === true,
34598
+ ...ownerDmCap !== void 0 ? { owner_dm_search_cap_effective: ownerDmCap } : {},
34552
34599
  legacy_memory: localLegacySources(doc).map(({ path: _path, ...source }) => source),
34553
34600
  secret_paths: parseSecretPathsReport(doc.secret_paths)
34554
34601
  };
@@ -34666,7 +34713,8 @@ import { sha256 as sha25611 } from "@noble/hashes/sha256";
34666
34713
  import { ed25519 as ed255195 } from "@noble/curves/ed25519";
34667
34714
  var TOOL_GRANTS_DOMAIN = "SECURECOMS-TOOL-GRANTS-V1:";
34668
34715
  var MANIFEST_KNOWN_TOOLS = Object.freeze({
34669
- owner_dm: Object.freeze(["web_search", "fetch_url", "http_request", "sql_query"]),
34716
+ owner_dm: Object.freeze(["web_search", "web_extract", "fetch_url", "http_request", "sql_query"]),
34717
+ // web_extract: owner DM only, never team_room (spec §5.4, D5)
34670
34718
  team_room: Object.freeze(["web_search"])
34671
34719
  });
34672
34720
  function manifestSignedBytes(manifest) {
@@ -34741,7 +34789,11 @@ function parseConfig(v) {
34741
34789
  }
34742
34790
  if ("search" in v) {
34743
34791
  const sr = v.search;
34744
- if (!isPlainObject(sr) || !exactKeys(sr, ["provider"]) || sr.provider !== "brave" && sr.provider !== "tavily") return null;
34792
+ if (!isPlainObject(sr) || !exactKeys(sr, ["provider"], ["owner_dm_daily_cap"]) || !isSearchProvider(sr.provider)) return null;
34793
+ if ("owner_dm_daily_cap" in sr) {
34794
+ const cap = sr.owner_dm_daily_cap;
34795
+ if (typeof cap !== "number" || !Number.isSafeInteger(cap) || cap < 0 || cap > OWNER_DM_SEARCH_DAILY_CAP_MAX) return null;
34796
+ }
34745
34797
  }
34746
34798
  const mem = v.memory;
34747
34799
  if (!isPlainObject(mem) || !exactKeys(mem, ["mode"], ["recall_days"])) return null;
@@ -34751,7 +34803,12 @@ function parseConfig(v) {
34751
34803
  return Object.freeze({
34752
34804
  persona_sha256: v.persona_sha256,
34753
34805
  ...isPlainObject(v.llm) ? { llm: Object.freeze({ provider: v.llm.provider, model: v.llm.model }) } : {},
34754
- ...isPlainObject(v.search) ? { search: Object.freeze({ provider: v.search.provider }) } : {},
34806
+ ...isPlainObject(v.search) ? {
34807
+ search: Object.freeze({
34808
+ provider: v.search.provider,
34809
+ ..."owner_dm_daily_cap" in v.search ? { owner_dm_daily_cap: v.search.owner_dm_daily_cap } : {}
34810
+ })
34811
+ } : {},
34755
34812
  ...isStringArray(v.tools_allow) ? { tools_allow: Object.freeze([...v.tools_allow]) } : {},
34756
34813
  flags: Object.freeze([...v.flags]),
34757
34814
  memory: Object.freeze({ mode: mem.mode, ..."recall_days" in mem ? { recall_days: mem.recall_days } : {} }),
@@ -34982,7 +35039,20 @@ function bindTargets(raw, signed, equals) {
34982
35039
  }
34983
35040
  return { written, mismatches };
34984
35041
  }
34985
- function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true }) {
35042
+ function signedManifestTools(manifest, audience, searchProvidersCapable) {
35043
+ return MANIFEST_KNOWN_TOOLS[audience].filter((t) => (manifest.grants[audience] ?? []).includes(t) && (t !== "web_extract" || searchProvidersCapable));
35044
+ }
35045
+ function manifestPolicyFor(manifest, switches, searchProvidersCapable) {
35046
+ return {
35047
+ v: 1,
35048
+ manifest_tools: [.../* @__PURE__ */ new Set([
35049
+ ...switches.ownerDm ? signedManifestTools(manifest, "owner_dm", searchProvidersCapable) : [],
35050
+ ...switches.room ? signedManifestTools(manifest, "team_room", searchProvidersCapable) : []
35051
+ ])].sort(),
35052
+ secret_path_mode: "enforce"
35053
+ };
35054
+ }
35055
+ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true, room: true }, searchProvidersCapable = false) {
34986
35056
  const mismatches = /* @__PURE__ */ new Set();
34987
35057
  const cfg = manifest.config;
34988
35058
  const out = {};
@@ -35011,6 +35081,7 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
35011
35081
  for (const name of Object.keys(files)) {
35012
35082
  if (name !== LLM_SOURCE && !PERSONA_FILES.includes(name)) mismatches.add("files");
35013
35083
  }
35084
+ const ownerDmTools = signedManifestTools(manifest, "owner_dm", searchProvidersCapable);
35014
35085
  const sj = isObj2(payload.skills_json) ? payload.skills_json : void 0;
35015
35086
  const eps = bindTargets(sj?.endpoints, manifest.endpoints, endpointEquals);
35016
35087
  const dbs = bindTargets(sj?.databases, manifest.databases, databaseEquals);
@@ -35028,14 +35099,20 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
35028
35099
  appliedSearch = searchBlock;
35029
35100
  } else {
35030
35101
  mismatches.add("search");
35031
- searchBlock = (prior.search?.provider ?? void 0) === cfg.search?.provider ? prior.search : void 0;
35102
+ const usablePrior = cfg.search !== void 0 && prior.search?.provider === cfg.search.provider ? prior.search : void 0;
35103
+ searchBlock = cfg.search === void 0 ? void 0 : usablePrior ?? Object.freeze({ provider: cfg.search.provider });
35032
35104
  }
35033
35105
  const skillsDoc = {
35034
35106
  endpoints: eps.written,
35035
35107
  databases: dbs.written,
35036
35108
  flags: [...cfg.flags],
35037
35109
  ...deny !== void 0 || cfg.tools_allow !== void 0 ? { tools: { ...deny !== void 0 ? { deny } : {}, ...cfg.tools_allow !== void 0 ? { allow: [...cfg.tools_allow] } : {} } } : {},
35038
- ...searchBlock ? { search: searchBlock } : {}
35110
+ ...searchBlock ? { search: searchBlock } : {},
35111
+ // D31: the marker the runtime keys D24 (iii) on; written under every manifest, true when config.search is signed.
35112
+ search_signed: cfg.search !== void 0,
35113
+ // D36, D39: the signed owner-DM cap, only when the owner-DM switch is on and the manifest grants an owner-DM tool;
35114
+ // otherwise none, so the runtime clamps a signed raise to the default. Never read from the payload.
35115
+ ...cfg.search?.owner_dm_daily_cap !== void 0 && switches.ownerDm && ownerDmTools.length > 0 ? { owner_dm_search_cap: cfg.search.owner_dm_daily_cap } : {}
35039
35116
  };
35040
35117
  out["skills.json"] = JSON.stringify(skillsDoc, null, 2) + "\n";
35041
35118
  const signedMemory = { mode: cfg.memory.mode, ...cfg.memory.recall_days !== void 0 ? { recall_days: cfg.memory.recall_days } : {} };
@@ -35066,12 +35143,7 @@ function bindRuntimeConfig(manifest, payload, prior, switches = { ownerDm: true,
35066
35143
  mismatches.add("legacy_assign");
35067
35144
  legacyDecisions = decisions.filter((d) => isObj2(d) && d.action === "delete");
35068
35145
  }
35069
- const signedTools = (a) => MANIFEST_KNOWN_TOOLS[a].filter((t) => (manifest.grants[a] ?? []).includes(t));
35070
- const policy = {
35071
- v: 1,
35072
- manifest_tools: [.../* @__PURE__ */ new Set([...switches.ownerDm ? signedTools("owner_dm") : [], ...switches.room ? signedTools("team_room") : []])].sort(),
35073
- secret_path_mode: "enforce"
35074
- };
35146
+ const policy = manifestPolicyFor(manifest, switches, searchProvidersCapable);
35075
35147
  return {
35076
35148
  mismatches: CONFIG_BINDING_FIELDS.filter((f) => mismatches.has(f)).sort(),
35077
35149
  targetMismatches: { endpoint: eps.mismatches, database: dbs.mismatches },
@@ -35210,6 +35282,8 @@ var TOOL_ROOM_CLASSES = Object.freeze({
35210
35282
  current_time: Object.freeze(["room_safe_read"]),
35211
35283
  echo: Object.freeze(["room_safe_read"]),
35212
35284
  web_search: Object.freeze(["owner_credential"]),
35285
+ web_extract: Object.freeze(["owner_credential", "egress"]),
35286
+ // D5: spends the owner's key and sends URLs out
35213
35287
  fetch_url: Object.freeze(["egress"]),
35214
35288
  sql_query: Object.freeze(["owner_credential"]),
35215
35289
  http_request: Object.freeze(["owner_credential", "egress", "side_effect"])
@@ -35631,6 +35705,8 @@ function hexToBytes4(h) {
35631
35705
  return new Uint8Array(Buffer.from(h, "hex"));
35632
35706
  }
35633
35707
  var BOOT_DRAIN_WAIT_MS_DEFAULT = 6e4;
35708
+ var SEARCH_CAPABILITY_REFETCH_DEBOUNCE_MS = 1e3;
35709
+ var SEARCH_CAPABILITY_REFETCH_MAX_WAIT_MS = 1e4;
35634
35710
  var BootDrainRotationRefusedError = class extends Error {
35635
35711
  constructor() {
35636
35712
  super("key-package rotation refused: the boot drain has not settled");
@@ -35826,6 +35902,8 @@ var BridgeAgent = class _BridgeAgent {
35826
35902
  this.actionClock = deps.actionThrottleNow ?? Date.now;
35827
35903
  this.rng = deps.rng ?? Math.random;
35828
35904
  this.bootDrainWaitMs = deps.bootDrainWaitMs ?? BOOT_DRAIN_WAIT_MS_DEFAULT;
35905
+ this.searchCapabilityRefetchDebounceMs = deps.searchCapabilityRefetchDebounceMs ?? SEARCH_CAPABILITY_REFETCH_DEBOUNCE_MS;
35906
+ this.searchCapabilityRefetchMaxWaitMs = deps.searchCapabilityRefetchMaxWaitMs ?? SEARCH_CAPABILITY_REFETCH_MAX_WAIT_MS;
35829
35907
  this.replySleep = deps.replySleep ?? ((ms) => new Promise((resolve4) => {
35830
35908
  const t = setTimeout(resolve4, ms);
35831
35909
  t.unref?.();
@@ -36264,6 +36342,8 @@ var BridgeAgent = class _BridgeAgent {
36264
36342
  bootSweepTimer;
36265
36343
  rng;
36266
36344
  bootDrainWaitMs;
36345
+ searchCapabilityRefetchDebounceMs;
36346
+ searchCapabilityRefetchMaxWaitMs;
36267
36347
  /** Resolved (and cleared) each time an ingest cycle settles: see start(). */
36268
36348
  ingestSettledWaiters = [];
36269
36349
  /** The boot key-package publish, when start() had to defer it. Test seam. */
@@ -36709,9 +36789,14 @@ var BridgeAgent = class _BridgeAgent {
36709
36789
  }
36710
36790
  const skillsJson = payload.skills_json ?? {};
36711
36791
  if (version && inForce === null) {
36792
+ let asDelivered = skillsJson;
36793
+ if (skillsJson !== null && typeof skillsJson === "object" && !Array.isArray(skillsJson)) {
36794
+ const { owner_dm_search_cap: _cap, search_signed: _signed, ...rest } = skillsJson;
36795
+ asDelivered = rest;
36796
+ }
36712
36797
  this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, {
36713
36798
  ...payload.files ?? {},
36714
- "skills.json": `${JSON.stringify(skillsJson, null, 2)}
36799
+ "skills.json": `${JSON.stringify(asDelivered, null, 2)}
36715
36800
  `
36716
36801
  });
36717
36802
  }
@@ -36766,7 +36851,7 @@ var BridgeAgent = class _BridgeAgent {
36766
36851
  }
36767
36852
  let bound;
36768
36853
  try {
36769
- bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants });
36854
+ bound = this.grantWriters.bindRuntimeConfig(manifest, payload, { search: prior }, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.searchProvidersCapable());
36770
36855
  } catch (e) {
36771
36856
  bound = null;
36772
36857
  return this.bindFailed(manifest, payload.memory, e);
@@ -36785,9 +36870,9 @@ var BridgeAgent = class _BridgeAgent {
36785
36870
  return true;
36786
36871
  }
36787
36872
  try {
36788
- this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, bound.policy);
36789
36873
  this.grantWriters.writeManagedFiles(this.home, this.cfg.identityId, bound.files);
36790
36874
  if (manifest.config.llm === void 0) this.grantWriters.removeManagedLlmSource(this.home, this.cfg.identityId);
36875
+ this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, this.searchProvidersCapable()));
36791
36876
  } catch (e) {
36792
36877
  this.manifestBinding = null;
36793
36878
  throw e;
@@ -36840,6 +36925,7 @@ var BridgeAgent = class _BridgeAgent {
36840
36925
  /** The report half of refetchRuntimeConfigOnce (split out for the plan 4 Task 11 bind_failed path). */
36841
36926
  async postRuntimeConfigReport(base, version, skillsJson) {
36842
36927
  const local = readLocalReport(this.home, this.cfg.identityId);
36928
+ const reportedSearchProviders = this.searchProvidersCapable();
36843
36929
  const report = {
36844
36930
  applied_version: version,
36845
36931
  paths: checkConfiguredPaths(skillsJson),
@@ -36854,6 +36940,13 @@ var BridgeAgent = class _BridgeAgent {
36854
36940
  // to the backend. The backend needs it to generate a truthful tool
36855
36941
  // paragraph, so it rides along here.
36856
36942
  has_search_key: local?.has_search_key ?? false,
36943
+ // L-2 (spec §7.9): only the runtime knows whether an extract-capable provider key is readable; never derived
36944
+ // from has_search_key. An older runtime omits it: false.
36945
+ // Final review I-2 (controller ruling): AND the current search_providers_v2, so the web never shows extract while
36946
+ // the bridge withholds web_extract from the runtime.
36947
+ has_extract_provider: (local?.has_extract_provider ?? false) && reportedSearchProviders,
36948
+ // D39: the owner-DM daily cap the runtime enforces, passed through when it reported a valid one; never the signed value.
36949
+ ...local?.owner_dm_search_cap_effective !== void 0 ? { owner_dm_search_cap_effective: local.owner_dm_search_cap_effective } : {},
36857
36950
  // Same idea, for the BYOK LLM key file: only the runtime can see
36858
36951
  // whether one is configured in its own environment.
36859
36952
  has_llm_key: local?.has_llm_key ?? false,
@@ -36893,7 +36986,11 @@ var BridgeAgent = class _BridgeAgent {
36893
36986
  // Plan 4 Task 15 (OQ-5): sorted, unique enums; capability is the latest hello's class.
36894
36987
  config_binding_mismatches: [...this.configBindingMismatches],
36895
36988
  target_mismatches: ["database", "endpoint"].filter((k) => this.targetMismatches[k] > 0),
36896
- capability: this.lastConsumerCapability
36989
+ capability: this.lastConsumerCapability,
36990
+ // D28 (§11.1 step 3): a separate key, never a value in the capability enum. False with no runtime attached.
36991
+ search_providers_v2: reportedSearchProviders,
36992
+ // D19 item 4: content-free; the signed provider is one the attached runtime(s) cannot honour.
36993
+ search_provider_unsupported: this.searchProviderUnsupported(reportedSearchProviders)
36897
36994
  }
36898
36995
  };
36899
36996
  const reportedVerifyFailures = this.ownerVerifyFailures;
@@ -36914,6 +37011,7 @@ var BridgeAgent = class _BridgeAgent {
36914
37011
  return;
36915
37012
  }
36916
37013
  this.ownerVerifyFailures = Math.max(0, this.ownerVerifyFailures - reportedVerifyFailures);
37014
+ this.lastReportedSearchProviders = reportedSearchProviders;
36917
37015
  if (version) writeServiceLog("info", "runtime_config.applied", { version });
36918
37016
  else writeServiceLog("info", "runtime_config.legacy_memory_reported");
36919
37017
  }
@@ -36983,6 +37081,7 @@ var BridgeAgent = class _BridgeAgent {
36983
37081
  this.startedAt = Date.now();
36984
37082
  await this.restoreRuntimeInbox();
36985
37083
  await this.loadToolGrantState();
37084
+ this.recomputeManifestPolicy(false);
36986
37085
  try {
36987
37086
  this.store.pruneRoomSearchCap(utcDayOf(Date.now()));
36988
37087
  } catch (e) {
@@ -42774,6 +42873,115 @@ ${channelId}`;
42774
42873
  manifestConsumerAttached() {
42775
42874
  return this.manifestConsumer !== null;
42776
42875
  }
42876
+ /**
42877
+ * D28 (B-4): every socket that said hello, with whether its latest hello declared search_providers_v2 on the
42878
+ * manifest_grants_v1 predicate (ws-server.ts). The agent value is the AND over all of them, absent (false) with
42879
+ * none: unlike manifestConsumer, never newest wins. The per-delivery filter judges each socket by its own hello.
42880
+ */
42881
+ searchProvidersSockets = /* @__PURE__ */ new Map();
42882
+ /**
42883
+ * The D28 value the files were last recomputed for; null until the first socket observation, so that observation
42884
+ * always recomputes (final review I-1), even when it is non-capable.
42885
+ */
42886
+ searchProvidersState = null;
42887
+ searchCapabilityRefetchTimer = null;
42888
+ /** When the current burst's first change happened (valid while the timer is set). */
42889
+ searchCapabilityBurstStart = 0;
42890
+ /** The D28 value in the last report posted (null until one is); a change needs a refetch to reach the web. */
42891
+ lastReportedSearchProviders = null;
42892
+ noteSearchProvidersSocket(token, declared) {
42893
+ this.searchProvidersSockets.set(token, declared);
42894
+ this.onSearchProvidersMaybeChanged();
42895
+ }
42896
+ clearSearchProvidersSocket(token) {
42897
+ if (this.searchProvidersSockets.delete(token)) this.onSearchProvidersMaybeChanged();
42898
+ }
42899
+ /** D28: every attached runtime socket declared search_providers_v2; no socket attached means absent (false). */
42900
+ searchProvidersCapable() {
42901
+ if (this.searchProvidersSockets.size === 0) return false;
42902
+ for (const v of this.searchProvidersSockets.values()) if (!v) return false;
42903
+ return true;
42904
+ }
42905
+ /**
42906
+ * D19 item 3 (B-6): the search provider signed in the stored verified manifest in force (a pin loaded and a good
42907
+ * manifest), never the server payload; undefined with no pin, no manifest, or no signed search block. The
42908
+ * per-delivery filter (consumer-grants.ts withSearchCapability) and the S-28 report reason both read this.
42909
+ */
42910
+ signedSearchProvider() {
42911
+ const pin = this.cfg.ownerGrantPin?.pin ?? null;
42912
+ return pin !== null ? this.toolGrantManifest?.config.search?.provider : void 0;
42913
+ }
42914
+ /** D19 item 4, D34: the in-force signed provider is not brave, and the attached runtime(s) lack the capability. */
42915
+ searchProviderUnsupported(capable) {
42916
+ if (capable) return false;
42917
+ const provider = this.signedSearchProvider();
42918
+ return provider !== void 0 && provider !== "brave";
42919
+ }
42920
+ /**
42921
+ * D35 (P-4): on a capability change, rewrite manifest-policy.json at once from the stored manifest, the kill
42922
+ * switches and the new capability, then a debounced refetch (one per burst) only when needed.
42923
+ */
42924
+ onSearchProvidersMaybeChanged() {
42925
+ const capable = this.searchProvidersCapable();
42926
+ const prev = this.searchProvidersState;
42927
+ if (capable === prev) return;
42928
+ this.searchProvidersState = capable;
42929
+ this.recomputeManifestPolicy(capable);
42930
+ if (this.closingDown) return;
42931
+ if (prev === null && !capable) return;
42932
+ const now = Date.now();
42933
+ if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
42934
+ else this.searchCapabilityBurstStart = now;
42935
+ const delay = Math.max(0, Math.min(this.searchCapabilityRefetchDebounceMs, this.searchCapabilityBurstStart + this.searchCapabilityRefetchMaxWaitMs - now));
42936
+ this.searchCapabilityRefetchTimer = setTimeout(() => {
42937
+ this.searchCapabilityRefetchTimer = null;
42938
+ if (this.searchCapabilityRefetchNeeded()) void this.refetchRuntimeConfig();
42939
+ }, delay);
42940
+ this.searchCapabilityRefetchTimer.unref?.();
42941
+ }
42942
+ /**
42943
+ * A refetch re-renders skills.json (owner_dm_search_cap counts web_extract only with the capability, D39) and posts
42944
+ * the report. Needed under a manifest in force, or when the reported value is stale. Never before the boot fetch.
42945
+ */
42946
+ searchCapabilityRefetchNeeded() {
42947
+ if (!this.bootConfigSettled || this.closingDown) return false;
42948
+ const inForce = (this.cfg.ownerGrantPin?.pin ?? null) !== null && this.toolGrantManifest !== null;
42949
+ return inForce || this.lastReportedSearchProviders !== this.searchProvidersCapable();
42950
+ }
42951
+ /**
42952
+ * D35: the local recompute. Widening (web_extract back in) only while the binding for the in-force manifest stands,
42953
+ * so a policy is never written ahead of its skills.json. Narrowing never waits: with no binding in force, a file that
42954
+ * lists web_extract loses it at once, so an older runtime never reads it as unreadable (which drops every grant).
42955
+ * Content-free; a write failure is logged and the debounced refetch tries again.
42956
+ */
42957
+ recomputeManifestPolicy(capable) {
42958
+ const pin = this.cfg.ownerGrantPin?.pin ?? null;
42959
+ const manifest = pin !== null ? this.toolGrantManifest : null;
42960
+ try {
42961
+ if (manifest !== null && this.manifestBinding !== null && this.manifestBinding.counter === manifest.counter) {
42962
+ this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, manifestPolicyFor(manifest, { ownerDm: this.ownerDmManifestGrants, room: this.roomManifestGrants }, capable));
42963
+ return;
42964
+ }
42965
+ if (capable) return;
42966
+ const path = join19(runtimeConfigDir(this.home, this.cfg.identityId), MANIFEST_POLICY_FILE);
42967
+ if (!existsSync5(path)) return;
42968
+ let doc;
42969
+ try {
42970
+ doc = JSON.parse(readFileSync11(path, "utf8"));
42971
+ } catch {
42972
+ return;
42973
+ }
42974
+ const tools = doc?.manifest_tools;
42975
+ if (!Array.isArray(tools) || !tools.includes("web_extract")) return;
42976
+ this.grantWriters.writeManifestPolicy(this.home, this.cfg.identityId, {
42977
+ v: 1,
42978
+ manifest_tools: tools.filter((t) => typeof t === "string" && t !== "web_extract"),
42979
+ secret_path_mode: "enforce"
42980
+ });
42981
+ } catch (e) {
42982
+ writeServiceLog("warn", "authz.search_capability", { state: ServiceLogEnum.of("policy_write_failed", ["policy_write_failed"]) }, e);
42983
+ }
42984
+ }
42777
42985
  noteConsumerCapability(state) {
42778
42986
  this.lastConsumerCapability = state;
42779
42987
  if (state !== "ok") {
@@ -42965,6 +43173,8 @@ ${channelId}`;
42965
43173
  this.supersedeRecheckTimers.clear();
42966
43174
  if (this.redeliveryTimer !== null) clearTimeout(this.redeliveryTimer);
42967
43175
  this.redeliveryTimer = null;
43176
+ if (this.searchCapabilityRefetchTimer !== null) clearTimeout(this.searchCapabilityRefetchTimer);
43177
+ this.searchCapabilityRefetchTimer = null;
42968
43178
  this.pingHandle?.close();
42969
43179
  if (this.safetyTimer) clearInterval(this.safetyTimer);
42970
43180
  if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
@@ -43075,11 +43285,11 @@ ${channelId}`;
43075
43285
 
43076
43286
  // src/auth.ts
43077
43287
  import { randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
43078
- import { existsSync as existsSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync2 } from "node:fs";
43288
+ import { existsSync as existsSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync2 } from "node:fs";
43079
43289
  function resolveToken(opts) {
43080
43290
  if (opts.token) return opts.token;
43081
43291
  const f = opts.tokenFile;
43082
- if (f && existsSync6(f)) return readFileSync11(f, "utf8").trim();
43292
+ if (f && existsSync6(f)) return readFileSync12(f, "utf8").trim();
43083
43293
  const tok = randomBytes11(32).toString("base64url");
43084
43294
  if (f) writeFileSync2(f, tok, { mode: 384 });
43085
43295
  return tok;
@@ -43180,6 +43390,7 @@ function withConsumerGrants(m, perToolConsumer) {
43180
43390
  return { ...m, authorization: Object.freeze({ ...rest, ...stripGrants ? { tool_grants: NO_GRANTS } : {} }) };
43181
43391
  }
43182
43392
  var MANIFEST_GRANTS_FEATURE = "manifest_grants_v1";
43393
+ var SEARCH_PROVIDERS_FEATURE = "search_providers_v2";
43183
43394
  var CONSTANT_TOOL_NAMES = Object.freeze({
43184
43395
  owner_dm: Object.freeze(["current_time", "web_search"]),
43185
43396
  team_room: Object.freeze(["current_time"])
@@ -43202,6 +43413,15 @@ function withHarnessGrants(m, withheld) {
43202
43413
  if (!a || a.tool_grants.length === 0) return m;
43203
43414
  return { ...m, authorization: Object.freeze({ ...a, tool_grants: NO_GRANTS }) };
43204
43415
  }
43416
+ function withSearchCapability(m, consumerHasFeature, signedProvider) {
43417
+ if (consumerHasFeature) return m;
43418
+ const a = m.authorization;
43419
+ if (!a) return m;
43420
+ const stripSearch = signedProvider !== void 0 && signedProvider !== "brave";
43421
+ const kept = a.tool_grants.filter((g) => g.tool !== "web_extract" && !(stripSearch && g.tool === "web_search"));
43422
+ if (kept.length === a.tool_grants.length) return m;
43423
+ return { ...m, authorization: Object.freeze({ ...a, tool_grants: kept.length === 0 ? NO_GRANTS : Object.freeze(kept) }) };
43424
+ }
43205
43425
 
43206
43426
  // src/http-server.ts
43207
43427
  function writeSse(res, msgs, onWriteError) {
@@ -43224,7 +43444,8 @@ data: ${JSON.stringify(msgs[i])}
43224
43444
  }
43225
43445
  function deliverSseBatch(res, batch, agent, onWriteError) {
43226
43446
  const withheld = agent.harnessGrantsWithheld();
43227
- const left3 = writeSse(res, batch.map((m) => withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false)), onWriteError);
43447
+ const provider = agent.signedSearchProvider();
43448
+ const left3 = writeSse(res, batch.map((m) => withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider)), onWriteError);
43228
43449
  const leftIds = new Set(left3.map((message) => message.message_id));
43229
43450
  agent.ackRuntimeMessages(batch.filter((message) => !leftIds.has(message.message_id)).map((message) => message.message_id));
43230
43451
  if (left3.length) agent.requeueFront(batch.filter((message) => leftIds.has(message.message_id)));
@@ -43495,7 +43716,10 @@ function createHttpServer(agent, opts) {
43495
43716
  if (method === "GET" && path === "/v1/messages") {
43496
43717
  const channel = url.searchParams.get("channel_id") ?? void 0;
43497
43718
  const wait = Math.max(0, Math.min(50, Number(url.searchParams.get("wait") ?? 0)));
43498
- return json(res, 200, (await agent.drain(channel, wait)).map((m) => withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), agent.harnessGrantsWithheld()), false)));
43719
+ const drained = await agent.drain(channel, wait);
43720
+ const withheld = agent.harnessGrantsWithheld();
43721
+ const provider = agent.signedSearchProvider();
43722
+ return json(res, 200, drained.map((m) => withSearchCapability(withManifestCapability(withHarnessGrants(withConsumerGrants(m, false), withheld), false), false, provider)));
43499
43723
  }
43500
43724
  if (method === "GET" && path === "/v1/events") {
43501
43725
  const channel = url.searchParams.get("channel_id") ?? void 0;
@@ -43669,6 +43893,7 @@ function attachAgentWebSocket(server, agent, opts) {
43669
43893
  consumer: "unknown",
43670
43894
  perToolGrants: false,
43671
43895
  manifestGrants: false,
43896
+ searchProviders: false,
43672
43897
  // helloWaitMs 0 keeps today's synchronous initial flush exactly (the opt-out the existing tests use).
43673
43898
  deliveryOpen: helloWaitMs <= 0,
43674
43899
  deliveryToken: /* @__PURE__ */ new Map(),
@@ -43722,9 +43947,13 @@ function attachAgentWebSocket(server, agent, opts) {
43722
43947
  continue;
43723
43948
  }
43724
43949
  const out = withConsumerNotes(
43725
- withManifestCapability(
43726
- withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
43727
- session.manifestGrants
43950
+ withSearchCapability(
43951
+ withManifestCapability(
43952
+ withHarnessGrants(withConsumerGrants(batch[i], session.perToolGrants), !session.perToolGrants && agent.harnessGrantsWithheld()),
43953
+ session.manifestGrants
43954
+ ),
43955
+ session.searchProviders,
43956
+ agent.signedSearchProvider()
43728
43957
  ),
43729
43958
  batch[i],
43730
43959
  session,
@@ -43760,6 +43989,7 @@ function attachAgentWebSocket(server, agent, opts) {
43760
43989
  const cleanup = () => {
43761
43990
  if (cleaned) return;
43762
43991
  cleaned = true;
43992
+ session.closed = true;
43763
43993
  if (helloTimer) clearTimeout(helloTimer);
43764
43994
  agent.noteSessionClose();
43765
43995
  unsubscribe();
@@ -43780,6 +44010,7 @@ function attachAgentWebSocket(server, agent, opts) {
43780
44010
  if (session.harness) agent.clearHarnessReport(session.harness);
43781
44011
  if (session.perToolGrants) agent.clearPerToolConsumer(session);
43782
44012
  if (session.manifestGrants) agent.clearManifestConsumer(session);
44013
+ agent.clearSearchProvidersSocket(session);
43783
44014
  };
43784
44015
  current = { sock, cleanup, sweepStale };
43785
44016
  sock.on("close", cleanup);
@@ -43855,6 +44086,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
43855
44086
  return;
43856
44087
  }
43857
44088
  if (m.type === "hello") {
44089
+ if (session.closed) return;
43858
44090
  const report = m.harness === void 0 ? null : parseHarnessReport(m.harness);
43859
44091
  if (report) {
43860
44092
  session.harness = report;
@@ -43878,6 +44110,8 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
43878
44110
  agent.clearManifestConsumer(session);
43879
44111
  }
43880
44112
  agent.noteConsumerCapability(manifest ? "ok" : session.harnessClaimed ? "harness_consumer" : "missing");
44113
+ session.searchProviders = session.consumer === "runtime" && session.harnessClaimed !== true && perTool && Array.isArray(m.features) && m.features.includes(SEARCH_PROVIDERS_FEATURE);
44114
+ agent.noteSearchProvidersSocket(session, session.searchProviders);
43881
44115
  if (session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_NOTES_FEATURE)) {
43882
44116
  agent.noteNotesConsumer();
43883
44117
  }
@@ -43885,7 +44119,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
43885
44119
  if (session.recallConsumer) agent.noteRecallConsumer();
43886
44120
  session.unifiedConsumer = agent.unifiedFeatureOn() && session.consumer === "runtime" && session.harnessClaimed !== true && Array.isArray(m.features) && m.features.includes(MEMORY_UNIFIED_FEATURE);
43887
44121
  if (session.unifiedConsumer) agent.noteUnifiedConsumer();
43888
- sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE] });
44122
+ sendJson(sock, { type: "hello", features: ["nack", MEMORY_NOTES_FEATURE, MEMORY_RECALL_FEATURE, ...agent.unifiedFeatureOn() ? [MEMORY_UNIFIED_FEATURE] : [], MANIFEST_GRANTS_FEATURE, SEARCH_PROVIDERS_FEATURE] });
43889
44123
  if (!session.deliveryOpen) {
43890
44124
  session.deliveryOpen = true;
43891
44125
  flush();
@@ -44111,7 +44345,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
44111
44345
  var LOWER_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
44112
44346
 
44113
44347
  // src/bin.ts
44114
- 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";
44348
+ 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";
44115
44349
  import { join as join25, dirname as dirname13, isAbsolute as isAbsolute3, resolve as resolve3 } from "node:path";
44116
44350
  import { fileURLToPath as fileURLToPath4 } from "node:url";
44117
44351
  import { homedir as homedir5, userInfo } from "node:os";
@@ -44197,7 +44431,7 @@ function slugify(s) {
44197
44431
  function deriveServiceConfig(input) {
44198
44432
  if (isVersionEmbeddedNodePath(input.execPath)) {
44199
44433
  throw new Error(
44200
- `install: refusing to bake a version-embedded Node path into the service definition: ${input.execPath}. Re-run install under a system/Homebrew Node, or pass --exec-path <stable-path>.`
44434
+ `The Node at ${input.execPath} moves when it is upgraded, so the background service can't use it. Install Node ${REQUIRED_NODE_MAJOR} from https://nodejs.org, then run the install command again.`
44201
44435
  );
44202
44436
  }
44203
44437
  const stem = input.label ?? basename5(input.credentialPath, extname(input.credentialPath));
@@ -44278,6 +44512,26 @@ WantedBy=default.target
44278
44512
  // src/service/install.ts
44279
44513
  var ServiceUnsupportedError = class extends Error {
44280
44514
  };
44515
+ var PREVIOUS_EXIT_WAIT_MS = 1e4;
44516
+ var PREVIOUS_EXIT_POLL_MS = 100;
44517
+ function launchdPid(deps, target) {
44518
+ const r = deps.run("launchctl", ["print", target]);
44519
+ if (r.status !== 0) return void 0;
44520
+ const m = /^\s*pid = (\d+)\s*$/m.exec(r.stdout);
44521
+ const pid = m ? Number(m[1]) : NaN;
44522
+ return Number.isInteger(pid) && pid > 0 ? pid : void 0;
44523
+ }
44524
+ function waitForPreviousExit(deps, pid) {
44525
+ const deadline = deps.now() + PREVIOUS_EXIT_WAIT_MS;
44526
+ while (deps.pidAlive(pid)) {
44527
+ if (deps.now() >= deadline) {
44528
+ throw new Error(
44529
+ `the previous bridge (pid ${pid}) did not exit within ${PREVIOUS_EXIT_WAIT_MS / 1e3} s of launchctl bootout, so a new copy would find the instance lock held and stay down. Wait for that process to exit (check with: ps -p ${pid}), then run install again.`
44530
+ );
44531
+ }
44532
+ deps.sleepMs(PREVIOUS_EXIT_POLL_MS);
44533
+ }
44534
+ }
44281
44535
  function tokenHint(input) {
44282
44536
  return `${dirname8(input.credentialPath)}/bridge-token`;
44283
44537
  }
@@ -44290,7 +44544,10 @@ function installService(input, deps) {
44290
44544
  deps.mkdirp(dirname8(cfg.plistPath));
44291
44545
  deps.mkdirp(dirname8(cfg.logPath));
44292
44546
  deps.writeFile(cfg.plistPath, renderLaunchdPlist(cfg));
44293
- deps.run("launchctl", ["bootout", `gui/${cfg.uid}/${cfg.label}`]);
44547
+ const target = `gui/${cfg.uid}/${cfg.label}`;
44548
+ const previousPid = launchdPid(deps, target);
44549
+ deps.run("launchctl", ["bootout", target]);
44550
+ if (previousPid !== void 0) waitForPreviousExit(deps, previousPid);
44294
44551
  requireOk(deps.run("launchctl", ["bootstrap", `gui/${cfg.uid}`, cfg.plistPath]), "launchctl bootstrap");
44295
44552
  deps.run("launchctl", ["enable", `gui/${cfg.uid}/${cfg.label}`]);
44296
44553
  requireOk(deps.run("launchctl", ["kickstart", "-k", `gui/${cfg.uid}/${cfg.label}`]), "launchctl kickstart");
@@ -44301,7 +44558,8 @@ function installService(input, deps) {
44301
44558
  deps.writeFile(cfg.unitPath, renderSystemdUnit(cfg));
44302
44559
  deps.run("systemctl", ["--user", "daemon-reload"]);
44303
44560
  deps.run("loginctl", ["enable-linger", deps.user]);
44304
- requireOk(deps.run("systemctl", ["--user", "enable", "--now", cfg.unitName]), "systemctl --user enable --now");
44561
+ requireOk(deps.run("systemctl", ["--user", "enable", cfg.unitName]), "systemctl --user enable");
44562
+ requireOk(deps.run("systemctl", ["--user", "restart", cfg.unitName]), "systemctl --user restart");
44305
44563
  return { label: cfg.unitName, filePath: cfg.unitPath, logHint: `journalctl --user -u ${cfg.unitName} -f`, tokenHint: tokenHint(input) };
44306
44564
  }
44307
44565
  throw new ServiceUnsupportedError(`Service install isn't supported on ${deps.platform} yet. Run the agent in a terminal, or use a process manager.`);
@@ -45050,7 +45308,7 @@ function renderStatusTable(rows) {
45050
45308
  import {
45051
45309
  chmodSync as chmodSync5,
45052
45310
  mkdirSync as mkdirSync6,
45053
- readFileSync as readFileSync12,
45311
+ readFileSync as readFileSync13,
45054
45312
  renameSync as renameSync4,
45055
45313
  unlinkSync as unlinkSync2,
45056
45314
  writeFileSync as writeFileSync3
@@ -45062,7 +45320,7 @@ var REAL_FS2 = {
45062
45320
  chmod: (p, mode) => chmodSync5(p, mode),
45063
45321
  writeFile: (p, data, mode) => writeFileSync3(p, data, { mode }),
45064
45322
  rename: (a, b) => renameSync4(a, b),
45065
- readFile: (p) => readFileSync12(p, "utf8"),
45323
+ readFile: (p) => readFileSync13(p, "utf8"),
45066
45324
  unlink: (p) => unlinkSync2(p)
45067
45325
  };
45068
45326
  function securecomsDir(home) {
@@ -45111,7 +45369,7 @@ function removeConnectionFileIfOwned(home, identityId, instanceId, deps = REAL_F
45111
45369
 
45112
45370
  // src/service/instance-lock.ts
45113
45371
  import { execFileSync } from "node:child_process";
45114
- 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";
45372
+ 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";
45115
45373
  import { hostname, uptime as uptime2 } from "node:os";
45116
45374
  import { dirname as dirname11 } from "node:path";
45117
45375
  var LOCK_PARTIAL_WRITE_GRACE_MS = 1e4;
@@ -45125,7 +45383,7 @@ function errno(error) {
45125
45383
  }
45126
45384
  function readTrimmed(path) {
45127
45385
  try {
45128
- const v = readFileSync13(path, "utf8").trim();
45386
+ const v = readFileSync14(path, "utf8").trim();
45129
45387
  return v === "" ? void 0 : v;
45130
45388
  } catch {
45131
45389
  return void 0;
@@ -45240,7 +45498,7 @@ var InstanceLock = class {
45240
45498
  if (!this.held) return false;
45241
45499
  let current;
45242
45500
  try {
45243
- current = readFileSync13(this.path, "utf8");
45501
+ current = readFileSync14(this.path, "utf8");
45244
45502
  } catch {
45245
45503
  current = void 0;
45246
45504
  }
@@ -45258,7 +45516,7 @@ var InstanceLock = class {
45258
45516
  let ageMs;
45259
45517
  this.env.beforeLockRead?.();
45260
45518
  try {
45261
- raw = readFileSync13(this.path, "utf8");
45519
+ raw = readFileSync14(this.path, "utf8");
45262
45520
  ageMs = Date.now() - statSync6(this.path).mtimeMs;
45263
45521
  } catch (error) {
45264
45522
  if (errno(error) === "ENOENT") {
@@ -45299,7 +45557,7 @@ var InstanceLock = class {
45299
45557
  const written = this.written;
45300
45558
  this.drop();
45301
45559
  try {
45302
- if (written !== void 0 && readFileSync13(this.path, "utf8") === written) unlinkSync3(this.path);
45560
+ if (written !== void 0 && readFileSync14(this.path, "utf8") === written) unlinkSync3(this.path);
45303
45561
  } catch {
45304
45562
  }
45305
45563
  }
@@ -45313,7 +45571,7 @@ var InstanceLock = class {
45313
45571
  took() {
45314
45572
  this.held = true;
45315
45573
  try {
45316
- this.written = readFileSync13(this.path, "utf8");
45574
+ this.written = readFileSync14(this.path, "utf8");
45317
45575
  } catch {
45318
45576
  this.written = void 0;
45319
45577
  }
@@ -45395,7 +45653,7 @@ var InstanceLock = class {
45395
45653
  this.env.beforeTakeoverReread?.();
45396
45654
  let current;
45397
45655
  try {
45398
- current = readFileSync13(this.path, "utf8");
45656
+ current = readFileSync14(this.path, "utf8");
45399
45657
  } catch {
45400
45658
  current = void 0;
45401
45659
  }
@@ -45409,7 +45667,7 @@ var InstanceLock = class {
45409
45667
  if (created !== "created") {
45410
45668
  if (created === "failed") return "failed";
45411
45669
  try {
45412
- this.holderPid = parseBody(readFileSync13(this.path, "utf8"))?.pid;
45670
+ this.holderPid = parseBody(readFileSync14(this.path, "utf8"))?.pid;
45413
45671
  } catch {
45414
45672
  }
45415
45673
  return "held";
@@ -45435,7 +45693,7 @@ import {
45435
45693
  fsyncSync as fsyncSync2,
45436
45694
  lstatSync as lstatSync8,
45437
45695
  openSync as openSync5,
45438
- readFileSync as readFileSync14,
45696
+ readFileSync as readFileSync15,
45439
45697
  realpathSync as realpathSync2,
45440
45698
  renameSync as renameSync5,
45441
45699
  unlinkSync as unlinkSync4,
@@ -45472,7 +45730,7 @@ function isSymlink(path) {
45472
45730
  }
45473
45731
  }
45474
45732
  function readJson(path) {
45475
- return JSON.parse(readFileSync14(path, "utf8"));
45733
+ return JSON.parse(readFileSync15(path, "utf8"));
45476
45734
  }
45477
45735
  function publicKeyOf(v) {
45478
45736
  const r = parsePinObject(v);
@@ -45617,6 +45875,20 @@ function runPinCli(argv, io) {
45617
45875
  return 1;
45618
45876
  }
45619
45877
 
45878
+ // src/service/credential-arg.ts
45879
+ function readCredentialArg(argv, env) {
45880
+ const i = argv.indexOf("--credential");
45881
+ if (i >= 0) {
45882
+ const v = argv[i + 1];
45883
+ return v ? { kind: "path", path: v } : { kind: "empty_flag" };
45884
+ }
45885
+ const fromEnv = env.SECURECOMS_CREDENTIAL;
45886
+ return fromEnv ? { kind: "path", path: fromEnv } : { kind: "absent" };
45887
+ }
45888
+ function noCredentialLine(verb) {
45889
+ return verb === "run" ? "No key file was given. If you used the copied run command, no key file for this agent was found in your Downloads folder." : "No key file was found in your Downloads folder. Download the key file again from SecureComs, or add --credential <path>.";
45890
+ }
45891
+
45620
45892
  // src/bin.ts
45621
45893
  function nodeMajorOk(version) {
45622
45894
  return Number(version.split(".")[0]) >= 24;
@@ -45653,15 +45925,22 @@ function realServiceDeps() {
45653
45925
  removeFile: (p) => rmSync8(p, { force: true }),
45654
45926
  mkdirp: (p) => mkdirSync8(p, { recursive: true }),
45655
45927
  fileExists: (p) => existsSync7(p),
45656
- user: userInfo().username
45928
+ user: userInfo().username,
45929
+ pidAlive: pidAliveByKill,
45930
+ // installService is synchronous; Atomics.wait is a real blocking sleep.
45931
+ sleepMs: (ms) => {
45932
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
45933
+ },
45934
+ now: () => Date.now()
45657
45935
  };
45658
45936
  }
45659
45937
  function chooseInstallExecPath(runningExecPath, override, deps) {
45660
45938
  if (override) return override;
45661
45939
  const r = resolveStableNodePath(runningExecPath, deps);
45662
45940
  if (!r.stable) {
45941
+ const list = STABLE_LOCATIONS.slice(0, -1).join(", ") + " or " + STABLE_LOCATIONS[STABLE_LOCATIONS.length - 1];
45663
45942
  throw new Error(
45664
- "install: no reboot-stable Node found. Version-manager Nodes (nvm/asdf/fnm/volta/n) are removed on upgrade and will break the daemon after a reboot. Install a system Node (nodejs.org pkg \u2192 /usr/local/bin/node) or pass --exec-path <stable-path> to override."
45943
+ "The agent's background service needs Node " + REQUIRED_NODE_MAJOR + " or newer at a fixed location (" + list + "), and none was found. Node from nvm, fnm and similar tools moves when it is upgraded, so it is not used. Install Node " + REQUIRED_NODE_MAJOR + " from https://nodejs.org, then run the install command again."
45665
45944
  );
45666
45945
  }
45667
45946
  return r.path;
@@ -45695,7 +45974,7 @@ function realResolveDeps() {
45695
45974
  }
45696
45975
  function realPrepDeps() {
45697
45976
  return {
45698
- readFile: (p) => readFileSync16(p, "utf8"),
45977
+ readFile: (p) => readFileSync17(p, "utf8"),
45699
45978
  writeFileMode: (p, data, mode) => writeFileSync5(p, data, { mode }),
45700
45979
  mkdirp: (p, mode) => {
45701
45980
  mkdirSync8(p, { recursive: true, mode: mode ?? 493 });
@@ -45748,7 +46027,7 @@ function realReadFleet() {
45748
46027
  return [];
45749
46028
  }
45750
46029
  },
45751
- readFile: (p) => readFileSync16(p, "utf8")
46030
+ readFile: (p) => readFileSync17(p, "utf8")
45752
46031
  });
45753
46032
  }
45754
46033
  function runStatus() {
@@ -45773,7 +46052,7 @@ function realCopyConnector(srcDir, dstDir, harness) {
45773
46052
  }
45774
46053
  if (name.includes(".test.")) continue;
45775
46054
  if (!allowedExt.some((ext) => name.endsWith(ext))) continue;
45776
- writeFileSync5(join25(dstDir, name), readFileSync16(srcPath));
46055
+ writeFileSync5(join25(dstDir, name), readFileSync17(srcPath));
45777
46056
  }
45778
46057
  }
45779
46058
  function realWriteBinding(dstDir, identityId) {
@@ -45782,7 +46061,7 @@ function realWriteBinding(dstDir, identityId) {
45782
46061
  }
45783
46062
  function realReadBinding(dstDir) {
45784
46063
  try {
45785
- const parsed = JSON.parse(readFileSync16(join25(dstDir, "securecoms.binding.json"), "utf8"));
46064
+ const parsed = JSON.parse(readFileSync17(join25(dstDir, "securecoms.binding.json"), "utf8"));
45786
46065
  return parsed.identity_id ?? null;
45787
46066
  } catch {
45788
46067
  return null;
@@ -45800,7 +46079,7 @@ function realStatDir(p) {
45800
46079
  }
45801
46080
  function realReadHermesConfig(p) {
45802
46081
  try {
45803
- return readFileSync16(p, "utf8");
46082
+ return readFileSync17(p, "utf8");
45804
46083
  } catch {
45805
46084
  return null;
45806
46085
  }
@@ -45841,9 +46120,17 @@ function runWireVerb(verb) {
45841
46120
  process.exit(verb === "wire" ? runWire(realWireDeps()) : runUnwire(realWireDeps()));
45842
46121
  }
45843
46122
  async function runServiceVerb(verb) {
45844
- const credentialPath = argOrEnv("--credential", "SECURECOMS_CREDENTIAL");
46123
+ let credentialPath;
46124
+ if (verb === "install") {
46125
+ const cred = readCredentialArg(process.argv, process.env);
46126
+ credentialPath = cred.kind === "path" ? cred.path : void 0;
46127
+ } else {
46128
+ credentialPath = argOrEnv("--credential", "SECURECOMS_CREDENTIAL");
46129
+ }
45845
46130
  const label = argOrEnv("--label", "SECURECOMS_LABEL");
45846
46131
  if (verb === "install" ? !credentialPath : !credentialPath && !label) {
46132
+ if (verb === "install") process.stderr.write(`${noCredentialLine("install")}
46133
+ `);
45847
46134
  const execPathUsage = verb === "install" ? " [--exec-path <path>]" : "";
45848
46135
  process.stderr.write(`usage: agent-bridge ${verb} --credential <path> [--label <name>] [--store <path>] [--port N] [--harness openclaw|hermes|universal]${execPathUsage}
45849
46136
  `);
@@ -45966,6 +46253,26 @@ var HELD_POLL_TOTAL_MS = 1e4;
45966
46253
  var HELD_POLL_INTERVAL_MS = 500;
45967
46254
  var HELD_NO_PID_REACQUIRES = 3;
45968
46255
  var SHUTDOWN_HARD_EXIT_MS = 8e3;
46256
+ function configErrorLine(reason, o) {
46257
+ const at = o.isTTY ? ` at ${o.path}` : "";
46258
+ switch (reason) {
46259
+ case "credential_not_found":
46260
+ return `Can't find the key file${at}. Check the path after --credential.`;
46261
+ case "credential_unreadable":
46262
+ return `Can't open the key file${at}. Check that it exists and that your user can read it.`;
46263
+ case "credential_not_json":
46264
+ case "identity_id_invalid":
46265
+ return `The key file${at} is damaged. Get a new key file from the agent's page in SecureComs.`;
46266
+ case "credential_missing_field":
46267
+ return `The key file${at} is incomplete (no ${o.field}). Get a new key file from the agent's page in SecureComs.`;
46268
+ case "credential_voice_invalid":
46269
+ return o.source === "env" ? "The voice settings in this computer's environment are not valid." : `The voice settings in the key file${at} are not valid.`;
46270
+ case "bind_refused":
46271
+ return "The Connector only accepts connections from this computer unless TLS is set. Remove --bind, or add --tls-cert and --tls-key.";
46272
+ case "unknown":
46273
+ return void 0;
46274
+ }
46275
+ }
45969
46276
  function realRunModeDeps(credentialPath, storePath) {
45970
46277
  return {
45971
46278
  credentialPath,
@@ -45996,22 +46303,56 @@ function realRunModeDeps(credentialPath, storePath) {
45996
46303
  process.stderr.write(`${line}
45997
46304
  `);
45998
46305
  },
46306
+ isTTY: process.stderr.isTTY === true,
45999
46307
  log: writeServiceLog
46000
46308
  };
46001
46309
  }
46002
46310
  async function runMode(deps) {
46003
46311
  const { credentialPath, storePath, log } = deps;
46312
+ const refuse2 = (reason, extra, e) => {
46313
+ const fields = {
46314
+ reason: ServiceLogEnum.of(reason, [
46315
+ "credential_not_found",
46316
+ "credential_unreadable",
46317
+ "credential_not_json",
46318
+ "credential_missing_field",
46319
+ "credential_voice_invalid",
46320
+ "identity_id_invalid",
46321
+ "bind_refused",
46322
+ "unknown"
46323
+ ])
46324
+ };
46325
+ if (extra.field) {
46326
+ fields.field = ServiceLogEnum.of(extra.field, [
46327
+ "api_key",
46328
+ "identity_id",
46329
+ "device_id",
46330
+ "signing_private_key",
46331
+ "enclave_id",
46332
+ "api_base"
46333
+ ]);
46334
+ }
46335
+ log("error", "startup.config_invalid", fields, e);
46336
+ const line = configErrorLine(reason, { path: credentialPath, isTTY: deps.isTTY, ...extra });
46337
+ if (line !== void 0) deps.print(line);
46338
+ };
46004
46339
  let cfg, daemon;
46005
46340
  try {
46006
46341
  cfg = loadConfig({ credentialPath, storePath });
46007
46342
  daemon = parseDaemonArgs(deps.argv, deps.env);
46343
+ } catch (e) {
46344
+ if (e instanceof ConfigError) refuse2(e.reason, { field: e.field, source: e.source }, e);
46345
+ else refuse2("unknown", {}, e);
46346
+ return deps.exit(1);
46347
+ }
46348
+ try {
46008
46349
  validateBind(daemon.bind, daemon.tls);
46009
46350
  } catch (e) {
46010
- log("error", "startup.config_invalid", {}, e);
46351
+ refuse2("bind_refused", {}, e);
46011
46352
  return deps.exit(1);
46012
46353
  }
46013
46354
  if (!CANONICAL_UUID2.test(cfg.identityId)) {
46014
- log("error", "startup.config_invalid", { field: ServiceLogEnum.of("identity_id", ["identity_id"]) });
46355
+ refuse2("identity_id_invalid", { field: "identity_id" });
46015
46356
  return deps.exit(1);
46016
46357
  }
46017
46358
  const lockDir = join25(deps.home, ".securecoms", "locks");
@@ -46050,7 +46391,9 @@ async function runMode(deps) {
46050
46391
  lock_path: ServiceLogEnum.of("home_securecoms_locks_identity", ["home_securecoms_locks_identity"]),
46051
46392
  remedy: ServiceLogEnum.of("delete_lock_if_no_other_host", ["delete_lock_if_no_other_host"])
46052
46393
  });
46053
- deps.print(`This agent's lock file was written under a different computer name: ${lockPath}. If no other computer runs this agent, delete that file and start the agent again.`);
46394
+ deps.print(
46395
+ deps.isTTY ? `This agent's lock file was written under a different computer name: ${lockPath}. If no other computer runs this agent, delete that file and start the agent again.` : "This agent's lock file was written under a different computer name. If no other computer runs this agent, delete that file and start the agent again."
46396
+ );
46054
46397
  return deps.exit(1);
46055
46398
  case "unreadable":
46056
46399
  log("error", "startup.instance_lock_unreadable", {}, lock.lastError);
@@ -46126,7 +46469,7 @@ async function runMode(deps) {
46126
46469
  served = await deps.serve(agent, {
46127
46470
  token,
46128
46471
  allowOrigins: daemon.allowOrigins,
46129
- readTls: () => daemon.tls ? { cert: readFileSync16(daemon.tlsCert, "utf8"), key: readFileSync16(daemon.tlsKey, "utf8") } : void 0
46472
+ readTls: () => daemon.tls ? { cert: readFileSync17(daemon.tlsCert, "utf8"), key: readFileSync17(daemon.tlsKey, "utf8") } : void 0
46130
46473
  });
46131
46474
  } catch (e) {
46132
46475
  if (!(e instanceof SessionSupersededError)) throw e;
@@ -46171,6 +46514,10 @@ async function runMode(deps) {
46171
46514
  }
46172
46515
  });
46173
46516
  }
46517
+ function printNoCredentialRunLine() {
46518
+ process.stderr.write(`${noCredentialLine("run")}
46519
+ `);
46520
+ }
46174
46521
  async function main() {
46175
46522
  if (!nodeMajorOk(process.versions.node)) {
46176
46523
  writeServiceLog("error", "startup.node_unsupported");
@@ -46197,11 +46544,13 @@ async function main() {
46197
46544
  await runServiceVerb(verb);
46198
46545
  return;
46199
46546
  }
46200
- const credentialPath = argOrEnv("--credential", "SECURECOMS_CREDENTIAL");
46201
- if (!credentialPath) {
46547
+ const cred = readCredentialArg(process.argv, process.env);
46548
+ if (cred.kind !== "path") {
46549
+ printNoCredentialRunLine();
46202
46550
  writeServiceLog("error", "startup.credential_required.use_--credential");
46203
46551
  process.exit(2);
46204
46552
  }
46553
+ const credentialPath = cred.path;
46205
46554
  const storePath = argOrEnv("--store", "SECURECOMS_STORE");
46206
46555
  await runMode(realRunModeDeps(credentialPath, storePath));
46207
46556
  }
@@ -46219,6 +46568,7 @@ export {
46219
46568
  HELD_POLL_TOTAL_MS,
46220
46569
  SHUTDOWN_HARD_EXIT_MS,
46221
46570
  chooseInstallExecPath,
46571
+ configErrorLine,
46222
46572
  formatServiceError,
46223
46573
  nodeMajorOk,
46224
46574
  parseVerb,