@securecoms/agent-bridge 0.16.0 → 0.17.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.
Files changed (3) hide show
  1. package/README.md +60 -1
  2. package/dist/bin.js +22 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -305,6 +305,65 @@ Pick one receive mode — WebSocket, SSE, or long-poll — and stick with it. Th
305
305
 
306
306
  ---
307
307
 
308
+ ## Bring your own agent
309
+
310
+ The daemon has no approved-framework list. It checks a bearer token and nothing else, so there is exactly one requirement:
311
+
312
+ > **You must be able to add code to a process that stays running.**
313
+
314
+ That is the whole test. If your agent is a program you control, it can hold a SecureComs channel in about fifty lines — see [`examples/ws-agent-client.mjs`](./examples/ws-agent-client.mjs) and [`ws-agent-client.py`](./examples/ws-agent-client.py), which are runnable as-is.
315
+
316
+ To check your own connector against every rule in this section, run [`examples/byo-conformance.mjs`](./examples/byo-conformance.mjs) — a zero-dependency agent that follows the section literally and prints a pass/fail line per rule:
317
+
318
+ ```sh
319
+ node examples/byo-conformance.mjs <your agent's identity_id>
320
+ ```
321
+
322
+ **This fits.** Anything you build yourself, in any language: LangChain, LangGraph, CrewAI, AutoGen, Pydantic AI, the OpenAI or Anthropic agent SDKs, LlamaIndex — or a plain script calling an LLM API with no framework at all. You already own the process; you add a socket to it.
323
+
324
+ **This does not fit.** Consumer chat products (claude.ai, ChatGPT, the Gemini app) have no extension point. Session-based coding CLIs have no always-on loop to attach to. A fully hosted agent you cannot run code beside cannot reach the daemon.
325
+
326
+ **A platform you run but did not write** (Dify, Flowise, n8n, and the like) fits only if it lets you run persistent custom code. If it does, what you are writing is a plugin for that platform — the same job the bundled `openclaw` and `hermes` connectors do.
327
+
328
+ Install with `--harness universal` to skip connector wiring and print the details your agent needs. (`--harness custom` is the old name for the same thing and still works.) Running `install` with no `--harness` at all does the same thing when neither OpenClaw nor Hermes is present.
329
+
330
+ ### Where your agent runs
331
+
332
+ Same machine by default — the daemon binds `127.0.0.1`. Another machine works too, over TLS (see **Cross-host use** under Security). One caveat: a delivered attachment gives you a **local file path** on the daemon's machine, so an off-host agent can receive text, status, vault and A2A traffic but cannot read attachment bytes.
333
+
334
+ ### The five rules
335
+
336
+ 1. **Ack everything.** Delivery is at-least-once. Send `{type:"ack", message_id}` once you have durably handled a message, and dedup by `message_id`.
337
+ 2. **One consumer per daemon.** A second `/v1/ws` closes the first. Two clients will evict each other in a loop.
338
+ 3. **Answer the pings.** The daemon pings every 30s and reaps a socket that misses a pong. Standard WebSocket clients do this for you.
339
+ 4. **Read the token from its file, never from an environment value.** An env *value* is inherited by child processes and visible in `ps eww` and crash dumps. A path is not.
340
+ 5. **Check the identity gate before you attach.** Details below.
341
+
342
+ ### The identity gate
343
+
344
+ Skip this and a single-agent host still works. The failure appears later, when a second agent is installed on the same machine, and it is silent.
345
+
346
+ The daemon rewrites `~/.securecoms/connections/<identity_id>.json` on every start, giving you its current port and token-file path — so a moved port is picked up automatically. But if your connector reads the *wrong* agent's file, everything looks consistent: it authenticates successfully to that agent's daemon, drives that agent's identity, and evicts that agent's real connector.
347
+
348
+ The bearer token does not catch this. It rejects a caller with *no* valid token; it does nothing against a caller holding a *valid but wrong* pair.
349
+
350
+ So, before opening the WebSocket:
351
+
352
+ 1. Know which agent you are meant to serve — a fixed `identity_id`, from your own config. **Never** infer it by scanning the connections directory: on a multi-agent host a slow-starting daemon can leave exactly one file present at the moment you look, and you will bind to the wrong agent.
353
+ 2. Read that agent's connection file for `bridge_url` and `token_file`.
354
+ 3. `GET /healthz` (no token needed) and require **both**: the file's `identity_id` and your own fixed `identity_id` each equal `healthz.identity_id`.
355
+ 4. Any mismatch: do not open the socket. Back off with jitter and retry from step 2 — the port may have moved.
356
+
357
+ On reconnect, re-read the connection file and the token, but **not** your identity. Re-deriving it on every reconnect lets a file appearing or disappearing silently re-bind a running connector to a different agent.
358
+
359
+ The bundled `openclaw` and `hermes` connectors implement all of this; read either one as a worked example.
360
+
361
+ ### Separate compartments need separate OS uids
362
+
363
+ The gate prevents honest mistakes. It is not a tenant boundary. The hop from daemon to agent carries plaintext, and the token and sealed store are readable by anyone sharing the uid — so any agent under the same uid can already drive another's daemon directly. **Agents in different orgs or enclaves that must not cross MUST run under different OS uids, or on different hosts.**
364
+
365
+ ---
366
+
308
367
  ## Run as a service
309
368
 
310
369
  For an autonomous agent running on a server, keep the daemon up with systemd so it restarts on failure and starts on boot. A sleeping or crashed daemon means its WebSocket/SSE connections drop and messages queue on the server until the daemon reconnects; they replay losslessly on wake. The daemon can't wake a sleeping machine.
@@ -358,7 +417,7 @@ pm2 start "npx @agentvault/connect-agent-bridge --credential ./agent.json" \
358
417
  - A **free port is auto-selected** from `7077-7276` (lowest free wins; sibling service definitions and live binds both count as taken). Pass `--port N` to override; the explicit port is honored as-is.
359
418
  - Install **verifies the daemon actually came up** and fails loudly with the log path if it didn't — no silent crash-loops.
360
419
  - Install echoes the credential's `device_id`, `issued_at`, and a redacted api-key fingerprint so you can confirm you installed the newest download. **Re-running install is the only way to update a live (e.g. rotated) credential** — replacing the downloaded file alone does nothing.
361
- - Optional `--harness openclaw|hermes|custom` prints a paste-ready config snippet with the chosen port and the token-file path (the token itself never appears in output).
420
+ - Optional `--harness openclaw|hermes|universal` prints a paste-ready config snippet with the chosen port and the token-file path (the token itself never appears in output). (`--harness custom` is an accepted alias for `universal`.)
362
421
  - **One harness gateway serves one agent.** The connector registers a single platform and resolves a single bound identity for the life of the gateway process, so a connector directory (`~/.hermes/plugins/securecoms`, `~/.openclaw/local-plugins/securecoms`) holds exactly one agent. Install and `wire` therefore **refuse** to overwrite a binding that names a different agent — doing so would take the harness away from that agent, not add one alongside it. To run two agents on one host, give each its own harness profile with its own connector directory and binding. To deliberately move a harness to a different agent, pass `--rebind`.
363
422
 
364
423
  ---
package/dist/bin.js CHANGED
@@ -11125,15 +11125,30 @@ function runUnwire(deps) {
11125
11125
  function customFacts(input) {
11126
11126
  const ws = `ws://127.0.0.1:${input.port ?? "<PORT>"}/v1/ws`;
11127
11127
  return [
11128
- ` bridge WS: ${ws}`,
11129
- ` token file: ${input.tokenFilePath}`,
11128
+ ` bridge WS: ${ws}`,
11129
+ ` token file: ${input.tokenFilePath}`,
11130
+ ` agent id: ${input.identityId}`,
11131
+ ` discovery: ~/.securecoms/connections/${input.identityId}.json (rewritten on every daemon start)`,
11130
11132
  ` Bring your own connector: open the WS with the bearer token read from that file.`,
11133
+ ` Ack every message you receive; delivery is at-least-once, so dedup by message_id.`,
11134
+ ` Before you attach, check the identity gate: the daemon's /healthz identity_id MUST equal`,
11135
+ ` the agent id above. Skipping it means a second agent on this host can silently`,
11136
+ ` drive the wrong daemon. See "Bring your own agent" in the agent-bridge README.`,
11131
11137
  ` Protocol reference: the "WebSocket" section of the agent-bridge README.`
11132
11138
  ].join("\n");
11133
11139
  }
11134
11140
  function autoWireHarness(input, deps) {
11135
11141
  const h = input.harness;
11136
- if (!h || h === "none") return { echo: "", wired: false, error: false };
11142
+ if (h === "none") return { echo: "", wired: false, error: false };
11143
+ if (!h) {
11144
+ return {
11145
+ echo: `[securecoms] No agent harness detected (no ~/.openclaw, no ~/.hermes) \u2014 the bridge service is installed and running, but nothing is attached to it yet.
11146
+ Bring your own agent:
11147
+ ${customFacts(input)}`,
11148
+ wired: false,
11149
+ error: false
11150
+ };
11151
+ }
11137
11152
  if (h === "openclaw") {
11138
11153
  const retry = `agent-bridge wire --harness openclaw --identity ${input.slug}`;
11139
11154
  const parent2 = `${input.home}/.openclaw`;
@@ -11141,7 +11156,7 @@ function autoWireHarness(input, deps) {
11141
11156
  return {
11142
11157
  echo: `[securecoms] OpenClaw not found at ~/.openclaw \u2014 the bridge service is installed, but the harness was NOT wired.
11143
11158
  Install OpenClaw, then re-run install, or run: ${retry}
11144
- (or re-run install with --harness custom).`,
11159
+ (or re-run install with --harness universal).`,
11145
11160
  wired: false,
11146
11161
  error: true
11147
11162
  };
@@ -11182,7 +11197,7 @@ function autoWireHarness(input, deps) {
11182
11197
  return {
11183
11198
  echo: `[securecoms] Hermes not found at ~/.hermes \u2014 the bridge service is installed, but the harness was NOT wired.
11184
11199
  Install Hermes, then re-run install, or run: ${retry}
11185
- (or re-run install with --harness custom).`,
11200
+ (or re-run install with --harness universal).`,
11186
11201
  wired: false,
11187
11202
  error: true
11188
11203
  };
@@ -11226,7 +11241,7 @@ function autoWireHarness(input, deps) {
11226
11241
  };
11227
11242
  }
11228
11243
  }
11229
- return { echo: `[securecoms] Custom harness \u2014 bring your own connector:
11244
+ return { echo: `[securecoms] Universal \u2014 bring your own agent:
11230
11245
  ${customFacts(input)}`, wired: false, error: false };
11231
11246
  }
11232
11247
 
@@ -11797,7 +11812,7 @@ async function runServiceVerb(verb) {
11797
11812
  const label = argOrEnv("--label", "SECURECOMS_LABEL");
11798
11813
  if (verb === "install" ? !credentialPath : !credentialPath && !label) {
11799
11814
  const execPathUsage = verb === "install" ? " [--exec-path <path>]" : "";
11800
- process.stderr.write(`usage: agent-bridge ${verb} --credential <path> [--label <name>] [--store <path>] [--port N] [--harness openclaw|hermes|custom]${execPathUsage}
11815
+ process.stderr.write(`usage: agent-bridge ${verb} --credential <path> [--label <name>] [--store <path>] [--port N] [--harness openclaw|hermes|universal]${execPathUsage}
11801
11816
  `);
11802
11817
  process.exit(2);
11803
11818
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securecoms/agent-bridge",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",