@securecoms/agent-bridge 0.30.0 → 0.30.1

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 readFileSync14, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
7559
+ import { readFileSync as readFileSync15, 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 = [];
@@ -7564,7 +7564,7 @@ async function importModelBundle(srcDir, root3, m) {
7564
7564
  const src = join24(srcDir, f.name);
7565
7565
  let size;
7566
7566
  try {
7567
- const s = statSync6(src);
7567
+ const s = statSync7(src);
7568
7568
  if (!s.isFile()) throw new Error("not a file");
7569
7569
  size = s.size;
7570
7570
  } catch {
@@ -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 = readFileSync14(src);
7577
+ bytes = readFileSync15(src);
7578
7578
  } catch {
7579
7579
  throw new ModelImportError(`${f.name} is missing from the import directory`);
7580
7580
  }
@@ -24803,6 +24803,7 @@ function base64UrlNoPad(s) {
24803
24803
  }
24804
24804
  var BEARER_SUBPROTOCOL = "securecoms.bearer.v1";
24805
24805
  var WS_CLOSE_IDENTITY_REVOKED = 4006;
24806
+ var WS_CLOSE_SESSION_SUPERSEDED = 4409;
24806
24807
  function isIdentityRevokedClose(code, reason) {
24807
24808
  return code === WS_CLOSE_IDENTITY_REVOKED || reason === "identity_revoked";
24808
24809
  }
@@ -25018,7 +25019,7 @@ function openDeliveryPings(o) {
25018
25019
  end();
25019
25020
  };
25020
25021
  if (!hardened) {
25021
- if (code === 4001 || isIdentityRevokedClose(code, reason)) {
25022
+ if (code === 4001 || code === WS_CLOSE_SESSION_SUPERSEDED || isIdentityRevokedClose(code, reason)) {
25022
25023
  terminalClose();
25023
25024
  return;
25024
25025
  }
@@ -25027,7 +25028,7 @@ function openDeliveryPings(o) {
25027
25028
  }
25028
25029
  const tr = terminalReason;
25029
25030
  terminalReason = void 0;
25030
- if (code === 4006 || tr && code === 4001) {
25031
+ if (code === 4006 || code === WS_CLOSE_SESSION_SUPERSEDED || tr && code === 4001) {
25031
25032
  lifecycle("terminal", tr ?? `code ${code}`);
25032
25033
  terminalClose();
25033
25034
  return;
@@ -25184,10 +25185,14 @@ var RestTransport = class {
25184
25185
  f;
25185
25186
  ws;
25186
25187
  tokenProvider;
25187
- /** The most recent device-wide WS send handle, captured on subscribe and
25188
- * cleared on its close. Control frames (status keepalive) route through it;
25189
- * a no-op when no subscription is active. */
25188
+ /** The shared delivery-ping socket's send, set while that socket is open and
25189
+ * cleared when it closes (last subscriber gone, or terminal). Control frames
25190
+ * (status keepalive) route through it; a no-op when no subscription is active. */
25190
25191
  _controlSend = null;
25192
+ /** P-5: the one delivery-ping stream this transport holds, shared by every subscriber. */
25193
+ pingHub = null;
25194
+ /** Set once the shared stream ends on a terminal close; no socket is ever reopened. */
25195
+ pingsTerminal = false;
25191
25196
  constructor(opts) {
25192
25197
  this.opts = opts;
25193
25198
  this.f = opts.fetchImpl ?? fetch;
@@ -25289,25 +25294,52 @@ var RestTransport = class {
25289
25294
  idempotentReplay: j.idempotent_replay === true
25290
25295
  };
25291
25296
  }
25297
+ /** One device-wide delivery-ping socket per transport (P-5): the backend
25298
+ * supersedes a second socket for the same device, so every subscriber shares
25299
+ * one `openDeliveryPings` handle. Each gets its own bounded queue; its close()
25300
+ * detaches only itself; the socket closes with the last subscriber and a later
25301
+ * subscribe opens a fresh one. After a terminal close the transport is
25302
+ * terminal: later calls open no socket and get an already-ended iterator. */
25292
25303
  subscribeDeliveryPings(_channelId) {
25304
+ if (this.pingsTerminal) {
25305
+ const ended = new PingSubscriber();
25306
+ ended.end();
25307
+ return { pings: ended.pings, close: () => {
25308
+ }, send: () => {
25309
+ } };
25310
+ }
25311
+ const hub = this.pingHub ??= this.openPingHub();
25312
+ const sub = hub.attach();
25313
+ return { pings: sub.pings, close: () => hub.detach(sub), send: hub.send };
25314
+ }
25315
+ openPingHub() {
25293
25316
  let wsUrl = this.opts.baseUrl.replace(/^http/, "ws").replace(/\/$/, "") + "/api/v1/ws";
25294
25317
  if (this.opts.connectIdentityId) {
25295
25318
  wsUrl += `?connect_identity_id=${encodeURIComponent(this.opts.connectIdentityId)}`;
25296
25319
  }
25297
- const handle = openDeliveryPings({
25320
+ const hub = new PingHub(openDeliveryPings({
25298
25321
  url: wsUrl,
25299
25322
  tokenProvider: this.tokenProvider,
25300
25323
  wsImpl: this.ws,
25301
25324
  hardened: this.opts.hardenedReconnect ?? false,
25302
- onLifecycle: this.opts.onWsLifecycle
25303
- });
25304
- this._controlSend = handle.send;
25305
- const close = () => {
25306
- if (this._controlSend === handle.send)
25307
- this._controlSend = null;
25308
- handle.close();
25309
- };
25310
- return { pings: handle.pings, close, send: handle.send };
25325
+ onLifecycle: this.opts.onWsLifecycle,
25326
+ // Any terminal close (4001, 4006, 4409, hardened war-cap) of the shared
25327
+ // socket makes this transport terminal. The shared iterator ends right
25328
+ // after this call, so the pump exit tears down every subscriber.
25329
+ onTerminalClose: (close) => {
25330
+ this.pingsTerminal = true;
25331
+ this.opts.onTerminalClose?.(close);
25332
+ }
25333
+ }), () => this.dropPingHub(hub));
25334
+ this._controlSend = hub.send;
25335
+ return hub;
25336
+ }
25337
+ dropPingHub(hub) {
25338
+ if (this.pingHub === hub) {
25339
+ this.pingHub = null;
25340
+ this._controlSend = null;
25341
+ }
25342
+ hub.shutdown();
25311
25343
  }
25312
25344
  sendControlFrame(_channelId, frame) {
25313
25345
  try {
@@ -25335,6 +25367,90 @@ var RestTransport = class {
25335
25367
  }));
25336
25368
  }
25337
25369
  };
25370
+ var SUBSCRIBER_QUEUE_MAX = 8;
25371
+ var PingSubscriber = class {
25372
+ queue = [];
25373
+ waiter;
25374
+ ended = false;
25375
+ started = false;
25376
+ pings = {
25377
+ [Symbol.asyncIterator]: () => {
25378
+ if (this.started)
25379
+ throw new Error("subscribeDeliveryPings: single-use async iterable");
25380
+ this.started = true;
25381
+ return {
25382
+ next: () => {
25383
+ if (this.ended)
25384
+ return Promise.resolve({ value: void 0, done: true });
25385
+ if (this.queue.length)
25386
+ return Promise.resolve({ value: this.queue.shift(), done: false });
25387
+ return new Promise((resolve4) => {
25388
+ this.waiter = resolve4;
25389
+ });
25390
+ }
25391
+ };
25392
+ }
25393
+ };
25394
+ push(p) {
25395
+ if (this.ended)
25396
+ return;
25397
+ if (this.waiter) {
25398
+ const w = this.waiter;
25399
+ this.waiter = void 0;
25400
+ w({ value: p, done: false });
25401
+ } else if (this.queue.length < SUBSCRIBER_QUEUE_MAX) {
25402
+ this.queue.push(p);
25403
+ }
25404
+ }
25405
+ end() {
25406
+ this.ended = true;
25407
+ this.queue.length = 0;
25408
+ if (this.waiter) {
25409
+ const w = this.waiter;
25410
+ this.waiter = void 0;
25411
+ w({ value: void 0, done: true });
25412
+ }
25413
+ }
25414
+ };
25415
+ var PingHub = class {
25416
+ handle;
25417
+ onGone;
25418
+ subs = /* @__PURE__ */ new Set();
25419
+ send;
25420
+ constructor(handle, onGone) {
25421
+ this.handle = handle;
25422
+ this.onGone = onGone;
25423
+ this.send = handle.send;
25424
+ void this.pump();
25425
+ }
25426
+ /** `openDeliveryPings` iterables never reject and push() never throws, so
25427
+ * this loop only ends when the shared stream ends (our close, or terminal). */
25428
+ async pump() {
25429
+ for await (const p of this.handle.pings)
25430
+ for (const s of this.subs)
25431
+ s.push(p);
25432
+ this.onGone();
25433
+ }
25434
+ attach() {
25435
+ const s = new PingSubscriber();
25436
+ this.subs.add(s);
25437
+ return s;
25438
+ }
25439
+ /** Idempotent: detaches and ends only `s`; the last one out closes the socket. */
25440
+ detach(s) {
25441
+ this.subs.delete(s);
25442
+ s.end();
25443
+ if (this.subs.size === 0)
25444
+ this.onGone();
25445
+ }
25446
+ /** Idempotent: `openDeliveryPings().close()` is, and the set is empty after. */
25447
+ shutdown() {
25448
+ for (const s of this.subs)
25449
+ s.end();
25450
+ this.subs.clear();
25451
+ this.handle.close();
25452
+ }
25453
+ };
25338
25454
 
25339
25455
  // ../connect-sdk-ts/dist/mls/rest-backend.js
25340
25456
  function describeType(v) {
@@ -35208,6 +35324,7 @@ var ROOM_DEFAULT_READ_GRANTS = Object.freeze([
35208
35324
  Object.freeze({ tool: "current_time", mode: "read" })
35209
35325
  ]);
35210
35326
  var NO_TOOL_GRANTS = Object.freeze([]);
35327
+ var WS_CLOSE_SESSION_SUPERSEDED2 = 4409;
35211
35328
  var OWNER_REPLAY_PRUNE_INTERVAL_MS = 5 * 60 * 1e3;
35212
35329
  var OWNER_TURN_UNCHECKED = Object.freeze({ kind: "unchecked" });
35213
35330
  function ownerDmDefaultGrantsEnabled(env = process.env) {
@@ -35482,6 +35599,12 @@ function rosterFreshAt(entry, heldEpoch, heldGroupId) {
35482
35599
  var RoomGrantEpochMovedError = class extends Error {
35483
35600
  };
35484
35601
  var ROOM_GRANT_TRACK_MAX = 4096;
35602
+ var SessionSupersededError = class extends Error {
35603
+ constructor() {
35604
+ super("session_superseded: another copy of this agent took its session during start()");
35605
+ this.name = "SessionSupersededError";
35606
+ }
35607
+ };
35485
35608
  var GroupSupersededSendError = class extends Error {
35486
35609
  constructor(channelId, activeGroupId) {
35487
35610
  super(`send refused for channel ${channelId}: the channel moved to a new secure group; not sent`);
@@ -35521,9 +35644,18 @@ var BridgeAgent = class _BridgeAgent {
35521
35644
  const seed = hexToBytes4(cfg.signingPrivateKey);
35522
35645
  this.custody = SoftwareKeyCustody.fromPrivateKey("ed25519", seed);
35523
35646
  this.replyIdKey = deriveReplyIdKey(seed);
35647
+ const base = deps.fetchImpl ?? fetch;
35648
+ this.fetchImpl = (input, init) => this.sessionSuperseded ? Promise.reject(new Error("session_superseded")) : base(input, init);
35524
35649
  this.transport = deps.transport ?? new RestTransport({
35525
35650
  baseUrl: cfg.apiBase,
35526
35651
  bearerToken: cfg.apiKey,
35652
+ fetchImpl: this.fetchImpl,
35653
+ wsImpl: deps.wsImpl,
35654
+ // A 4409 close means another copy of this agent took the session.
35655
+ // Every other terminal close keeps today's behaviour.
35656
+ onTerminalClose: (close) => {
35657
+ if (close.code === WS_CLOSE_SESSION_SUPERSEDED2) this.onSessionSuperseded();
35658
+ },
35527
35659
  connectIdentityId: cfg.identityId,
35528
35660
  // P1b: hardened delivery-ping reconnect (verify-before-terminal +
35529
35661
  // half-open + wake + auth war-cap). On by default for the daemon;
@@ -35537,7 +35669,7 @@ var BridgeAgent = class _BridgeAgent {
35537
35669
  else writeServiceLog("warn", "transport.delivery_ping_lifecycle_unknown");
35538
35670
  }
35539
35671
  });
35540
- this.backend = deps.backend ?? new RestMlsBackend({ baseUrl: cfg.apiBase, bearerToken: cfg.apiKey });
35672
+ this.backend = deps.backend ?? new RestMlsBackend({ baseUrl: cfg.apiBase, bearerToken: cfg.apiKey, fetchImpl: this.fetchImpl });
35541
35673
  this.store = new SqliteMlsStateStore({
35542
35674
  dbPath: cfg.storePath,
35543
35675
  identityId: cfg.identityId,
@@ -35608,7 +35740,6 @@ var BridgeAgent = class _BridgeAgent {
35608
35740
  // throws (mirrors the divergence-heal signal path).
35609
35741
  onSupersedeUnopenable: (channelId, groupId) => this.healSupersedeUnopenable(channelId, groupId, this.drainExemptionAtCallIn())
35610
35742
  });
35611
- this.fetchImpl = deps.fetchImpl ?? fetch;
35612
35743
  this.home = deps.home ?? homedir4();
35613
35744
  this.grantWriters = {
35614
35745
  writeManifestPolicy: deps.grantWriters?.writeManifestPolicy ?? writeManifestPolicy,
@@ -35740,6 +35871,13 @@ var BridgeAgent = class _BridgeAgent {
35740
35871
  // stored for peek-then-join in ingestOnce
35741
35872
  custody;
35742
35873
  fetchImpl;
35874
+ /** P-1: set once a 4409 close says another copy took this agent's session.
35875
+ * The gated fetch built in the constructor refuses every request after it. */
35876
+ sessionSuperseded = false;
35877
+ /** C9: who hears `session_superseded` (runMode's quiesce). */
35878
+ sessionSupersededListeners = /* @__PURE__ */ new Set();
35879
+ /** C9 (spec §4.1.6, low 3): the one close() run every caller shares. */
35880
+ closing = null;
35743
35881
  /** Round 5: home for the managed runtime-config directory (test seam). */
35744
35882
  home;
35745
35883
  grantWriters;
@@ -36215,6 +36353,12 @@ var BridgeAgent = class _BridgeAgent {
36215
36353
  action;
36216
36354
  waiters = [];
36217
36355
  pingHandle;
36356
+ /** C10 (P-15): the delivery-ping subscription start() opens at its top,
36357
+ * held unconsumed until startReceivePump() takes it. */
36358
+ earlyPings = null;
36359
+ /** C10 fix round 1: settles (never rejects) when start()'s body does; close()
36360
+ * awaits it, bounded, so the store never closes under a start() step. */
36361
+ startInFlight = null;
36218
36362
  safetyTimer;
36219
36363
  /** The ingest cycle now running, if any (see ingestOnce: single-flight). */
36220
36364
  ingestInFlight = null;
@@ -36792,7 +36936,50 @@ var BridgeAgent = class _BridgeAgent {
36792
36936
  this.droppedMessages += overflow;
36793
36937
  }
36794
36938
  }
36939
+ /**
36940
+ * C10 (spec P-15, OQ-8 (b)): subscribe early, consume late. The
36941
+ * delivery-ping socket opens here, before anything else, so this copy claims
36942
+ * the device's session (and another copy gets its 4409) BEFORE this copy's
36943
+ * boot drain runs. The pings it carries are NOT consumed until
36944
+ * startReceivePump(), the same place as before: starting the pump first is
36945
+ * still forbidden (DRAIN BEFORE ROTATE, startBody).
36946
+ *
36947
+ * A 4409 during start() (audit H-3): the gated fetch is already set (C9 step
36948
+ * 0). start() then arms nothing more (supersededDuringStart before each
36949
+ * timer and before the pump), runs the memoized close() quiesce, and rejects
36950
+ * with SessionSupersededError. A step that FAILED because of the supersede
36951
+ * (a refused fetch, a store the quiesce closed) is the same outcome.
36952
+ */
36795
36953
  async start() {
36954
+ const early = this.transport.subscribeDeliveryPings("");
36955
+ this.earlyPings = early;
36956
+ this.pingHandle = { close: early.close };
36957
+ const body = this.startBody();
36958
+ this.startInFlight = body.then(() => {
36959
+ }, () => {
36960
+ });
36961
+ try {
36962
+ await body;
36963
+ } catch (e) {
36964
+ if (!this.sessionSuperseded) {
36965
+ if (this.earlyPings) {
36966
+ this.earlyPings.close();
36967
+ this.earlyPings = null;
36968
+ this.pingHandle = void 0;
36969
+ }
36970
+ throw e;
36971
+ }
36972
+ if (!(e instanceof SessionSupersededError)) writeServiceLog("warn", "startup.superseded_start_error", {}, e);
36973
+ await this.close().catch(() => {
36974
+ });
36975
+ throw new SessionSupersededError();
36976
+ }
36977
+ }
36978
+ /** C10: true once a 4409 landed; checked before each arming step of start(). */
36979
+ supersededDuringStart() {
36980
+ return this.sessionSuperseded;
36981
+ }
36982
+ async startBody() {
36796
36983
  this.startedAt = Date.now();
36797
36984
  await this.restoreRuntimeInbox();
36798
36985
  await this.loadToolGrantState();
@@ -36852,10 +37039,12 @@ var BridgeAgent = class _BridgeAgent {
36852
37039
  await this.warmRestoredNotes();
36853
37040
  await this.reindexRestoredTurns();
36854
37041
  if (this.unifiedOn) await this.reindexUnifiedRestored();
37042
+ if (this.supersededDuringStart()) throw new SessionSupersededError();
36855
37043
  this.runtimeConfigTimer = setInterval(() => {
36856
37044
  void this.refetchRuntimeConfig();
36857
37045
  }, 6 * 60 * 60 * 1e3);
36858
37046
  this.runtimeConfigTimer.unref?.();
37047
+ if (this.supersededDuringStart()) throw new SessionSupersededError();
36859
37048
  this.membershipSweepTimer = setInterval(() => {
36860
37049
  if (this.closingDown || this.membershipSweepWork) return;
36861
37050
  this.membershipSweepWork = this.runMembershipSweeps().finally(() => {
@@ -36863,6 +37052,7 @@ var BridgeAgent = class _BridgeAgent {
36863
37052
  });
36864
37053
  }, PURGE_SWEEP_INTERVAL_MS);
36865
37054
  this.membershipSweepTimer.unref?.();
37055
+ if (this.supersededDuringStart()) throw new SessionSupersededError();
36866
37056
  this.recallPruneTimer = setInterval(() => {
36867
37057
  if (this.closingDown || this.recallPruneWork) return;
36868
37058
  this.recallPruneWork = this.runRecallPruneTick().finally(() => {
@@ -36870,6 +37060,7 @@ var BridgeAgent = class _BridgeAgent {
36870
37060
  });
36871
37061
  }, 60 * 60 * 1e3);
36872
37062
  this.recallPruneTimer.unref?.();
37063
+ if (this.supersededDuringStart()) throw new SessionSupersededError();
36873
37064
  if (this.machinePaths) this.recallMaintenanceTimer = setInterval(() => {
36874
37065
  if (this.closingDown) return;
36875
37066
  const n = ++this.maintenanceTickN;
@@ -36880,6 +37071,7 @@ var BridgeAgent = class _BridgeAgent {
36880
37071
  });
36881
37072
  }, 1e3);
36882
37073
  this.recallMaintenanceTimer?.unref?.();
37074
+ if (this.supersededDuringStart()) throw new SessionSupersededError();
36883
37075
  this.startReceivePump();
36884
37076
  if (drainedInTime) {
36885
37077
  this.armSelfHealSweeps();
@@ -41143,7 +41335,8 @@ var BridgeAgent = class _BridgeAgent {
41143
41335
  }
41144
41336
  }
41145
41337
  startReceivePump() {
41146
- const sub = this.transport.subscribeDeliveryPings("");
41338
+ const sub = this.earlyPings ?? this.transport.subscribeDeliveryPings("");
41339
+ this.earlyPings = null;
41147
41340
  this.pingHandle = { close: sub.close };
41148
41341
  void (async () => {
41149
41342
  try {
@@ -42728,7 +42921,42 @@ ${channelId}`;
42728
42921
  build: BRIDGE_VERSION
42729
42922
  };
42730
42923
  }
42731
- async close() {
42924
+ /** C9: `session_superseded` once a 4409 said another copy took this
42925
+ * agent's session (the gated fetch already refuses every request). The
42926
+ * local HTTP routes read it: every route but `/healthz` answers 503. */
42927
+ get sessionState() {
42928
+ return this.sessionSuperseded ? "session_superseded" : "live";
42929
+ }
42930
+ /** C9: subscribe to `session_superseded`. runMode subscribes right after
42931
+ * construction, before start(), so a 4409 during start() is heard too. */
42932
+ on(event, listener) {
42933
+ this.sessionSupersededListeners.add(listener);
42934
+ return () => this.sessionSupersededListeners.delete(listener);
42935
+ }
42936
+ /** P-1 step 0, then the event (spec §4.1.6): another copy of this agent took
42937
+ * the session (4409). The gated-fetch switch is set FIRST, so no HTTP
42938
+ * request leaves the process after this, whatever a listener does. */
42939
+ onSessionSuperseded() {
42940
+ this.sessionSuperseded = true;
42941
+ for (const listener of [...this.sessionSupersededListeners]) {
42942
+ try {
42943
+ listener();
42944
+ } catch (e) {
42945
+ writeServiceLog("error", "transport.session_superseded_listener_failed", {}, e);
42946
+ }
42947
+ }
42948
+ }
42949
+ /**
42950
+ * Idempotent (C9, spec §4.1.6 low 3): every call returns the FIRST call's
42951
+ * promise, so a SIGTERM during or after the supersede quiesce never runs
42952
+ * store.close() twice. A rejected close stays rejected: a retry would close
42953
+ * the store a second time.
42954
+ */
42955
+ close() {
42956
+ this.closing ??= this.closeOnce();
42957
+ return this.closing;
42958
+ }
42959
+ async closeOnce() {
42732
42960
  this.closingDown = true;
42733
42961
  this.statusGate.close();
42734
42962
  for (const t of this.pauseRecheckTimers.values()) clearTimeout(t);
@@ -42741,6 +42969,28 @@ ${channelId}`;
42741
42969
  if (this.safetyTimer) clearInterval(this.safetyTimer);
42742
42970
  if (this.runtimeConfigTimer) clearInterval(this.runtimeConfigTimer);
42743
42971
  if (this.membershipSweepTimer) clearInterval(this.membershipSweepTimer);
42972
+ const ingest = this.ingestInFlight;
42973
+ if (ingest) {
42974
+ let timer;
42975
+ await Promise.race([
42976
+ ingest.released,
42977
+ new Promise((resolve4) => {
42978
+ timer = setTimeout(resolve4, _BridgeAgent.GRACEFUL_DRAIN_MS);
42979
+ })
42980
+ ]);
42981
+ if (timer) clearTimeout(timer);
42982
+ }
42983
+ const starting = this.startInFlight;
42984
+ if (starting) {
42985
+ let timer;
42986
+ await Promise.race([
42987
+ starting,
42988
+ new Promise((resolve4) => {
42989
+ timer = setTimeout(resolve4, _BridgeAgent.GRACEFUL_DRAIN_MS);
42990
+ })
42991
+ ]);
42992
+ if (timer) clearTimeout(timer);
42993
+ }
42744
42994
  if (this.membershipSweepWork) {
42745
42995
  let timer;
42746
42996
  await Promise.race([
@@ -42899,6 +43149,9 @@ var BRIDGE_ERRORS = {
42899
43149
  // longer holds the shared bytes or key. Terminal for that link: never fall
42900
43150
  // back to the current version. `detail.pin` names the refused pin.
42901
43151
  vault_ref_changed: { http: 422, message: "this file changed in Files after it was shared; not opened" },
43152
+ // C9 (spec §4.1.6 step 1): another copy of this agent took its SecureComs
43153
+ // session (WS 4409), so this copy stopped. Every local route but /healthz.
43154
+ session_superseded: { http: 503, message: "another copy of this agent took its session; this copy stopped" },
42902
43155
  internal: { http: 500, message: "internal error" }
42903
43156
  };
42904
43157
  function errorBody(code, detail) {
@@ -43129,6 +43382,10 @@ function createHttpServer(agent, opts) {
43129
43382
  const url = new URL(req.url ?? "/", "http://localhost");
43130
43383
  const path = url.pathname;
43131
43384
  const method = req.method ?? "GET";
43385
+ if (agent.sessionState === "session_superseded") {
43386
+ if (method === "GET" && path === "/healthz") return json(res, 200, { state: "session_superseded" });
43387
+ return jsonError(res, "session_superseded");
43388
+ }
43132
43389
  if (method === "GET" && path === "/healthz") {
43133
43390
  return json(res, 200, { ...agent.health(), capabilities: capabilities(opts) });
43134
43391
  }
@@ -43340,6 +43597,7 @@ var PING_INTERVAL_MS = 3e4;
43340
43597
  var INFLIGHT_STALE_SWEEP_MS = 6e4;
43341
43598
  var INFLIGHT_STALE_AFTER_MS = 15 * 6e4;
43342
43599
  var HELLO_WAIT_MS = 2e3;
43600
+ var WS_CLOSE_SESSION_SUPERSEDED3 = 4409;
43343
43601
  var MEMORY_NOTES_FEATURE = "memory_notes";
43344
43602
  var MEMORY_RECALL_FEATURE = "memory_recall";
43345
43603
  var MEMORY_UNIFIED_FEATURE = "memory_unified";
@@ -43360,7 +43618,12 @@ function authedUpgrade(req, opts) {
43360
43618
  return true;
43361
43619
  }
43362
43620
  function attachAgentWebSocket(server, agent, opts) {
43363
- const wss = new WebSocketServer({ server, path: "/v1/ws" });
43621
+ let sessionSuperseded = false;
43622
+ const wss = new WebSocketServer({
43623
+ server,
43624
+ path: "/v1/ws",
43625
+ verifyClient: (_info, done) => sessionSuperseded ? done(false, 503, "session_superseded") : done(true)
43626
+ });
43364
43627
  const keepalive = setInterval(() => {
43365
43628
  for (const client of wss.clients) {
43366
43629
  const sock = client;
@@ -43500,6 +43763,7 @@ function attachAgentWebSocket(server, agent, opts) {
43500
43763
  if (helloTimer) clearTimeout(helloTimer);
43501
43764
  agent.noteSessionClose();
43502
43765
  unsubscribe();
43766
+ if (sessionSuperseded) inflight.clear();
43503
43767
  if (inflight.size) {
43504
43768
  agent.requeueFailedRuntimeDeliveries(
43505
43769
  [...inflight.values()].filter((m) => !agent.isRuntimeMessageRetired(m.message_id))
@@ -43521,7 +43785,10 @@ function attachAgentWebSocket(server, agent, opts) {
43521
43785
  sock.on("close", cleanup);
43522
43786
  sock.on("error", cleanup);
43523
43787
  sock.on("message", (data) => {
43524
- void handleInbound(sock, agent, data, inflight, flush, session);
43788
+ if (sessionSuperseded) return;
43789
+ handleInbound(sock, agent, data, inflight, flush, session).catch((e) => {
43790
+ writeServiceLog("error", "ws.inbound_failed", {}, e);
43791
+ });
43525
43792
  });
43526
43793
  flush();
43527
43794
  });
@@ -43536,6 +43803,13 @@ function attachAgentWebSocket(server, agent, opts) {
43536
43803
  }
43537
43804
  }
43538
43805
  wss.close();
43806
+ },
43807
+ /** C9 (spec §4.1.6 step 1, OQ-9): close every open socket with 4409 /
43808
+ * `session_superseded`, and answer every later upgrade with 503. The
43809
+ * runtime's reconnect backoff does the rest; idempotent. */
43810
+ enterSessionSuperseded() {
43811
+ sessionSuperseded = true;
43812
+ for (const client of wss.clients) client.close(WS_CLOSE_SESSION_SUPERSEDED3, "session_superseded");
43539
43813
  }
43540
43814
  };
43541
43815
  }
@@ -43837,11 +44111,12 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
43837
44111
  var LOWER_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
43838
44112
 
43839
44113
  // src/bin.ts
43840
- import { readFileSync as readFileSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync5, rmSync as rmSync8, existsSync as existsSync7, realpathSync as realpathSync3, readdirSync as readdirSync4, statSync as statSync7 } from "node:fs";
43841
- import { join as join25, dirname as dirname12, isAbsolute as isAbsolute3, resolve as resolve3 } from "node:path";
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";
44115
+ import { join as join25, dirname as dirname13, isAbsolute as isAbsolute3, resolve as resolve3 } from "node:path";
43842
44116
  import { fileURLToPath as fileURLToPath4 } from "node:url";
43843
44117
  import { homedir as homedir5, userInfo } from "node:os";
43844
44118
  import { spawnSync } from "node:child_process";
44119
+ import { createServer as netCreateServer } from "node:net";
43845
44120
 
43846
44121
  // src/service/install.ts
43847
44122
  import { dirname as dirname8 } from "node:path";
@@ -43962,7 +44237,10 @@ ${programArgs}
43962
44237
  <key>RunAtLoad</key>
43963
44238
  <true/>
43964
44239
  <key>KeepAlive</key>
43965
- <true/>
44240
+ <dict>
44241
+ <key>SuccessfulExit</key>
44242
+ <false/>
44243
+ </dict>
43966
44244
  <key>StandardOutPath</key>
43967
44245
  <string>${escapeXml(cfg.logPath)}</string>
43968
44246
  <key>StandardErrorPath</key>
@@ -43989,7 +44267,7 @@ Wants=network-online.target
43989
44267
  [Service]
43990
44268
  Type=simple
43991
44269
  ExecStart=${execStart}
43992
- Restart=always
44270
+ Restart=on-failure
43993
44271
  RestartSec=5
43994
44272
 
43995
44273
  [Install]
@@ -44462,6 +44740,10 @@ ${customFacts(input)}`, wired: false, error: false };
44462
44740
  }
44463
44741
 
44464
44742
  // src/service/install-prep.ts
44743
+ function isDaemonRouted(h) {
44744
+ return h != null && h.state !== "session_superseded";
44745
+ }
44746
+ var VERIFY_BUDGET_MS = BOOT_DRAIN_WAIT_MS_DEFAULT + 15e3;
44465
44747
  function assertReadableCredential(absPath, deps) {
44466
44748
  let raw;
44467
44749
  try {
@@ -44489,6 +44771,44 @@ function durableDir(home, slug) {
44489
44771
  function durableCredentialPath(home, slug) {
44490
44772
  return join21(durableDir(home, slug), `${slug}.json`);
44491
44773
  }
44774
+ var DURABLE_PREFIX = "securecoms-agent-";
44775
+ var InstallRefusedError = class extends Error {
44776
+ };
44777
+ function existingIdentityError(dir, label) {
44778
+ return new InstallRefusedError(
44779
+ `This agent is already installed in ${dir} with a different key. Uninstall it first: agent-bridge uninstall --label ${label}`
44780
+ );
44781
+ }
44782
+ function refuseExistingIdentity(args2, deps) {
44783
+ const destName = `${DURABLE_PREFIX}${args2.slug}`;
44784
+ let names;
44785
+ try {
44786
+ names = deps.listDir(args2.home);
44787
+ } catch {
44788
+ names = [];
44789
+ }
44790
+ const others = names.filter((n) => n.startsWith(DURABLE_PREFIX) && n !== destName).sort();
44791
+ for (const name of [destName, ...others]) {
44792
+ const slug = name.slice(DURABLE_PREFIX.length);
44793
+ const dir = join21(args2.home, name);
44794
+ const credPath = join21(dir, `${slug}.json`);
44795
+ if (!deps.fileExists(credPath)) continue;
44796
+ let existing = null;
44797
+ try {
44798
+ const j = JSON.parse(deps.readFile(credPath));
44799
+ if (j && typeof j === "object") existing = j;
44800
+ } catch {
44801
+ }
44802
+ if (!existing || typeof existing.identity_id !== "string" || !existing.identity_id) {
44803
+ if (deps.fileExists(join21(dir, "bridge-state.db"))) throw existingIdentityError(dir, slug);
44804
+ continue;
44805
+ }
44806
+ if (existing.identity_id !== args2.parsed.identityId) continue;
44807
+ if (name !== destName || existing.device_id !== args2.parsed.deviceId) {
44808
+ throw existingIdentityError(dir, slug);
44809
+ }
44810
+ }
44811
+ }
44492
44812
  function takeCustody(args2, deps) {
44493
44813
  const dir = durableDir(args2.home, args2.slug);
44494
44814
  const dest = durableCredentialPath(args2.home, args2.slug);
@@ -44496,10 +44816,11 @@ function takeCustody(args2, deps) {
44496
44816
  if (deps.fileExists(dest) && deps.realpath(args2.sourceAbsPath) === deps.realpath(dest)) return dest;
44497
44817
  } catch {
44498
44818
  }
44819
+ refuseExistingIdentity(args2, deps);
44499
44820
  if (deps.fileExists(dest)) {
44500
44821
  try {
44501
44822
  const existing = JSON.parse(deps.readFile(dest));
44502
- if (existing.device_id && existing.device_id !== args2.parsed.deviceId) {
44823
+ if (existing.identity_id && existing.identity_id !== args2.parsed.identityId) {
44503
44824
  throw new Error(
44504
44825
  `install: ${dir} already belongs to a different agent (device ${existing.device_id}). Pass a distinct --label to install this agent in its own directory.`
44505
44826
  );
@@ -44554,21 +44875,19 @@ function selectPort(args2, deps) {
44554
44875
  throw new Error(`install: no free port in ${PORT_MIN}-${PORT_MAX} \u2014 stop an unused bridge or pass --port`);
44555
44876
  }
44556
44877
  async function verifyDaemonUp(args2, deps) {
44557
- const attempts = args2.attempts ?? 20;
44558
44878
  const delayMs = args2.delayMs ?? 250;
44559
- for (let i = 0; i < attempts; i++) {
44560
- if (deps.probeConnect(args2.port)) return true;
44561
- try {
44562
- if (deps.readFile(args2.logPath).includes("[bridge] listening on")) return true;
44563
- } catch {
44564
- }
44879
+ const deadline = deps.now() + VERIFY_BUDGET_MS;
44880
+ const maxAttempts = args2.attempts ?? Number.POSITIVE_INFINITY;
44881
+ for (let i = 0; i < maxAttempts; i++) {
44882
+ if (isDaemonRouted(deps.getHealth(args2.port))) return true;
44883
+ if (deps.now() >= deadline) return false;
44565
44884
  await deps.sleep(delayMs);
44566
44885
  }
44567
44886
  return false;
44568
44887
  }
44569
44888
  async function orchestrateInstall(ctx) {
44570
44889
  const parsed = assertReadableCredential(ctx.credentialAbsPath, ctx.prepDeps);
44571
- const stemForSlug = ctx.label ?? ctx.credentialAbsPath.replace(/^.*[\\/]/, "").replace(/\.[^.]*$/, "");
44890
+ const stemForSlug = ctx.label ?? ctx.credentialAbsPath.replace(/^.*[\\/]/, "").replace(/\.[^.]*$/, "").replace(/ \(\d+\)$/, "");
44572
44891
  const slug = slugify(stemForSlug);
44573
44892
  const durablePath = takeCustody(
44574
44893
  { home: ctx.home, slug, sourceAbsPath: ctx.credentialAbsPath, parsed },
@@ -44692,13 +45011,13 @@ function readFleet(deps) {
44692
45011
  // src/service/status.ts
44693
45012
  function readFleetStatus(deps) {
44694
45013
  return deps.readFleet().map((e) => {
44695
- const up = e.port != null && deps.probeConnect(e.port);
45014
+ const h = e.port != null ? deps.getHealth(e.port) : null;
45015
+ const up = isDaemonRouted(h);
44696
45016
  let harness = "-";
44697
45017
  let evictions = 0;
44698
- if (up && e.port != null) {
44699
- const h = deps.getHealth(e.port);
44700
- harness = (h?.agent_sessions ?? 0) >= 1 ? "attached" : "no brain";
44701
- evictions = h?.evictions ?? 0;
45018
+ if (up) {
45019
+ harness = (h.agent_sessions ?? 0) >= 1 ? "attached" : "no brain";
45020
+ evictions = h.evictions ?? 0;
44702
45021
  }
44703
45022
  return {
44704
45023
  slug: e.slug,
@@ -44790,20 +45109,339 @@ function removeConnectionFileIfOwned(home, identityId, instanceId, deps = REAL_F
44790
45109
  }
44791
45110
  }
44792
45111
 
45112
+ // src/service/instance-lock.ts
45113
+ 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";
45115
+ import { hostname, uptime as uptime2 } from "node:os";
45116
+ import { dirname as dirname11 } from "node:path";
45117
+ var LOCK_PARTIAL_WRITE_GRACE_MS = 1e4;
45118
+ var LOCK_TAKEOVER_GUARD_STALE_MS = 3e4;
45119
+ var BOOT_TOLERANCE_SEC = 120;
45120
+ var LOCK_HEARTBEAT_MS = 6e4;
45121
+ var LOCK_FOREIGN_STALE_MS = 10 * LOCK_HEARTBEAT_MS;
45122
+ function errno(error) {
45123
+ const code = error?.code;
45124
+ return typeof code === "string" ? code : void 0;
45125
+ }
45126
+ function readTrimmed(path) {
45127
+ try {
45128
+ const v = readFileSync13(path, "utf8").trim();
45129
+ return v === "" ? void 0 : v;
45130
+ } catch {
45131
+ return void 0;
45132
+ }
45133
+ }
45134
+ function procStartFromStat(stat) {
45135
+ const close = stat.lastIndexOf(")");
45136
+ if (close < 0) return void 0;
45137
+ const rest = stat.slice(close + 1).trim().split(/\s+/);
45138
+ const v = rest[19];
45139
+ return v !== void 0 && /^\d+$/.test(v) ? v : void 0;
45140
+ }
45141
+ function pidAliveByKill(pid) {
45142
+ try {
45143
+ process.kill(pid, 0);
45144
+ return true;
45145
+ } catch (error) {
45146
+ return errno(error) === "EPERM";
45147
+ }
45148
+ }
45149
+ var bootIdCache;
45150
+ function readBootId() {
45151
+ if (bootIdCache) return bootIdCache.v;
45152
+ let v = readTrimmed("/proc/sys/kernel/random/boot_id");
45153
+ if (v === void 0 && process.platform === "darwin") {
45154
+ try {
45155
+ const out = execFileSync("/usr/sbin/sysctl", ["-n", "kern.bootsessionuuid"], {
45156
+ encoding: "utf8",
45157
+ timeout: 2e3,
45158
+ stdio: ["ignore", "pipe", "ignore"]
45159
+ }).trim();
45160
+ v = /^[0-9A-Fa-f-]{16,64}$/.test(out) ? out : void 0;
45161
+ } catch {
45162
+ v = void 0;
45163
+ }
45164
+ }
45165
+ bootIdCache = { v };
45166
+ return v;
45167
+ }
45168
+ var realLockEnv = {
45169
+ pid: process.pid,
45170
+ hostname: () => hostname(),
45171
+ bootId: readBootId,
45172
+ bootEpochSec: () => Math.round(Date.now() / 1e3 - uptime2()),
45173
+ procStart: (pid) => {
45174
+ const stat = readTrimmed(`/proc/${pid}/stat`);
45175
+ return stat ? procStartFromStat(stat) : void 0;
45176
+ },
45177
+ pidAlive: pidAliveByKill
45178
+ };
45179
+ function parseBody(raw) {
45180
+ try {
45181
+ const b = JSON.parse(raw);
45182
+ if (b && b.v === 2 && Number.isInteger(b.pid) && b.pid > 0 && typeof b.host === "string" && Number.isFinite(b.boot)) {
45183
+ return b;
45184
+ }
45185
+ } catch {
45186
+ }
45187
+ return void 0;
45188
+ }
45189
+ var InstanceLock = class {
45190
+ constructor(path, env = realLockEnv, log = () => {
45191
+ }, heartbeatMs = LOCK_HEARTBEAT_MS) {
45192
+ this.path = path;
45193
+ this.env = env;
45194
+ this.log = log;
45195
+ this.heartbeatMs = heartbeatMs;
45196
+ }
45197
+ held = false;
45198
+ /** The exact body this process wrote (#808 N1): release and the ownership
45199
+ * check compare the file against THIS, never against live machine facts
45200
+ * (a hostname can change under a running process). */
45201
+ written;
45202
+ heartbeat;
45203
+ /** The error behind the last `unreadable` / `failed`, for the caller's one
45204
+ * log line (the lock itself never logs a refusal: it is retried per call). */
45205
+ lastError;
45206
+ /** PID read from the lock body of the live holder behind the last `held`. */
45207
+ holderPid;
45208
+ body() {
45209
+ const bootId = this.env.bootId();
45210
+ const start = this.env.procStart(this.env.pid);
45211
+ const b = {
45212
+ v: 2,
45213
+ pid: this.env.pid,
45214
+ host: this.env.hostname(),
45215
+ ...bootId !== void 0 ? { boot_id: bootId } : {},
45216
+ boot: this.env.bootEpochSec(),
45217
+ ...start !== void 0 ? { start } : {}
45218
+ };
45219
+ return JSON.stringify(b);
45220
+ }
45221
+ /** Try to hold the lock. Never throws (#808 L-e: machine reads included). */
45222
+ acquire() {
45223
+ this.holderPid = void 0;
45224
+ let result;
45225
+ try {
45226
+ result = this.acquireUnguarded();
45227
+ } catch (error) {
45228
+ this.lastError = error;
45229
+ result = "failed";
45230
+ }
45231
+ return result === "held" && this.holderPid !== void 0 ? { result, holderPid: this.holderPid } : { result };
45232
+ }
45233
+ /**
45234
+ * True while the file is still exactly the body this process wrote. Read
45235
+ * before every reserve (one small read), so a holder whose lock was taken
45236
+ * while it was frozen stops before it spends (#808 N1). A lost lock is
45237
+ * dropped: the caller then refuses, and later calls try to re-acquire.
45238
+ */
45239
+ stillHeld() {
45240
+ if (!this.held) return false;
45241
+ let current;
45242
+ try {
45243
+ current = readFileSync13(this.path, "utf8");
45244
+ } catch {
45245
+ current = void 0;
45246
+ }
45247
+ if (current !== void 0 && current === this.written) return true;
45248
+ this.drop();
45249
+ this.log("warn", "bridge.instance_lock_lost", {});
45250
+ return false;
45251
+ }
45252
+ acquireUnguarded() {
45253
+ if (this.held) return "acquired";
45254
+ const created = this.create();
45255
+ if (created === "created") return this.took();
45256
+ if (created === "failed") return "failed";
45257
+ let raw;
45258
+ let ageMs;
45259
+ this.env.beforeLockRead?.();
45260
+ try {
45261
+ raw = readFileSync13(this.path, "utf8");
45262
+ ageMs = Date.now() - statSync6(this.path).mtimeMs;
45263
+ } catch (error) {
45264
+ if (errno(error) === "ENOENT") {
45265
+ const again = this.create();
45266
+ return again === "created" ? this.took() : again === "failed" ? "failed" : "busy";
45267
+ }
45268
+ this.lastError = error;
45269
+ return "unreadable";
45270
+ }
45271
+ const b = parseBody(raw);
45272
+ if (!b) {
45273
+ if (ageMs < LOCK_PARTIAL_WRITE_GRACE_MS) return "busy";
45274
+ return this.takeOver(raw, void 0);
45275
+ }
45276
+ if (b.host !== this.env.hostname()) {
45277
+ const bootId = this.env.bootId();
45278
+ const sameKernel = b.boot_id !== void 0 && bootId !== void 0 && b.boot_id === bootId;
45279
+ if (sameKernel && ageMs > LOCK_FOREIGN_STALE_MS) return this.takeOver(raw, b.pid);
45280
+ return "foreign_host";
45281
+ }
45282
+ if (!this.sameBoot(b)) return this.takeOver(raw, b.pid);
45283
+ const liveStart = this.env.procStart(b.pid);
45284
+ const reused = b.start !== void 0 && liveStart !== void 0 && liveStart !== b.start;
45285
+ if (b.pid === this.env.pid) {
45286
+ if (!reused) return this.took();
45287
+ return this.takeOver(raw, b.pid);
45288
+ }
45289
+ if (this.env.pidAlive(b.pid) && !reused) {
45290
+ this.holderPid = b.pid;
45291
+ return "held";
45292
+ }
45293
+ return this.takeOver(raw, b.pid);
45294
+ }
45295
+ /** Remove the lock only while it is still exactly the body this process
45296
+ * wrote (#808 N1: not the live hostname, which can change under us). */
45297
+ release() {
45298
+ if (!this.held) return;
45299
+ const written = this.written;
45300
+ this.drop();
45301
+ try {
45302
+ if (written !== void 0 && readFileSync13(this.path, "utf8") === written) unlinkSync3(this.path);
45303
+ } catch {
45304
+ }
45305
+ }
45306
+ drop() {
45307
+ this.held = false;
45308
+ if (this.heartbeat) clearInterval(this.heartbeat);
45309
+ this.heartbeat = void 0;
45310
+ }
45311
+ /** We hold it: remember what we wrote (the file as it is now, ours) and
45312
+ * start the heartbeat. */
45313
+ took() {
45314
+ this.held = true;
45315
+ try {
45316
+ this.written = readFileSync13(this.path, "utf8");
45317
+ } catch {
45318
+ this.written = void 0;
45319
+ }
45320
+ if (!this.heartbeat && this.heartbeatMs > 0) {
45321
+ this.heartbeat = setInterval(() => this.beat(), this.heartbeatMs);
45322
+ this.heartbeat.unref?.();
45323
+ }
45324
+ return "acquired";
45325
+ }
45326
+ /** Refresh the mtime while the file is still ours; stop if it is not. */
45327
+ beat() {
45328
+ if (!this.stillHeld()) return;
45329
+ try {
45330
+ const now = /* @__PURE__ */ new Date();
45331
+ utimesSync(this.path, now, now);
45332
+ } catch {
45333
+ }
45334
+ }
45335
+ sameBoot(b) {
45336
+ const bootId = this.env.bootId();
45337
+ if (b.boot_id !== void 0 && bootId !== void 0) return b.boot_id === bootId;
45338
+ return Math.abs(b.boot - this.env.bootEpochSec()) <= BOOT_TOLERANCE_SEC;
45339
+ }
45340
+ /** O_EXCL create with our body. A write failure removes the half-written
45341
+ * file (it is ours: we created it) and reports `failed`, never throws. */
45342
+ create() {
45343
+ let fd;
45344
+ try {
45345
+ mkdirSync7(dirname11(this.path), { recursive: true, mode: 448 });
45346
+ fd = openSync4(this.path, "wx", 384);
45347
+ } catch (error) {
45348
+ if (errno(error) === "EEXIST") return "exists";
45349
+ this.lastError = error;
45350
+ return "failed";
45351
+ }
45352
+ try {
45353
+ writeSync2(fd, this.body());
45354
+ fsyncSync(fd);
45355
+ } catch (error) {
45356
+ try {
45357
+ closeSync4(fd);
45358
+ } catch {
45359
+ }
45360
+ try {
45361
+ unlinkSync3(this.path);
45362
+ } catch {
45363
+ }
45364
+ this.lastError = error;
45365
+ return "failed";
45366
+ }
45367
+ closeSync4(fd);
45368
+ return "created";
45369
+ }
45370
+ /**
45371
+ * Replace a stale lock. Only the holder of the O_EXCL takeover guard may
45372
+ * remove a lock, and only after re-reading it UNCHANGED, so two runtimes
45373
+ * that both saw the same stale lock cannot each delete the other's new one.
45374
+ * A fresh guard means a takeover is in progress: busy this time.
45375
+ */
45376
+ takeOver(staleRaw, stalePid, retryGuard = true) {
45377
+ const guard = `${this.path}.takeover`;
45378
+ let fd;
45379
+ try {
45380
+ fd = openSync4(guard, "wx", 384);
45381
+ } catch (error) {
45382
+ if (errno(error) !== "EEXIST") return "failed";
45383
+ let crashed = false;
45384
+ try {
45385
+ crashed = Date.now() - statSync6(guard).mtimeMs > LOCK_TAKEOVER_GUARD_STALE_MS;
45386
+ if (crashed) unlinkSync3(guard);
45387
+ } catch {
45388
+ crashed = false;
45389
+ }
45390
+ if (crashed && retryGuard) return this.takeOver(staleRaw, stalePid, false);
45391
+ return "busy";
45392
+ }
45393
+ closeSync4(fd);
45394
+ try {
45395
+ this.env.beforeTakeoverReread?.();
45396
+ let current;
45397
+ try {
45398
+ current = readFileSync13(this.path, "utf8");
45399
+ } catch {
45400
+ current = void 0;
45401
+ }
45402
+ if (current !== void 0 && current !== staleRaw) {
45403
+ this.holderPid = parseBody(current)?.pid;
45404
+ return "held";
45405
+ }
45406
+ if (current !== void 0) unlinkSync3(this.path);
45407
+ this.env.beforeTakeoverCreate?.();
45408
+ const created = this.create();
45409
+ if (created !== "created") {
45410
+ if (created === "failed") return "failed";
45411
+ try {
45412
+ this.holderPid = parseBody(readFileSync13(this.path, "utf8"))?.pid;
45413
+ } catch {
45414
+ }
45415
+ return "held";
45416
+ }
45417
+ this.log("warn", "bridge.instance_lock_stale_taken_over", stalePid !== void 0 ? { stale_pid: stalePid } : {});
45418
+ return this.took();
45419
+ } catch (error) {
45420
+ this.lastError = error;
45421
+ return "failed";
45422
+ } finally {
45423
+ try {
45424
+ unlinkSync3(guard);
45425
+ } catch {
45426
+ }
45427
+ }
45428
+ }
45429
+ };
45430
+
44793
45431
  // src/grants/pin-command.ts
44794
45432
  import {
44795
- closeSync as closeSync4,
45433
+ closeSync as closeSync5,
44796
45434
  fchmodSync,
44797
- fsyncSync,
45435
+ fsyncSync as fsyncSync2,
44798
45436
  lstatSync as lstatSync8,
44799
- openSync as openSync4,
44800
- readFileSync as readFileSync13,
45437
+ openSync as openSync5,
45438
+ readFileSync as readFileSync14,
44801
45439
  realpathSync as realpathSync2,
44802
45440
  renameSync as renameSync5,
44803
- unlinkSync as unlinkSync3,
44804
- writeSync as writeSync2
45441
+ unlinkSync as unlinkSync4,
45442
+ writeSync as writeSync3
44805
45443
  } from "node:fs";
44806
- import { dirname as dirname11, join as join23, resolve as resolve2 } from "node:path";
45444
+ import { dirname as dirname12, join as join23, resolve as resolve2 } from "node:path";
44807
45445
  var PIN_COPY = Object.freeze({
44808
45446
  saved: "pin saved. Restart the Connector to use it.",
44809
45447
  // [COPY, approved by Eric 2026-10-12]
@@ -44834,27 +45472,27 @@ function isSymlink(path) {
44834
45472
  }
44835
45473
  }
44836
45474
  function readJson(path) {
44837
- return JSON.parse(readFileSync13(path, "utf8"));
45475
+ return JSON.parse(readFileSync14(path, "utf8"));
44838
45476
  }
44839
45477
  function publicKeyOf(v) {
44840
45478
  const r = parsePinObject(v);
44841
45479
  return r.ok ? r.pin.publicKeyB64 : null;
44842
45480
  }
44843
45481
  function writeTemp(path, bytes) {
44844
- const fd = openSync4(path, "wx", 384);
45482
+ const fd = openSync5(path, "wx", 384);
44845
45483
  try {
44846
45484
  fchmodSync(fd, 384);
44847
- writeSync2(fd, bytes);
44848
- fsyncSync(fd);
45485
+ writeSync3(fd, bytes);
45486
+ fsyncSync2(fd);
44849
45487
  } finally {
44850
- closeSync4(fd);
45488
+ closeSync5(fd);
44851
45489
  }
44852
45490
  }
44853
45491
  function runPinCommand(opts) {
44854
- const credDir = dirname11(resolve2(opts.credentialPath));
45492
+ const credDir = dirname12(resolve2(opts.credentialPath));
44855
45493
  const target = join23(credDir, PIN_FILE_NAME);
44856
45494
  try {
44857
- if (realpathSync2(dirname11(resolve2(opts.pinSourcePath))) !== realpathSync2(credDir)) return refuse("directory_mismatch");
45495
+ if (realpathSync2(dirname12(resolve2(opts.pinSourcePath))) !== realpathSync2(credDir)) return refuse("directory_mismatch");
44858
45496
  } catch {
44859
45497
  return refuse("io_error");
44860
45498
  }
@@ -44901,7 +45539,7 @@ function runPinCommand(opts) {
44901
45539
  const fail = () => {
44902
45540
  for (const p of created) {
44903
45541
  try {
44904
- unlinkSync3(p);
45542
+ unlinkSync4(p);
44905
45543
  } catch {
44906
45544
  }
44907
45545
  }
@@ -44937,11 +45575,11 @@ function runPinCommand(opts) {
44937
45575
  created.splice(created.indexOf(credTmp), 1);
44938
45576
  step("cred_renamed");
44939
45577
  try {
44940
- const dfd = openSync4(credDir, "r");
45578
+ const dfd = openSync5(credDir, "r");
44941
45579
  try {
44942
- fsyncSync(dfd);
45580
+ fsyncSync2(dfd);
44943
45581
  } finally {
44944
- closeSync4(dfd);
45582
+ closeSync5(dfd);
44945
45583
  }
44946
45584
  } catch {
44947
45585
  }
@@ -44997,7 +45635,7 @@ function parseVerb(argv) {
44997
45635
  return v === "install" || v === "uninstall" || v === "status" || v === "wire" || v === "unwire" || v === "models" || v === "pin" ? v : null;
44998
45636
  }
44999
45637
  function slugFromCredentialPath(credentialPath) {
45000
- const dir = dirname12(credentialPath);
45638
+ const dir = dirname13(credentialPath);
45001
45639
  const base = dir.split(/[\\/]/).pop() ?? "";
45002
45640
  const m = base.match(/^securecoms-agent-(.+)$/);
45003
45641
  if (m) return m[1];
@@ -45013,7 +45651,7 @@ function realServiceDeps() {
45013
45651
  },
45014
45652
  writeFile: (p, data) => writeFileSync5(p, data, { mode: 420 }),
45015
45653
  removeFile: (p) => rmSync8(p, { force: true }),
45016
- mkdirp: (p) => mkdirSync7(p, { recursive: true }),
45654
+ mkdirp: (p) => mkdirSync8(p, { recursive: true }),
45017
45655
  fileExists: (p) => existsSync7(p),
45018
45656
  user: userInfo().username
45019
45657
  };
@@ -45057,10 +45695,10 @@ function realResolveDeps() {
45057
45695
  }
45058
45696
  function realPrepDeps() {
45059
45697
  return {
45060
- readFile: (p) => readFileSync15(p, "utf8"),
45698
+ readFile: (p) => readFileSync16(p, "utf8"),
45061
45699
  writeFileMode: (p, data, mode) => writeFileSync5(p, data, { mode }),
45062
45700
  mkdirp: (p, mode) => {
45063
- mkdirSync7(p, { recursive: true, mode: mode ?? 493 });
45701
+ mkdirSync8(p, { recursive: true, mode: mode ?? 493 });
45064
45702
  },
45065
45703
  fileExists: (p) => existsSync7(p),
45066
45704
  realpath: (p) => realpathSync3(p),
@@ -45080,25 +45718,22 @@ function realPrepDeps() {
45080
45718
  ]);
45081
45719
  return r.status === 0;
45082
45720
  },
45083
- probeConnect: (port) => {
45084
- const r = spawnSync(process.execPath, [
45085
- "-e",
45086
- `const s=require('net').connect(${port},'127.0.0.1');s.once('connect',()=>{s.destroy();process.exit(0)});s.once('error',()=>process.exit(1));setTimeout(()=>process.exit(1),500);`
45087
- ]);
45088
- return r.status === 0;
45089
- },
45721
+ getHealth: realGetHealth,
45090
45722
  // No unref: an unref'd timer would let the process exit mid-verification.
45091
- sleep: (ms) => new Promise((res) => setTimeout(res, ms))
45723
+ sleep: (ms) => new Promise((res) => setTimeout(res, ms)),
45724
+ now: () => Date.now()
45092
45725
  };
45093
45726
  }
45094
45727
  function realGetHealth(port) {
45095
45728
  const r = spawnSync(process.execPath, [
45096
45729
  "-e",
45097
- `const http=require('http');const q=http.get({host:'127.0.0.1',port:${port},path:'/healthz',timeout:800},res=>{let d='';res.on('data',c=>d+=c);res.on('end',()=>{process.stdout.write(d);process.exit(0)})});q.on('error',()=>process.exit(1));q.on('timeout',()=>process.exit(1));`
45098
- ], { encoding: "utf8" });
45730
+ `const http=require('http');const q=http.get({host:'127.0.0.1',port:${port},path:'/healthz',timeout:800},res=>{let d='';res.on('data',c=>d+=c);res.on('end',()=>{process.stdout.write(res.statusCode+'\\n'+d);process.exit(0)})});q.on('error',()=>process.exit(1));q.on('timeout',()=>process.exit(1));`
45731
+ ], { encoding: "utf8", timeout: 2e3 });
45099
45732
  if (r.status !== 0) return null;
45733
+ const nl = r.stdout.indexOf("\n");
45734
+ if (nl < 0 || r.stdout.slice(0, nl) !== "200") return null;
45100
45735
  try {
45101
- return JSON.parse(r.stdout);
45736
+ return JSON.parse(r.stdout.slice(nl + 1));
45102
45737
  } catch {
45103
45738
  return null;
45104
45739
  }
@@ -45113,19 +45748,12 @@ function realReadFleet() {
45113
45748
  return [];
45114
45749
  }
45115
45750
  },
45116
- readFile: (p) => readFileSync15(p, "utf8")
45751
+ readFile: (p) => readFileSync16(p, "utf8")
45117
45752
  });
45118
45753
  }
45119
45754
  function runStatus() {
45120
45755
  const deps = {
45121
45756
  readFleet: realReadFleet,
45122
- probeConnect: (port) => {
45123
- const r = spawnSync(process.execPath, [
45124
- "-e",
45125
- `const s=require('net').connect(${port},'127.0.0.1');s.once('connect',()=>{s.destroy();process.exit(0)});s.once('error',()=>process.exit(1));setTimeout(()=>process.exit(1),500);`
45126
- ]);
45127
- return r.status === 0;
45128
- },
45129
45757
  getHealth: realGetHealth
45130
45758
  };
45131
45759
  const rows = readFleetStatus(deps);
@@ -45135,26 +45763,26 @@ function runStatus() {
45135
45763
  }
45136
45764
  function realCopyConnector(srcDir, dstDir, harness) {
45137
45765
  const allowedExt = CONNECTOR_FILE_EXTENSIONS[harness];
45138
- mkdirSync7(dstDir, { recursive: true });
45766
+ mkdirSync8(dstDir, { recursive: true });
45139
45767
  for (const name of readdirSync4(srcDir)) {
45140
45768
  const srcPath = join25(srcDir, name);
45141
- if (statSync7(srcPath).isDirectory()) {
45769
+ if (statSync8(srcPath).isDirectory()) {
45142
45770
  if (CONNECTOR_SKIP_DIRS.has(name)) continue;
45143
45771
  realCopyConnector(srcPath, join25(dstDir, name), harness);
45144
45772
  continue;
45145
45773
  }
45146
45774
  if (name.includes(".test.")) continue;
45147
45775
  if (!allowedExt.some((ext) => name.endsWith(ext))) continue;
45148
- writeFileSync5(join25(dstDir, name), readFileSync15(srcPath));
45776
+ writeFileSync5(join25(dstDir, name), readFileSync16(srcPath));
45149
45777
  }
45150
45778
  }
45151
45779
  function realWriteBinding(dstDir, identityId) {
45152
- mkdirSync7(dstDir, { recursive: true });
45780
+ mkdirSync8(dstDir, { recursive: true });
45153
45781
  writeFileSync5(join25(dstDir, "securecoms.binding.json"), JSON.stringify({ identity_id: identityId }) + "\n", { mode: 384 });
45154
45782
  }
45155
45783
  function realReadBinding(dstDir) {
45156
45784
  try {
45157
- const parsed = JSON.parse(readFileSync15(join25(dstDir, "securecoms.binding.json"), "utf8"));
45785
+ const parsed = JSON.parse(readFileSync16(join25(dstDir, "securecoms.binding.json"), "utf8"));
45158
45786
  return parsed.identity_id ?? null;
45159
45787
  } catch {
45160
45788
  return null;
@@ -45165,14 +45793,14 @@ function realRemoveBinding(dstDir) {
45165
45793
  }
45166
45794
  function realStatDir(p) {
45167
45795
  try {
45168
- return statSync7(p).isDirectory();
45796
+ return statSync8(p).isDirectory();
45169
45797
  } catch {
45170
45798
  return false;
45171
45799
  }
45172
45800
  }
45173
45801
  function realReadHermesConfig(p) {
45174
45802
  try {
45175
- return readFileSync15(p, "utf8");
45803
+ return readFileSync16(p, "utf8");
45176
45804
  } catch {
45177
45805
  return null;
45178
45806
  }
@@ -45293,12 +45921,15 @@ async function runServiceVerb(verb) {
45293
45921
  }
45294
45922
  process.exit(0);
45295
45923
  } catch (e) {
45296
- const raw = e instanceof ServiceUnsupportedError || e instanceof GlobalInstallPermissionError;
45297
- process.stderr.write(`${raw ? e.message : `service ${verb} failed: ${e.message ?? e}`}
45298
- `);
45924
+ process.stderr.write(formatServiceError(verb, e));
45299
45925
  process.exit(1);
45300
45926
  }
45301
45927
  }
45928
+ function formatServiceError(verb, e) {
45929
+ const raw = e instanceof ServiceUnsupportedError || e instanceof GlobalInstallPermissionError || e instanceof InstallRefusedError;
45930
+ return `${raw ? e.message : `service ${verb} failed: ${e.message ?? e}`}
45931
+ `;
45932
+ }
45302
45933
  async function startThenServe(agent, opts) {
45303
45934
  await agent.start();
45304
45935
  const tls = opts.readTls();
@@ -45330,6 +45961,216 @@ async function runModelsVerb() {
45330
45961
  function runPinVerb() {
45331
45962
  process.exit(runPinCli(process.argv, { out: (l) => process.stdout.write(l + "\n"), err: (l) => process.stderr.write(l + "\n") }));
45332
45963
  }
45964
+ var CANONICAL_UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
45965
+ var HELD_POLL_TOTAL_MS = 1e4;
45966
+ var HELD_POLL_INTERVAL_MS = 500;
45967
+ var HELD_NO_PID_REACQUIRES = 3;
45968
+ var SHUTDOWN_HARD_EXIT_MS = 8e3;
45969
+ function realRunModeDeps(credentialPath, storePath) {
45970
+ return {
45971
+ credentialPath,
45972
+ storePath,
45973
+ argv: process.argv,
45974
+ env: process.env,
45975
+ home: homedir5(),
45976
+ newLock: (path) => new InstanceLock(path, void 0, writeServiceLog),
45977
+ // 7b Task 19 fix M2: the production construction owns the machine paths (heartbeat, model marker and GC).
45978
+ newAgent: (cfg) => new BridgeAgent(cfg, { hostMachine: true }),
45979
+ net: { createServer: netCreateServer },
45980
+ serve: startThenServe,
45981
+ now: () => Date.now(),
45982
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
45983
+ setHardExitTimer: (cb, ms) => {
45984
+ const t = setTimeout(cb, ms);
45985
+ t.unref?.();
45986
+ return () => clearTimeout(t);
45987
+ },
45988
+ onSignal: (signal, handler) => {
45989
+ process.on(signal, handler);
45990
+ },
45991
+ onExit: (handler) => {
45992
+ process.once("exit", handler);
45993
+ },
45994
+ exit: (code) => process.exit(code),
45995
+ print: (line) => {
45996
+ process.stderr.write(`${line}
45997
+ `);
45998
+ },
45999
+ log: writeServiceLog
46000
+ };
46001
+ }
46002
+ async function runMode(deps) {
46003
+ const { credentialPath, storePath, log } = deps;
46004
+ let cfg, daemon;
46005
+ try {
46006
+ cfg = loadConfig({ credentialPath, storePath });
46007
+ daemon = parseDaemonArgs(deps.argv, deps.env);
46008
+ validateBind(daemon.bind, daemon.tls);
46009
+ } catch (e) {
46010
+ log("error", "startup.config_invalid", {}, e);
46011
+ return deps.exit(1);
46012
+ }
46013
+ if (!CANONICAL_UUID2.test(cfg.identityId)) {
46014
+ log("error", "startup.config_invalid", { field: ServiceLogEnum.of("identity_id", ["identity_id"]) });
46015
+ return deps.exit(1);
46016
+ }
46017
+ const lockDir = join25(deps.home, ".securecoms", "locks");
46018
+ const lockPath = join25(lockDir, `${cfg.identityId.toLowerCase()}.lock`);
46019
+ try {
46020
+ mkdirSync8(lockDir, { recursive: true, mode: 448 });
46021
+ chmodSync6(lockDir, 448);
46022
+ } catch (e) {
46023
+ log("error", "startup.instance_lock_failed", {}, e);
46024
+ return deps.exit(1);
46025
+ }
46026
+ const lock = deps.newLock(lockPath);
46027
+ let got = lock.acquire();
46028
+ const pollStart = deps.now();
46029
+ while (got.result === "held" && deps.now() - pollStart < HELD_POLL_TOTAL_MS) {
46030
+ await deps.sleep(HELD_POLL_INTERVAL_MS);
46031
+ got = lock.acquire();
46032
+ }
46033
+ for (let i = 0; i < HELD_NO_PID_REACQUIRES && got.result === "held" && got.holderPid === void 0; i++) {
46034
+ await deps.sleep(HELD_POLL_INTERVAL_MS);
46035
+ got = lock.acquire();
46036
+ }
46037
+ switch (got.result) {
46038
+ case "acquired":
46039
+ break;
46040
+ case "held":
46041
+ if (got.holderPid === void 0) {
46042
+ log("error", "startup.instance_lock_busy");
46043
+ return deps.exit(1);
46044
+ }
46045
+ log("info", "startup.already_running");
46046
+ deps.print(`This agent is already running on this computer (process ${got.holderPid}). Stop that copy first, or run: agent-bridge status`);
46047
+ return deps.exit(0);
46048
+ case "foreign_host":
46049
+ log("error", "startup.instance_lock_foreign_host", {
46050
+ lock_path: ServiceLogEnum.of("home_securecoms_locks_identity", ["home_securecoms_locks_identity"]),
46051
+ remedy: ServiceLogEnum.of("delete_lock_if_no_other_host", ["delete_lock_if_no_other_host"])
46052
+ });
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.`);
46054
+ return deps.exit(1);
46055
+ case "unreadable":
46056
+ log("error", "startup.instance_lock_unreadable", {}, lock.lastError);
46057
+ return deps.exit(1);
46058
+ case "busy":
46059
+ log("error", "startup.instance_lock_busy");
46060
+ return deps.exit(1);
46061
+ case "failed":
46062
+ log("error", "startup.instance_lock_failed", {}, lock.lastError);
46063
+ return deps.exit(1);
46064
+ }
46065
+ deps.onExit(() => lock.release());
46066
+ const tokenFile = daemon.tokenFile ?? join25(dirname13(credentialPath), "bridge-token");
46067
+ const token = resolveToken({ tokenFile, token: daemon.token });
46068
+ const preBound = deps.net.createServer((sock) => sock.destroy());
46069
+ const bindError = await new Promise((resolveBind) => {
46070
+ const onError = (e) => resolveBind(e);
46071
+ preBound.once("error", onError);
46072
+ preBound.listen(daemon.port, daemon.bind, () => {
46073
+ preBound.removeListener("error", onError);
46074
+ resolveBind(void 0);
46075
+ });
46076
+ });
46077
+ if (bindError) {
46078
+ if (bindError.code === "EADDRINUSE") {
46079
+ log("error", "startup.port_in_use", { port: daemon.port });
46080
+ deps.print(`Port ${daemon.port} is already in use by another program on this computer, so the agent did not start.`);
46081
+ } else {
46082
+ log("error", "startup.listen_failed", {}, bindError);
46083
+ }
46084
+ lock.release();
46085
+ return deps.exit(1);
46086
+ }
46087
+ const agent = deps.newAgent(cfg);
46088
+ const instanceId = newInstanceId();
46089
+ const identityId = cfg.identityId;
46090
+ let served = null;
46091
+ let superseded = false;
46092
+ const stopIngress = (s) => {
46093
+ s.agentWs.enterSessionSuperseded();
46094
+ s.closeAllSseStreams();
46095
+ };
46096
+ agent.on("session_superseded", () => {
46097
+ if (superseded) return;
46098
+ superseded = true;
46099
+ if (served) stopIngress(served);
46100
+ log("warn", "transport.session_superseded");
46101
+ deps.print("This agent connected from another place, so this copy stopped. Stop the other copy, then restart this one.");
46102
+ void (async () => {
46103
+ try {
46104
+ await agent.close();
46105
+ } catch (e) {
46106
+ log("error", "transport.session_superseded_close_failed", {}, e);
46107
+ }
46108
+ removeConnectionFileIfOwned(deps.home, identityId, instanceId);
46109
+ })();
46110
+ });
46111
+ let exiting = false;
46112
+ const closeReleaseExit = () => {
46113
+ if (exiting) return;
46114
+ exiting = true;
46115
+ const cancelHard = deps.setHardExitTimer(() => {
46116
+ lock.release();
46117
+ deps.exit(0);
46118
+ }, SHUTDOWN_HARD_EXIT_MS);
46119
+ void agent.close().finally(() => {
46120
+ cancelHard();
46121
+ lock.release();
46122
+ deps.exit(0);
46123
+ });
46124
+ };
46125
+ try {
46126
+ served = await deps.serve(agent, {
46127
+ token,
46128
+ allowOrigins: daemon.allowOrigins,
46129
+ readTls: () => daemon.tls ? { cert: readFileSync16(daemon.tlsCert, "utf8"), key: readFileSync16(daemon.tlsKey, "utf8") } : void 0
46130
+ });
46131
+ } catch (e) {
46132
+ if (!(e instanceof SessionSupersededError)) throw e;
46133
+ deps.onSignal("SIGINT", closeReleaseExit);
46134
+ deps.onSignal("SIGTERM", closeReleaseExit);
46135
+ return;
46136
+ }
46137
+ if (superseded) stopIngress(served);
46138
+ const { server, closeAllSseStreams, agentWs } = served;
46139
+ let down = false;
46140
+ const shutdown = () => {
46141
+ if (down) return;
46142
+ down = true;
46143
+ agentWs.close();
46144
+ server.close();
46145
+ closeAllSseStreams();
46146
+ closeReleaseExit();
46147
+ };
46148
+ deps.onSignal("SIGINT", shutdown);
46149
+ deps.onSignal("SIGTERM", shutdown);
46150
+ const slug = slugFromCredentialPath(credentialPath);
46151
+ deps.onExit(() => removeConnectionFileIfOwned(deps.home, identityId, instanceId));
46152
+ server.listen(preBound, () => {
46153
+ log("info", "startup.listening", { port: daemon.port, tls: !!daemon.tls });
46154
+ log("info", "startup.token_loaded");
46155
+ const addr = server.address();
46156
+ const boundPort = typeof addr === "object" && addr ? addr.port : daemon.port;
46157
+ if (superseded) return;
46158
+ try {
46159
+ writeConnectionFile(deps.home, buildConnectionFile({
46160
+ identityId,
46161
+ port: boundPort,
46162
+ tokenFile,
46163
+ slug,
46164
+ label: slug,
46165
+ instanceId,
46166
+ now: (/* @__PURE__ */ new Date()).toISOString()
46167
+ }));
46168
+ log("info", "startup.connection_file_written");
46169
+ } catch (e) {
46170
+ log("warn", "startup.connection_file_write_failed", {}, e);
46171
+ }
46172
+ });
46173
+ }
45333
46174
  async function main() {
45334
46175
  if (!nodeMajorOk(process.versions.node)) {
45335
46176
  writeServiceLog("error", "startup.node_unsupported");
@@ -45362,63 +46203,7 @@ async function main() {
45362
46203
  process.exit(2);
45363
46204
  }
45364
46205
  const storePath = argOrEnv("--store", "SECURECOMS_STORE");
45365
- let cfg, daemon;
45366
- try {
45367
- cfg = loadConfig({ credentialPath, storePath });
45368
- daemon = parseDaemonArgs(process.argv, process.env);
45369
- validateBind(daemon.bind, daemon.tls);
45370
- } catch (e) {
45371
- writeServiceLog("error", "startup.config_invalid", {}, e);
45372
- process.exit(1);
45373
- }
45374
- const tokenFile = daemon.tokenFile ?? join25(dirname12(credentialPath), "bridge-token");
45375
- const token = resolveToken({ tokenFile, token: daemon.token });
45376
- const agent = new BridgeAgent(cfg, { hostMachine: true });
45377
- const { server, closeAllSseStreams, agentWs } = await startThenServe(agent, {
45378
- token,
45379
- allowOrigins: daemon.allowOrigins,
45380
- readTls: () => daemon.tls ? { cert: readFileSync15(daemon.tlsCert, "utf8"), key: readFileSync15(daemon.tlsKey, "utf8") } : void 0
45381
- });
45382
- let down = false;
45383
- const shutdown = () => {
45384
- if (down) return;
45385
- down = true;
45386
- agentWs.close();
45387
- server.close();
45388
- closeAllSseStreams();
45389
- const hard = setTimeout(() => process.exit(0), 8e3);
45390
- hard.unref?.();
45391
- void agent.close().finally(() => {
45392
- clearTimeout(hard);
45393
- process.exit(0);
45394
- });
45395
- };
45396
- process.on("SIGINT", shutdown);
45397
- process.on("SIGTERM", shutdown);
45398
- const instanceId = newInstanceId();
45399
- const identityId = agent.health().identity_id;
45400
- const slug = slugFromCredentialPath(credentialPath);
45401
- process.once("exit", () => removeConnectionFileIfOwned(homedir5(), identityId, instanceId));
45402
- server.listen(daemon.port, daemon.bind, () => {
45403
- writeServiceLog("info", "startup.listening", { port: daemon.port, tls: !!daemon.tls });
45404
- writeServiceLog("info", "startup.token_loaded");
45405
- const addr = server.address();
45406
- const boundPort = typeof addr === "object" && addr ? addr.port : daemon.port;
45407
- try {
45408
- writeConnectionFile(homedir5(), buildConnectionFile({
45409
- identityId,
45410
- port: boundPort,
45411
- tokenFile,
45412
- slug,
45413
- label: slug,
45414
- instanceId,
45415
- now: (/* @__PURE__ */ new Date()).toISOString()
45416
- }));
45417
- writeServiceLog("info", "startup.connection_file_written");
45418
- } catch (e) {
45419
- writeServiceLog("warn", "startup.connection_file_write_failed", {}, e);
45420
- }
45421
- });
46206
+ await runMode(realRunModeDeps(credentialPath, storePath));
45422
46207
  }
45423
46208
  var here = fileURLToPath4(import.meta.url);
45424
46209
  var isMain = !!process.argv[1] && refersToSameFile(here, process.argv[1], realpathSync3);
@@ -45429,10 +46214,17 @@ if (isMain) {
45429
46214
  });
45430
46215
  }
45431
46216
  export {
46217
+ HELD_NO_PID_REACQUIRES,
46218
+ HELD_POLL_INTERVAL_MS,
46219
+ HELD_POLL_TOTAL_MS,
46220
+ SHUTDOWN_HARD_EXIT_MS,
45432
46221
  chooseInstallExecPath,
46222
+ formatServiceError,
45433
46223
  nodeMajorOk,
45434
46224
  parseVerb,
45435
46225
  realCopyConnector,
46226
+ realGetHealth,
46227
+ runMode,
45436
46228
  slugFromCredentialPath,
45437
46229
  startThenServe,
45438
46230
  tokenFingerprint