@securecoms/agent-bridge 0.5.0 → 0.6.0

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.
@@ -24,6 +24,8 @@ export interface UpgradeDeps {
24
24
  cleanup: (p: string) => void;
25
25
  resolveNewNode: () => string | null;
26
26
  reexec: (nodePath: string) => boolean;
27
+ activeVersion: string;
28
+ existingCompatibleNode: () => string | null;
27
29
  }
28
30
  export declare function guidedUpgrade(deps: UpgradeDeps): Promise<boolean>;
29
31
  export declare function resolveNewNodeFrom(probe: (p: string) => string | null, candidates?: string[]): string | null;
@@ -21,6 +21,15 @@ var __copyProps = (to, from, except, desc) => {
21
21
  };
22
22
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
23
23
 
24
+ // src/service/resolve-node.ts
25
+ var REQUIRED_NODE_MAJOR;
26
+ var init_resolve_node = __esm({
27
+ "src/service/resolve-node.ts"() {
28
+ "use strict";
29
+ REQUIRED_NODE_MAJOR = 24;
30
+ }
31
+ });
32
+
24
33
  // src/preflight-upgrade.cts
25
34
  var preflight_upgrade_exports = {};
26
35
  __export(preflight_upgrade_exports, {
@@ -54,6 +63,13 @@ async function promptYesNo(question, deps) {
54
63
  async function guidedUpgrade(deps) {
55
64
  if (deps.platform !== "darwin") return false;
56
65
  try {
66
+ const existing = deps.existingCompatibleNode();
67
+ if (existing) {
68
+ deps.stderr(
69
+ "[bridge] Node " + deps.activeVersion + " is running, but a compatible Node (>= " + REQUIRED_NODE_MAJOR + ") is already installed at " + existing + " \u2014 it is shadowed earlier on your PATH. Continuing under it (no install needed).\n"
70
+ );
71
+ return deps.reexec(existing);
72
+ }
57
73
  const viaBrew = deps.hasBrew();
58
74
  const source = viaBrew ? "Homebrew (brew install node \u2014 user-space, no sudo)" : "the official installer from " + DIST_BASE + " (needs your admin password)";
59
75
  const ok = await promptYesNo(
@@ -94,7 +110,7 @@ function resolveNewNodeFrom(probe, candidates) {
94
110
  const list = candidates || NODE_CANDIDATES;
95
111
  for (let i = 0; i < list.length; i++) {
96
112
  const v = probe(list[i]);
97
- if (v && Number(String(v).replace(/^v/, "").split(".")[0]) >= 24) return list[i];
113
+ if (v && Number(String(v).replace(/^v/, "").split(".")[0]) >= REQUIRED_NODE_MAJOR) return list[i];
98
114
  }
99
115
  return null;
100
116
  }
@@ -104,6 +120,13 @@ function realUpgradeDeps(originalArgv) {
104
120
  const os = require("os");
105
121
  const https = require("https");
106
122
  const readline = require("readline");
123
+ const probeNodeVersion = (p) => {
124
+ try {
125
+ return String(execFileSync(p, ["--version"], { encoding: "utf8" })).trim();
126
+ } catch {
127
+ return null;
128
+ }
129
+ };
107
130
  function fetch(url, redirects) {
108
131
  return new Promise((resolve, reject) => {
109
132
  https.get(url, (res) => {
@@ -162,13 +185,12 @@ function realUpgradeDeps(originalArgv) {
162
185
  } catch {
163
186
  }
164
187
  },
165
- resolveNewNode: () => resolveNewNodeFrom((p) => {
166
- try {
167
- return String(execFileSync(p, ["--version"], { encoding: "utf8" })).trim();
168
- } catch {
169
- return null;
170
- }
171
- }),
188
+ resolveNewNode: () => resolveNewNodeFrom(probeNodeVersion),
189
+ activeVersion: process.versions.node,
190
+ // Same candidate probe as resolveNewNode: is a Node >= REQUIRED_NODE_MAJOR
191
+ // already installed at a stable location (shadowed on PATH)? If so, the
192
+ // pre-check re-execs under it and never prompts to install.
193
+ existingCompatibleNode: () => resolveNewNodeFrom(probeNodeVersion),
172
194
  reexec: (nodePath) => {
173
195
  const child = spawnSync(nodePath, originalArgv.slice(1), { stdio: "inherit" });
174
196
  process.exit(child.status !== null && child.status !== void 0 ? child.status : child.signal ? 130 : 1);
@@ -179,6 +201,7 @@ var DIST_BASE, NODE_CANDIDATES;
179
201
  var init_preflight_upgrade = __esm({
180
202
  "src/preflight-upgrade.cts"() {
181
203
  "use strict";
204
+ init_resolve_node();
182
205
  DIST_BASE = "https://nodejs.org/dist/latest-v24.x";
183
206
  NODE_CANDIDATES = [
184
207
  "/usr/local/bin/node",
@@ -0,0 +1,33 @@
1
+ export type ConnectionFileV1 = {
2
+ schema: "securecoms.connection/1";
3
+ identity_id: string;
4
+ bridge_url: string;
5
+ token_file: string;
6
+ instance_id: string;
7
+ slug: string;
8
+ label: string;
9
+ written_at: string;
10
+ };
11
+ export interface FsDeps {
12
+ mkdir: (p: string, mode: number) => void;
13
+ chmod: (p: string, mode: number) => void;
14
+ writeFile: (p: string, data: string, mode: number) => void;
15
+ rename: (a: string, b: string) => void;
16
+ readFile: (p: string) => string;
17
+ unlink: (p: string) => void;
18
+ }
19
+ export declare function securecomsDir(home: string): string;
20
+ export declare function connectionsDir(home: string): string;
21
+ export declare function connectionFilePath(home: string, identityId: string): string;
22
+ export declare function newInstanceId(): string;
23
+ export declare function buildConnectionFile(args: {
24
+ identityId: string;
25
+ port: number;
26
+ tokenFile: string;
27
+ slug?: string;
28
+ label?: string;
29
+ instanceId: string;
30
+ now: string;
31
+ }): ConnectionFileV1;
32
+ export declare function writeConnectionFile(home: string, doc: ConnectionFileV1, deps?: FsDeps): void;
33
+ export declare function removeConnectionFileIfOwned(home: string, identityId: string, instanceId: string, deps?: FsDeps): void;
@@ -0,0 +1,27 @@
1
+ export declare function parseArgFromDef(def: string, flag: string): string | null;
2
+ export declare function parseExecPathFromDef(def: string): string | null;
3
+ export interface FleetDeps {
4
+ home: string;
5
+ listDir: (p: string) => string[];
6
+ readFile: (p: string) => string;
7
+ }
8
+ export interface AgentFleetEntry {
9
+ identityId: string;
10
+ deviceId: string;
11
+ slug: string;
12
+ label: string;
13
+ environment: "prod" | "preprod" | "unknown";
14
+ interpreterStable: boolean;
15
+ port: number | null;
16
+ interpreterPath: string | null;
17
+ credentialDir: string | null;
18
+ }
19
+ export declare function surfaceableProjection(e: AgentFleetEntry): {
20
+ identityId: string;
21
+ deviceId: string;
22
+ slug: string;
23
+ label: string;
24
+ environment: "unknown" | "prod" | "preprod";
25
+ interpreterStable: boolean;
26
+ };
27
+ export declare function readFleet(deps: FleetDeps): AgentFleetEntry[];
@@ -42,6 +42,7 @@ export declare function harnessSnippet(args: {
42
42
  port: number;
43
43
  tokenFilePath: string;
44
44
  harness: string;
45
+ identityId: string;
45
46
  }): string;
46
47
  export interface OrchestrateInstallCtx {
47
48
  credentialAbsPath: string;
@@ -0,0 +1,13 @@
1
+ export declare const REQUIRED_NODE_MAJOR = 24;
2
+ export declare function isVersionEmbeddedNodePath(p: string): boolean;
3
+ export interface ResolveNodeDeps {
4
+ realpath: (p: string) => string;
5
+ fileExists: (p: string) => boolean;
6
+ nodeMajorOf: (p: string) => number | null;
7
+ arch: () => string;
8
+ archOf: (p: string) => string | null;
9
+ }
10
+ export declare function resolveStableNodePath(runningExecPath: string, deps: ResolveNodeDeps): {
11
+ path: string;
12
+ stable: boolean;
13
+ };
@@ -0,0 +1,25 @@
1
+ import type { AgentFleetEntry } from "./fleet.js";
2
+ export interface StatusDeps {
3
+ readFleet: () => AgentFleetEntry[];
4
+ probeConnect: (port: number) => boolean;
5
+ getHealth: (port: number) => {
6
+ agent_sessions?: number;
7
+ evictions?: number;
8
+ } | null;
9
+ }
10
+ export interface AgentStatusRow {
11
+ slug: string;
12
+ environment: string;
13
+ port: number | null;
14
+ bridge: "up" | "down";
15
+ harness: "attached" | "no brain" | "-";
16
+ interpreter: "stable" | "REBOOT-RISK";
17
+ /** Task 3 (audit M3): cumulative newest-wins WS evictions for this identity,
18
+ * read straight off the daemon's health(). Nonzero/climbing while harness
19
+ * stays "attached" means two connectors are flapping the same identity
20
+ * (split-brain), not one stable harness — 0 when the bridge is down or
21
+ * the daemon predates this counter. */
22
+ evictions: number;
23
+ }
24
+ export declare function readFleetStatus(deps: StatusDeps): AgentStatusRow[];
25
+ export declare function renderStatusTable(rows: AgentStatusRow[]): string;
@@ -0,0 +1,76 @@
1
+ import type { AgentFleetEntry } from "./fleet.js";
2
+ export type Harness = "openclaw" | "hermes";
3
+ export interface WireDeps {
4
+ home: string;
5
+ argv: string[];
6
+ readFleet: () => AgentFleetEntry[];
7
+ copyConnector: (srcDir: string, dstDir: string) => void;
8
+ writeBinding: (dstDir: string, identityId: string) => void;
9
+ removeBinding: (dstDir: string) => void;
10
+ getHealth: (port: number) => {
11
+ agent_sessions?: number;
12
+ } | null;
13
+ log: (s: string) => void;
14
+ /**
15
+ * Precomputed absolute path to the reference connector's source dir
16
+ * (`<pkgRoot>/connectors/openclaw`). REQUIRED: the only real caller
17
+ * (bin.ts's realWireDeps) always computes this via
18
+ * `connectorsSrcDir(import.meta.url)` using bin.ts's OWN import.meta.url —
19
+ * see that function's doc comment for why it has to be bin.ts's URL, not
20
+ * wire.ts's. A fallback that read wire.ts's own import.meta.url used to
21
+ * live here, but wire.ts sits one directory deeper than bin.ts
22
+ * (`src/service/wire.ts` vs `src/bin.ts`), so that fallback resolved to
23
+ * the wrong path — removed rather than fixed, since every real caller
24
+ * already supplies this field. Unit tests must pass a value too (any
25
+ * string is fine — `copyConnector` is mocked in tests and never inspects
26
+ * it for the non-hermes paths that reach it).
27
+ */
28
+ connectorsSrcDir: string;
29
+ }
30
+ /**
31
+ * Detects an installed harness by probing well-known home-relative dirs.
32
+ * Checked in order: openclaw, then hermes. Returns null when neither is
33
+ * present — the caller (bin.ts's realWireDeps) must then require an explicit
34
+ * --harness rather than guess.
35
+ */
36
+ export declare function detectHarness(home: string, statDir: (p: string) => boolean): Harness | null;
37
+ /**
38
+ * Where the connector gets installed for a given harness. Openclaw is the
39
+ * REFERENCE CONNECTOR (Task 6, connectors/openclaw/) and is fully wired.
40
+ * Hermes gets a defined target path so `detectHarness`/`wire --harness hermes`
41
+ * don't crash, but the Hermes CONNECTOR ITSELF is a fast-follow — only
42
+ * detection + this target dir ship in this task. Do not claim a working
43
+ * Hermes connector exists.
44
+ */
45
+ export declare function connectorTargetDir(home: string, harness: Harness): string;
46
+ /**
47
+ * Resolves the reference connector's SOURCE dir at runtime. `connectors/` is
48
+ * shipped SOURCE (not compiled — H2), so this must resolve from the running
49
+ * file's location back to the package root, never into dist/. Callers pass
50
+ * their OWN import.meta.url (bin.ts's), not wire.ts's: esbuild bundles
51
+ * wire.ts's code into dist/bin.js, so only the entry point's import.meta.url
52
+ * is guaranteed to sit exactly one directory below the package root in BOTH
53
+ * dev (src/bin.ts) and the built artifact (dist/bin.js) — mirrors
54
+ * resolve-bin.ts's loadPackageMeta (`dist/bin.js -> ../package.json`).
55
+ */
56
+ export declare function connectorsSrcDir(importMetaUrl: string): string;
57
+ /**
58
+ * Installs the reference connector into the harness's plugin dir and drops
59
+ * an explicit securecoms.binding.json naming the resolved agent's identity.
60
+ * There is NO auto-discovery (audit C1): the binding is written for EVERY
61
+ * agent, singleton included, every time `wire` runs.
62
+ *
63
+ * Hermes has no real connector yet (this round only ships detection +
64
+ * `connectorTargetDir`, see that function's doc comment) — refuse rather
65
+ * than install the openclaw connector's files into a Hermes plugin dir,
66
+ * which would be a false "installed" success.
67
+ */
68
+ export declare function runWire(deps: WireDeps): number;
69
+ /** Removes the explicit binding for the resolved agent. Leaves the copied
70
+ * connector files in place — unwire un-names the agent, it doesn't uninstall
71
+ * the plugin (that stays a harness-native operation).
72
+ *
73
+ * Hermes never has anything installed by `runWire` (see its hermes guard),
74
+ * so there's nothing to remove — no-op cleanly rather than call
75
+ * `removeBinding` against a dir that was never created. */
76
+ export declare function runUnwire(deps: WireDeps): number;
@@ -27,7 +27,7 @@
27
27
  * Inbound delivery is at-least-once. A delivered `message` is held in-flight
28
28
  * until the agent sends a matching `ack`; if the socket drops first, the frame
29
29
  * is re-buffered and redelivered to the next consumer. The agent dedups by
30
- * message_id (sends are already idempotent via idempotency_key).
30
+ * message_id (a fresh UUID minted per send; server-side dedup is on replay_nonce).
31
31
  *
32
32
  * Single consumer: the daemon serves one agent per process. A new `/v1/ws`
33
33
  * connection replaces the prior one (newest wins, mirroring the SSE path); the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securecoms/agent-bridge",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "files": [
17
17
  "dist",
18
+ "connectors",
18
19
  "README.md"
19
20
  ],
20
21
  "publishConfig": {
@@ -28,20 +29,20 @@
28
29
  },
29
30
  "dependencies": {
30
31
  "@agentvault/crypto": "^0.4.1",
32
+ "@noble/ciphers": "^1.3.0",
31
33
  "@noble/curves": "^1.9.7",
32
34
  "@noble/hashes": "^1.4.0",
33
- "@noble/ciphers": "^1.3.0",
34
35
  "ajv": "^8.18.0",
35
36
  "ajv-formats": "^3.0.1",
36
37
  "ws": "^8.21.0",
37
38
  "zod": "^3.23.0"
38
39
  },
39
40
  "devDependencies": {
41
+ "@agentvault/connect-core": "*",
40
42
  "@types/node": "^22.0.0",
41
43
  "@types/ws": "^8.18.1",
42
44
  "esbuild": "0.24.0",
43
45
  "typescript": "^5.4.0",
44
- "vitest": "^2.0.0",
45
- "@agentvault/connect-core": "*"
46
+ "vitest": "^2.0.0"
46
47
  }
47
48
  }