@securecoms/agent-runtime 0.13.0 → 0.13.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.
Files changed (3) hide show
  1. package/README.md +21 -0
  2. package/dist/bin.js +97 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -527,6 +527,27 @@ The paragraph is therefore an advisory configuration summary, not an authority
527
527
  claim. Channel authorization and the local gate are both enforced at call
528
528
  time, so a configured but ungranted or denied tool is still refused.
529
529
 
530
+ ## Release notes: 0.13.1
531
+
532
+ <!-- [COPY, approved by Eric 2026-10-15] -->
533
+
534
+ 0.13.1 prints a plain line when the runtime cannot reach the SecureComs Agent
535
+ Connector, and says so once instead of on every retry.
536
+
537
+ - **Plain connection lines.** While it waits or fails, the runtime prints one
538
+ line for the cause, for example `Waiting for the SecureComs Agent Connector
539
+ to start on this computer...` or `Another agent's Connector is using this
540
+ agent's port. Restart this agent's Connector.` When it connects it prints
541
+ `Connected to the SecureComs Agent Connector.`
542
+ - **Quieter log.** The service log records each cause once until it changes,
543
+ not on every retry. The log names the cause but never a token, a path or an
544
+ identity id.
545
+ - **Plainer install refusals.** When install cannot use this computer's Node,
546
+ it names the Node version it needs and the fixed places where Node must be
547
+ installed.
548
+
549
+ No setting changes. Use it with `@securecoms/agent-bridge` 0.30.2.
550
+
530
551
  ## Release notes: 0.13.0
531
552
 
532
553
  <!-- [COPY, approved by Eric 2026-10-12] -->
package/dist/bin.js CHANGED
@@ -77,6 +77,25 @@ function failureEpisodeReporter(warn) {
77
77
  }
78
78
  };
79
79
  }
80
+ function reasonEpisodeReporter(onNew) {
81
+ let last = null;
82
+ return {
83
+ failed(reason) {
84
+ if (reason === last) return;
85
+ last = reason;
86
+ try {
87
+ onNew(reason);
88
+ } catch {
89
+ }
90
+ },
91
+ attached() {
92
+ last = null;
93
+ }
94
+ };
95
+ }
96
+ function writeHumanLine(line2) {
97
+ console.error(line2);
98
+ }
80
99
  function writeServiceLog(level, event, fields, error) {
81
100
  const line2 = formatServiceLog(level, event, fields, error);
82
101
  if (level === "error") console.error(line2);
@@ -1547,7 +1566,10 @@ var STABLE_LOCATIONS = [
1547
1566
  // system (linux)
1548
1567
  ];
1549
1568
  function candidates(home) {
1550
- return home ? [...STABLE_LOCATIONS, `${home}/.local/bin/node`] : STABLE_LOCATIONS;
1569
+ return home ? [...STABLE_LOCATIONS, `${home}/.local/bin/node`] : [...STABLE_LOCATIONS];
1570
+ }
1571
+ function orList(items) {
1572
+ return items.length <= 1 ? items.join("") : `${items.slice(0, -1).join(", ")} or ${items[items.length - 1]}`;
1551
1573
  }
1552
1574
  function usable(path, deps) {
1553
1575
  if (!deps.fileExists(path)) return false;
@@ -1586,7 +1608,7 @@ function chooseInstallExecPath(runningExecPath, override, deps, home) {
1586
1608
  const r = resolveStableNodePath(runningExecPath, deps, home);
1587
1609
  if (!r.stable) {
1588
1610
  throw new Error(
1589
- `install: no reboot-stable Node found (need major >= ${REQUIRED_NODE_MAJOR} at a path whose location does not encode a version). Version-manager Nodes (nvm/asdf/fnm/volta/n) and Homebrew kegs are relocated on upgrade and would break the service after a reboot. Install a system Node (nodejs.org pkg \u2192 /usr/local/bin/node) or pass --exec-path <stable-path> to override.`
1611
+ `The agent's background service needs Node ${REQUIRED_NODE_MAJOR} or newer at a fixed location (${orList(candidates(home))}), 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.`
1590
1612
  );
1591
1613
  }
1592
1614
  return r.path;
@@ -1602,7 +1624,7 @@ function abs(p) {
1602
1624
  function deriveServiceConfig(input) {
1603
1625
  if (isVersionEmbeddedNodePath(input.execPath)) {
1604
1626
  throw new Error(
1605
- `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>.`
1627
+ `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.`
1606
1628
  );
1607
1629
  }
1608
1630
  const head = input.identityId.slice(0, 8).toLowerCase();
@@ -4384,6 +4406,26 @@ function parseVoiceGuidance(frame) {
4384
4406
  const g = frame.guidance.trim().slice(0, VOICE_GUIDANCE_MAX_CHARS);
4385
4407
  return g || void 0;
4386
4408
  }
4409
+ var TransportStepError = class extends Error {
4410
+ constructor(reason, options) {
4411
+ super(reason, options);
4412
+ this.reason = reason;
4413
+ this.name = "TransportStepError";
4414
+ }
4415
+ };
4416
+ var TRANSPORT_LINES = {
4417
+ no_connection_file: "Waiting for the SecureComs Agent Connector to start on this computer...",
4418
+ bridge_unreachable: "The SecureComs Agent Connector is not answering. Check that it is running on this computer.",
4419
+ wrong_agent: "Another agent's Connector is using this agent's port. Restart this agent's Connector.",
4420
+ attach_refused: "The Connector refused this runtime. Restart the Connector, then restart this runtime.",
4421
+ connection_file_unreadable: "Can't read the Connector's connection details. Restart the Connector.",
4422
+ token_unreadable: "Can't read the Connector's connection details. Restart the Connector.",
4423
+ unknown: "Can't connect to the SecureComs Agent Connector yet. Retrying.",
4424
+ attached: "Connected to the SecureComs Agent Connector."
4425
+ };
4426
+ function isEnoent(e) {
4427
+ return typeof e === "object" && e !== null && e.code === "ENOENT";
4428
+ }
4387
4429
  var SEEN_MAX = 5e3;
4388
4430
  var PENDING_REPLY_MAX = 1e3;
4389
4431
  var TERMINAL_REPLY_ERRORS = /* @__PURE__ */ new Set(["agent_paused", "send_forbidden"]);
@@ -4394,6 +4436,11 @@ var SecureComsDataPlane = class {
4394
4436
  initialDelayMs;
4395
4437
  maxDelayMs;
4396
4438
  log;
4439
+ print;
4440
+ /** Tier A item 6: one report per reason episode; the settle timer ends it. */
4441
+ episodes;
4442
+ /** The cause behind the failure currently being reported (its class is logged). */
4443
+ failureCause = void 0;
4397
4444
  sleep;
4398
4445
  random;
4399
4446
  healthTimeoutMs;
@@ -4466,6 +4513,11 @@ var SecureComsDataPlane = class {
4466
4513
  this.initialDelayMs = cfg.initialDelayMs ?? 250;
4467
4514
  this.maxDelayMs = cfg.maxDelayMs ?? 3e4;
4468
4515
  this.log = cfg.log ?? writeServiceLog;
4516
+ this.print = cfg.print ?? writeHumanLine;
4517
+ this.episodes = reasonEpisodeReporter((reason) => {
4518
+ this.log("error", "transport.connection_attempt_failed", { reason: logLabel(reason) }, this.failureCause);
4519
+ this.print(TRANSPORT_LINES[reason] ?? TRANSPORT_LINES.unknown);
4520
+ });
4469
4521
  this.sleep = cfg.sleep ?? ((ms2) => new Promise((resolve4) => {
4470
4522
  this.sleepTimer = setTimeout(() => {
4471
4523
  this.sleepTimer = null;
@@ -4552,6 +4604,13 @@ var SecureComsDataPlane = class {
4552
4604
  void this.sleep(ms2).then(finish, finish);
4553
4605
  });
4554
4606
  }
4607
+ /** A human line from a timer callback: a throwing sink must never escape it. */
4608
+ say(line2) {
4609
+ try {
4610
+ this.print(line2);
4611
+ } catch {
4612
+ }
4613
+ }
4555
4614
  clearSettle() {
4556
4615
  if (this.settle) {
4557
4616
  clearTimeout(this.settle);
@@ -4573,7 +4632,9 @@ var SecureComsDataPlane = class {
4573
4632
  await this.attemptOnce();
4574
4633
  delay = this.initialDelayMs;
4575
4634
  } catch (e) {
4576
- this.log("error", "transport.connection_attempt_failed", {}, e);
4635
+ const step = e instanceof TransportStepError ? e : null;
4636
+ this.failureCause = step && step.cause !== void 0 ? step.cause : e;
4637
+ this.episodes.failed(step ? step.reason : "unknown");
4577
4638
  }
4578
4639
  if (!live()) break;
4579
4640
  await this.wait(this.random() * delay);
@@ -4582,9 +4643,26 @@ var SecureComsDataPlane = class {
4582
4643
  if (this.generation === generation) this.running = false;
4583
4644
  }
4584
4645
  async attemptOnce() {
4585
- const conn = JSON.parse(readFileSync10(this.connectionPath, "utf8"));
4586
- const httpBase = conn.bridge_url.replace(/^ws/, "http").replace(/\/v1\/ws$/, "");
4587
- const token = readFileSync10(conn.token_file, "utf8").trim();
4646
+ let raw;
4647
+ try {
4648
+ raw = readFileSync10(this.connectionPath, "utf8");
4649
+ } catch (e) {
4650
+ throw new TransportStepError(isEnoent(e) ? "no_connection_file" : "connection_file_unreadable", { cause: e });
4651
+ }
4652
+ let conn;
4653
+ let httpBase;
4654
+ try {
4655
+ conn = JSON.parse(raw);
4656
+ httpBase = conn.bridge_url.replace(/^ws/, "http").replace(/\/v1\/ws$/, "");
4657
+ } catch (e) {
4658
+ throw new TransportStepError("connection_file_unreadable", { cause: e });
4659
+ }
4660
+ let token;
4661
+ try {
4662
+ token = readFileSync10(conn.token_file, "utf8").trim();
4663
+ } catch (e) {
4664
+ throw new TransportStepError("token_unreadable", { cause: e });
4665
+ }
4588
4666
  const healthController = new AbortController();
4589
4667
  this.healthController = healthController;
4590
4668
  const healthTimer = setTimeout(
@@ -4594,14 +4672,14 @@ var SecureComsDataPlane = class {
4594
4672
  let health;
4595
4673
  try {
4596
4674
  health = await (await fetch(`${httpBase}/healthz`, { signal: healthController.signal })).json();
4675
+ } catch (e) {
4676
+ throw new TransportStepError("bridge_unreachable", { cause: e });
4597
4677
  } finally {
4598
4678
  clearTimeout(healthTimer);
4599
4679
  if (this.healthController === healthController) this.healthController = null;
4600
4680
  }
4601
4681
  if (conn.identity_id !== health.identity_id || this.identityId !== health.identity_id) {
4602
- throw new Error(
4603
- `gate refused: expected ${this.identityId}, connection file says ${conn.identity_id}, daemon on this port is ${health.identity_id}`
4604
- );
4682
+ throw new TransportStepError("wrong_agent");
4605
4683
  }
4606
4684
  if (this.stopped) return;
4607
4685
  const ws = new WebSocket(`${conn.bridge_url}?token=${encodeURIComponent(token)}`);
@@ -4611,15 +4689,18 @@ var SecureComsDataPlane = class {
4611
4689
  try {
4612
4690
  await new Promise((resolve4, reject) => {
4613
4691
  ws.addEventListener("error", (e) => {
4614
- reject(Object.assign(new Error("socket_error"), { cause: e }));
4692
+ const err = Object.assign(new Error("socket_error"), { cause: e });
4693
+ reject(established ? err : new TransportStepError("attach_refused", { cause: err }));
4615
4694
  });
4616
4695
  ws.addEventListener("close", (e) => {
4617
4696
  if (established) resolve4();
4618
4697
  else {
4619
4698
  reject(
4620
- new Error(
4621
- `closed with code ${e.code} before the connection settled \u2014 bad token, or another connector took this port`
4622
- )
4699
+ new TransportStepError("attach_refused", {
4700
+ cause: new Error(
4701
+ `closed with code ${e.code} before the connection settled \u2014 bad token, or another connector took this port`
4702
+ )
4703
+ })
4623
4704
  );
4624
4705
  }
4625
4706
  });
@@ -4632,6 +4713,8 @@ var SecureComsDataPlane = class {
4632
4713
  settle = setTimeout(() => {
4633
4714
  established = true;
4634
4715
  this.log("info", "transport.attached");
4716
+ this.episodes.attached();
4717
+ this.say(TRANSPORT_LINES.attached);
4635
4718
  }, this.establishedMs);
4636
4719
  this.settle = settle;
4637
4720
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securecoms/agent-runtime",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "securecoms-agent": "dist/bin.js"