@securecoms/agent-bridge 0.6.0 → 0.8.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 (30) hide show
  1. package/README.md +2 -0
  2. package/connectors/hermes/__init__.py +60 -0
  3. package/connectors/hermes/__pycache__/__init__.cpython-311.pyc +0 -0
  4. package/connectors/hermes/__pycache__/adapter.cpython-311.pyc +0 -0
  5. package/connectors/hermes/__pycache__/gate.cpython-311.pyc +0 -0
  6. package/connectors/hermes/__pycache__/securecoms_gate.cpython-311.pyc +0 -0
  7. package/connectors/hermes/__pycache__/securecoms_transport.cpython-311.pyc +0 -0
  8. package/connectors/hermes/__pycache__/transport.cpython-311.pyc +0 -0
  9. package/connectors/hermes/adapter.py +470 -0
  10. package/connectors/hermes/plugin.yaml +25 -0
  11. package/connectors/hermes/pytest.ini +3 -0
  12. package/connectors/hermes/securecoms_gate.py +70 -0
  13. package/connectors/hermes/securecoms_transport.py +257 -0
  14. package/connectors/hermes/tests/__pycache__/conftest.cpython-311-pytest-8.4.0.pyc +0 -0
  15. package/connectors/hermes/tests/__pycache__/test_adapter_wiring.cpython-311-pytest-8.4.0.pyc +0 -0
  16. package/connectors/hermes/tests/__pycache__/test_gate.cpython-311-pytest-8.4.0.pyc +0 -0
  17. package/connectors/hermes/tests/__pycache__/test_securecoms_gate.cpython-311-pytest-8.4.0.pyc +0 -0
  18. package/connectors/hermes/tests/__pycache__/test_securecoms_transport.cpython-311-pytest-8.4.0.pyc +0 -0
  19. package/connectors/hermes/tests/__pycache__/test_transport.cpython-311-pytest-8.4.0.pyc +0 -0
  20. package/connectors/hermes/tests/conftest.py +23 -0
  21. package/connectors/hermes/tests/test_adapter_wiring.py +541 -0
  22. package/connectors/hermes/tests/test_securecoms_gate.py +113 -0
  23. package/connectors/hermes/tests/test_securecoms_transport.py +608 -0
  24. package/dist/bin.d.ts +2 -0
  25. package/dist/bin.js +436 -108
  26. package/dist/service/harness-wire.d.ts +22 -0
  27. package/dist/service/hermes-config.d.ts +51 -0
  28. package/dist/service/install-prep.d.ts +5 -8
  29. package/dist/service/wire.d.ts +97 -40
  30. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -7042,7 +7042,372 @@ function uninstallService(input, deps) {
7042
7042
  }
7043
7043
 
7044
7044
  // src/service/install-prep.ts
7045
- import { join as join8 } from "node:path";
7045
+ import { join as join9 } from "node:path";
7046
+
7047
+ // src/service/wire.ts
7048
+ import { join as join8, dirname as dirname5 } from "node:path";
7049
+ import { fileURLToPath } from "node:url";
7050
+
7051
+ // src/service/hermes-config.ts
7052
+ var HERMES_PLUGIN_NAME = "securecoms-platform";
7053
+ function indentOf(line) {
7054
+ const m = line.match(/^( *)/);
7055
+ return m ? m[1].length : 0;
7056
+ }
7057
+ function isCommentLine(line) {
7058
+ return /^\s*#/.test(line);
7059
+ }
7060
+ function isBlank(line) {
7061
+ return line.trim() === "";
7062
+ }
7063
+ function parseKeyLine(line, key) {
7064
+ const m = line.match(new RegExp(`^( *)${key}:(.*)$`));
7065
+ if (!m) return null;
7066
+ const indent = m[1].length;
7067
+ const rest = m[2].trim();
7068
+ if (rest === "" || rest.startsWith("#")) return { idx: -1, indent, inline: null };
7069
+ return { idx: -1, indent, inline: rest };
7070
+ }
7071
+ function findKeyLine(lines, key, start, end) {
7072
+ for (let i = start; i < end; i++) {
7073
+ const parsed = parseKeyLine(lines[i], key);
7074
+ if (parsed) return { ...parsed, idx: i };
7075
+ }
7076
+ return null;
7077
+ }
7078
+ function findBlockEnd(lines, start, parentIndent) {
7079
+ let i = start;
7080
+ for (; i < lines.length; i++) {
7081
+ if (isBlank(lines[i])) continue;
7082
+ if (indentOf(lines[i]) > parentIndent) continue;
7083
+ break;
7084
+ }
7085
+ return i;
7086
+ }
7087
+ function stripQuotesAndComment(raw) {
7088
+ let value = raw.replace(/\s+#.*$/, "").trim();
7089
+ if (value.length >= 2) {
7090
+ const first = value[0];
7091
+ const last = value[value.length - 1];
7092
+ if (first === '"' && last === '"' || first === "'" && last === "'") {
7093
+ value = value.slice(1, -1);
7094
+ }
7095
+ }
7096
+ return value;
7097
+ }
7098
+ function collectItems(lines, start) {
7099
+ const items = [];
7100
+ let i = start;
7101
+ for (; i < lines.length; i++) {
7102
+ const line = lines[i];
7103
+ if (isCommentLine(line)) {
7104
+ return { items, endIdx: i, ambiguous: true };
7105
+ }
7106
+ const m = line.match(/^( *)-\s*(.+?)\s*$/);
7107
+ if (!m) break;
7108
+ items.push({ idx: i, indent: m[1].length, value: stripQuotesAndComment(m[2]) });
7109
+ }
7110
+ return { items, endIdx: i, ambiguous: false };
7111
+ }
7112
+ var SIBLING_KEY_RE = /^[\w.-]+:(\s|$)/;
7113
+ function isLegitimateListEnd(lines, stopIdx, itemCol, enabledIndent) {
7114
+ if (stopIdx >= lines.length) return true;
7115
+ if (!isBlank(lines[stopIdx])) {
7116
+ return indentOf(lines[stopIdx]) === enabledIndent && SIBLING_KEY_RE.test(lines[stopIdx].trim());
7117
+ }
7118
+ let i = stopIdx;
7119
+ while (i < lines.length && isBlank(lines[i])) i++;
7120
+ if (i >= lines.length) return true;
7121
+ return indentOf(lines[i]) < itemCol;
7122
+ }
7123
+ function parseInlineList(inline) {
7124
+ const trimmed = inline.trim();
7125
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return [];
7126
+ const inner = trimmed.slice(1, -1).trim();
7127
+ if (inner === "") return [];
7128
+ return inner.split(",").map((s) => stripQuotesAndComment(s.trim()));
7129
+ }
7130
+ function noop(reason) {
7131
+ return reason ? { action: "noop", reason } : { action: "noop" };
7132
+ }
7133
+ var MANUAL_INSTRUCTIONS = [
7134
+ "add the following manually under the plugin's config (`~/.hermes/config.yaml`):",
7135
+ " plugins:",
7136
+ " enabled:",
7137
+ ` - ${HERMES_PLUGIN_NAME}`
7138
+ ].join("\n");
7139
+ function ensureHermesPluginEnabled(configText) {
7140
+ const eol = configText.includes("\r\n") ? "\r\n" : "\n";
7141
+ const lines = configText.split(/\r\n|\n/);
7142
+ const pluginsIdx = lines.findIndex((l) => /^plugins:\s*(#.*)?$/.test(l));
7143
+ if (pluginsIdx === -1) {
7144
+ return noop(`no \`plugins:\` block found in config.yaml \u2014 ${MANUAL_INSTRUCTIONS}`);
7145
+ }
7146
+ const pluginsIndent = indentOf(lines[pluginsIdx]);
7147
+ const blockEnd = findBlockEnd(lines, pluginsIdx + 1, pluginsIndent);
7148
+ const enabledKey = findKeyLine(lines, "enabled", pluginsIdx + 1, blockEnd);
7149
+ if (!enabledKey) {
7150
+ return noop(`no \`plugins.enabled\` key found in config.yaml \u2014 ${MANUAL_INSTRUCTIONS}`);
7151
+ }
7152
+ if (enabledKey.inline !== null) {
7153
+ return noop(
7154
+ `plugins.enabled uses the inline \`enabled: [...]\` form, which this editor refuses to rewrite (never reformat) \u2014 ${MANUAL_INSTRUCTIONS}`
7155
+ );
7156
+ }
7157
+ const { items, endIdx, ambiguous } = collectItems(lines, enabledKey.idx + 1);
7158
+ if (ambiguous) {
7159
+ return noop(
7160
+ `a comment line interrupts the plugins.enabled list \u2014 refusing rather than guess whether it belongs to the item above or below; ${MANUAL_INSTRUCTIONS}`
7161
+ );
7162
+ }
7163
+ if (items.length === 0) {
7164
+ return noop(`plugins.enabled has no items to copy an indent column from (empty list) \u2014 ${MANUAL_INSTRUCTIONS}`);
7165
+ }
7166
+ const col = items[0].indent;
7167
+ if (items.some((it) => it.indent !== col)) {
7168
+ return noop(`plugins.enabled items are inconsistently indented \u2014 refusing to guess the right column; ${MANUAL_INSTRUCTIONS}`);
7169
+ }
7170
+ const disabledKey = findKeyLine(lines, "disabled", pluginsIdx + 1, blockEnd);
7171
+ let disabledEntries = [];
7172
+ if (disabledKey) {
7173
+ if (disabledKey.inline !== null) {
7174
+ disabledEntries = parseInlineList(disabledKey.inline);
7175
+ } else {
7176
+ disabledEntries = collectItems(lines, disabledKey.idx + 1).items.map((it) => it.value);
7177
+ }
7178
+ }
7179
+ if (disabledEntries.includes(HERMES_PLUGIN_NAME)) {
7180
+ return noop(
7181
+ `${HERMES_PLUGIN_NAME} is listed under plugins.disabled, which wins over plugins.enabled in the Hermes loader \u2014 remove it from plugins.disabled (or move it to plugins.enabled) manually, then re-run wire.`
7182
+ );
7183
+ }
7184
+ if (items.some((it) => it.value === HERMES_PLUGIN_NAME)) {
7185
+ return { action: "noop" };
7186
+ }
7187
+ if (!isLegitimateListEnd(lines, endIdx, col, enabledKey.indent)) {
7188
+ return noop(
7189
+ `plugins.enabled's list doesn't end at a recognized boundary (expected a sibling key, a blank-then-dedent, or end of file) \u2014 refusing to insert at an unverified spot; ${MANUAL_INSTRUCTIONS}`
7190
+ );
7191
+ }
7192
+ const insertLine = `${" ".repeat(col)}- ${HERMES_PLUGIN_NAME}`;
7193
+ const newLines = [...lines.slice(0, endIdx), insertLine, ...lines.slice(endIdx)];
7194
+ return { action: "edited", text: newLines.join(eol) };
7195
+ }
7196
+
7197
+ // src/service/wire.ts
7198
+ var CONNECTOR_FILE_EXTENSIONS = {
7199
+ openclaw: [".js", ".json"],
7200
+ hermes: [".py", ".yaml", ".json"]
7201
+ };
7202
+ var CONNECTOR_SKIP_DIRS = /* @__PURE__ */ new Set(["tests", "__pycache__"]);
7203
+ function detectHarness(home, statDir) {
7204
+ if (statDir(`${home}/.openclaw`)) return "openclaw";
7205
+ if (statDir(`${home}/.hermes`)) return "hermes";
7206
+ return null;
7207
+ }
7208
+ function connectorTargetDir(home, harness) {
7209
+ if (harness === "openclaw") return `${home}/.openclaw/local-plugins/securecoms`;
7210
+ return `${home}/.hermes/plugins/securecoms`;
7211
+ }
7212
+ function connectorsSrcDir(importMetaUrl, harness) {
7213
+ const pkgRoot = dirname5(dirname5(fileURLToPath(importMetaUrl)));
7214
+ return join8(pkgRoot, "connectors", harness);
7215
+ }
7216
+ function hermesConfigPath(home) {
7217
+ return `${home}/.hermes/config.yaml`;
7218
+ }
7219
+ function hermesConfigBackupPath(configPath, ts, pid) {
7220
+ return `${configPath}.bak-wire-${ts}-${pid}`;
7221
+ }
7222
+ function enableHermesConfig(home, deps) {
7223
+ const path = hermesConfigPath(home);
7224
+ const text = deps.readHermesConfig(path);
7225
+ if (text === null) {
7226
+ return {
7227
+ ok: false,
7228
+ message: `Hermes config.yaml not found at ${path} (is ~/.hermes installed?) \u2014 add the plugin manually:
7229
+ plugins:
7230
+ enabled:
7231
+ - ${HERMES_PLUGIN_NAME}`
7232
+ };
7233
+ }
7234
+ const result = ensureHermesPluginEnabled(text);
7235
+ if (result.action === "noop") {
7236
+ if (result.reason) return { ok: false, message: result.reason };
7237
+ return { ok: true, message: `${HERMES_PLUGIN_NAME} is already enabled in ${path}.` };
7238
+ }
7239
+ deps.backupHermesConfig(path, text);
7240
+ deps.writeHermesConfig(path, result.text);
7241
+ return {
7242
+ ok: true,
7243
+ message: `enabled ${HERMES_PLUGIN_NAME} in ${path} (the original was backed up alongside it as config.yaml.bak-wire-<ts>-<pid>).`
7244
+ };
7245
+ }
7246
+ function parseArg(argv, flag) {
7247
+ const i = argv.indexOf(flag);
7248
+ return i >= 0 ? argv[i + 1] : void 0;
7249
+ }
7250
+ function resolveEntry(deps) {
7251
+ const identityArg = parseArg(deps.argv, "--identity");
7252
+ if (!identityArg) {
7253
+ deps.log("wire: missing --identity <id|slug>");
7254
+ return null;
7255
+ }
7256
+ const entry = deps.readFleet().find((e) => e.identityId === identityArg || e.slug === identityArg);
7257
+ if (!entry) {
7258
+ deps.log(`wire: unknown identity "${identityArg}" (not found in the local fleet)`);
7259
+ return null;
7260
+ }
7261
+ const harnessArg = parseArg(deps.argv, "--harness");
7262
+ if (harnessArg !== "openclaw" && harnessArg !== "hermes") {
7263
+ deps.log("wire: missing/invalid --harness <openclaw|hermes> (auto-detection did not resolve one)");
7264
+ return null;
7265
+ }
7266
+ return { entry, harness: harnessArg };
7267
+ }
7268
+ function runWire(deps) {
7269
+ const resolved = resolveEntry(deps);
7270
+ if (!resolved) return 1;
7271
+ const { entry, harness } = resolved;
7272
+ const targetDir = connectorTargetDir(deps.home, harness);
7273
+ deps.copyConnector(deps.connectorsSrcDir(harness), targetDir, harness);
7274
+ deps.writeBinding(targetDir, entry.identityId);
7275
+ if (harness === "hermes") {
7276
+ const enabled = enableHermesConfig(deps.home, deps);
7277
+ deps.log(`[securecoms] ${enabled.message}`);
7278
+ if (!enabled.ok) {
7279
+ deps.log(
7280
+ "wire: Hermes connector files were copied and the binding was written, but the config.yaml plugin entry was NOT enabled automatically \u2014 the harness is NOT wired until you add it manually (see above) and restart the gateway."
7281
+ );
7282
+ return 1;
7283
+ }
7284
+ }
7285
+ if (typeof entry.port === "number") {
7286
+ const h = deps.getHealth(entry.port);
7287
+ if (h && (h.agent_sessions ?? 0) >= 1) {
7288
+ deps.log(`wire: ${entry.slug} attached (agent_sessions=${h.agent_sessions})`);
7289
+ } else {
7290
+ deps.log(`wire: connector installed but not yet attached \u2014 restart the harness to pick it up.`);
7291
+ }
7292
+ } else {
7293
+ deps.log(`wire: connector installed (no bridge port known yet) \u2014 restart the harness to pick it up.`);
7294
+ }
7295
+ if (harness === "hermes") {
7296
+ deps.log(
7297
+ "wire: restart the Hermes gateway (`launchctl kickstart -k gui/$UID/ai.hermes.gateway` or your service manager) to pick up the connector."
7298
+ );
7299
+ }
7300
+ return 0;
7301
+ }
7302
+ function runUnwire(deps) {
7303
+ const resolved = resolveEntry(deps);
7304
+ if (!resolved) return 1;
7305
+ const { harness } = resolved;
7306
+ deps.removeBinding(connectorTargetDir(deps.home, harness));
7307
+ if (harness === "hermes") {
7308
+ deps.log(
7309
+ "[securecoms] Hermes binding removed. The connector files under ~/.hermes/plugins/securecoms and the plugins.enabled entry in config.yaml remain \u2014 removing a listed plugin's files would make Hermes log load errors at boot. Edit config.yaml manually to fully remove it, then restart the gateway."
7310
+ );
7311
+ }
7312
+ return 0;
7313
+ }
7314
+
7315
+ // src/service/harness-wire.ts
7316
+ function customFacts(input) {
7317
+ const ws = `ws://127.0.0.1:${input.port ?? "<PORT>"}/v1/ws`;
7318
+ return [
7319
+ ` bridge WS: ${ws}`,
7320
+ ` token file: ${input.tokenFilePath}`,
7321
+ ` Bring your own connector: open the WS with the bearer token read from that file.`,
7322
+ ` Protocol reference: the "WebSocket" section of the agent-bridge README.`
7323
+ ].join("\n");
7324
+ }
7325
+ function autoWireHarness(input, deps) {
7326
+ const h = input.harness;
7327
+ if (!h || h === "none") return { echo: "", wired: false, error: false };
7328
+ if (h === "openclaw") {
7329
+ const retry = `agent-bridge wire --harness openclaw --identity ${input.slug}`;
7330
+ const parent = `${input.home}/.openclaw`;
7331
+ if (!deps.statDir(parent)) {
7332
+ return {
7333
+ echo: `[securecoms] OpenClaw not found at ~/.openclaw \u2014 the bridge service is installed, but the harness was NOT wired.
7334
+ Install OpenClaw, then re-run install, or run: ${retry}
7335
+ (or re-run install with --harness custom).`,
7336
+ wired: false,
7337
+ error: true
7338
+ };
7339
+ }
7340
+ const targetDir = connectorTargetDir(input.home, "openclaw");
7341
+ try {
7342
+ const prev = deps.readBinding(targetDir);
7343
+ deps.copyConnector(input.connectorsSrcDir, targetDir, "openclaw");
7344
+ deps.writeBinding(targetDir, input.identityId);
7345
+ const rebind = prev && prev !== input.identityId ? `
7346
+ rebind: ${prev} \u2192 ${input.identityId}` : "";
7347
+ return {
7348
+ echo: `[securecoms] OpenClaw connector installed and bound to identity ${input.identityId}.${rebind}
7349
+ Now restart OpenClaw to pick up the connector.`,
7350
+ wired: true,
7351
+ error: false
7352
+ };
7353
+ } catch (e) {
7354
+ return {
7355
+ echo: `[securecoms] the bridge service is installed, but wiring the OpenClaw connector FAILED: ${e.message}
7356
+ Retry after fixing the path: ${retry}`,
7357
+ wired: false,
7358
+ error: true
7359
+ };
7360
+ }
7361
+ }
7362
+ if (h === "hermes") {
7363
+ const retry = `agent-bridge wire --harness hermes --identity ${input.slug}`;
7364
+ const parent = `${input.home}/.hermes`;
7365
+ if (!deps.statDir(parent)) {
7366
+ return {
7367
+ echo: `[securecoms] Hermes not found at ~/.hermes \u2014 the bridge service is installed, but the harness was NOT wired.
7368
+ Install Hermes, then re-run install, or run: ${retry}
7369
+ (or re-run install with --harness custom).`,
7370
+ wired: false,
7371
+ error: true
7372
+ };
7373
+ }
7374
+ const targetDir = connectorTargetDir(input.home, "hermes");
7375
+ try {
7376
+ const prev = deps.readBinding(targetDir);
7377
+ deps.copyConnector(input.connectorsSrcDir, targetDir, "hermes");
7378
+ deps.writeBinding(targetDir, input.identityId);
7379
+ const enabled = enableHermesConfig(input.home, deps);
7380
+ if (!enabled.ok) {
7381
+ return {
7382
+ echo: `[securecoms] Hermes connector copied and bound to identity ${input.identityId}, but the config.yaml plugin entry was NOT enabled automatically: ${enabled.message}
7383
+ The harness is NOT wired until you add it manually and restart the gateway.`,
7384
+ wired: false,
7385
+ error: true
7386
+ };
7387
+ }
7388
+ const rebind = prev && prev !== input.identityId ? `
7389
+ rebind: ${prev} \u2192 ${input.identityId}` : "";
7390
+ return {
7391
+ echo: `[securecoms] Hermes connector installed and bound to identity ${input.identityId}.${rebind}
7392
+ ${enabled.message}
7393
+ Restart the Hermes gateway (\`launchctl kickstart -k gui/$UID/ai.hermes.gateway\` or your service manager) after install completes.`,
7394
+ wired: true,
7395
+ error: false
7396
+ };
7397
+ } catch (e) {
7398
+ return {
7399
+ echo: `[securecoms] the bridge service is installed, but wiring the Hermes connector FAILED: ${e.message}
7400
+ Retry after fixing the path: ${retry}`,
7401
+ wired: false,
7402
+ error: true
7403
+ };
7404
+ }
7405
+ }
7406
+ return { echo: `[securecoms] Custom harness \u2014 bring your own connector:
7407
+ ${customFacts(input)}`, wired: false, error: false };
7408
+ }
7409
+
7410
+ // src/service/install-prep.ts
7046
7411
  function assertReadableCredential(absPath, deps) {
7047
7412
  let raw;
7048
7413
  try {
@@ -7065,10 +7430,10 @@ function assertReadableCredential(absPath, deps) {
7065
7430
  return { deviceId: json2.device_id, identityId: json2.identity_id, issuedAt };
7066
7431
  }
7067
7432
  function durableDir(home, slug) {
7068
- return join8(home, `securecoms-agent-${slug}`);
7433
+ return join9(home, `securecoms-agent-${slug}`);
7069
7434
  }
7070
7435
  function durableCredentialPath(home, slug) {
7071
- return join8(durableDir(home, slug), `${slug}.json`);
7436
+ return join9(durableDir(home, slug), `${slug}.json`);
7072
7437
  }
7073
7438
  function takeCustody(args2, deps) {
7074
7439
  const dir = durableDir(args2.home, args2.slug);
@@ -7115,7 +7480,7 @@ function readSiblingDefs(dir, prefix, deps) {
7115
7480
  for (const n of names) {
7116
7481
  if (!n.startsWith(prefix)) continue;
7117
7482
  try {
7118
- out.push(deps.readFile(join8(dir, n)));
7483
+ out.push(deps.readFile(join9(dir, n)));
7119
7484
  } catch {
7120
7485
  }
7121
7486
  }
@@ -7147,15 +7512,6 @@ async function verifyDaemonUp(args2, deps) {
7147
7512
  }
7148
7513
  return false;
7149
7514
  }
7150
- function harnessSnippet(args2) {
7151
- const label = args2.harness === "hermes" ? "Hermes" : args2.harness === "openclaw" ? "OpenClaw" : "your harness";
7152
- return [
7153
- `# ${label}: optional daemon-supervised path. Prefer 'agent-bridge wire' + the connection file.`,
7154
- `SECURECOMS_BRIDGE_URL=ws://127.0.0.1:${args2.port}/v1/ws`,
7155
- `SECURECOMS_CONNECTION_FILE=~/.securecoms/connections/${args2.identityId}.json`,
7156
- `SECURECOMS_BRIDGE_TOKEN_FILE=${args2.tokenFilePath}`
7157
- ].join("\n");
7158
- }
7159
7515
  async function orchestrateInstall(ctx) {
7160
7516
  const parsed = assertReadableCredential(ctx.credentialAbsPath, ctx.prepDeps);
7161
7517
  const stemForSlug = ctx.label ?? ctx.credentialAbsPath.replace(/^.*[\\/]/, "").replace(/\.[^.]*$/, "");
@@ -7189,22 +7545,24 @@ async function orchestrateInstall(ctx) {
7189
7545
  ` token: ${result.tokenHint}`,
7190
7546
  ` (Re-running install is the only way to update a live credential \u2014 replacing the download alone does nothing.)`
7191
7547
  ];
7192
- let harnessSnippetShown = false;
7193
- if (ctx.harness && ctx.harness !== "none") {
7194
- lines.push(
7195
- "",
7196
- `Primary: run 'agent-bridge wire --identity ${slug}' to attach a harness; the daemon writes ~/.securecoms/connections/${parsed.identityId}.json on startup.`,
7197
- "",
7198
- "Optional (daemon-supervised):",
7199
- harnessSnippet({ slug, port, tokenFilePath: result.tokenHint, harness: ctx.harness, identityId: parsed.identityId })
7200
- );
7201
- harnessSnippetShown = true;
7202
- }
7203
- return { echo: lines.join("\n") + "\n", harnessSnippetShown };
7548
+ const wire = autoWireHarness(
7549
+ {
7550
+ harness: ctx.harness,
7551
+ identityId: parsed.identityId,
7552
+ slug,
7553
+ port,
7554
+ home: ctx.home,
7555
+ tokenFilePath: result.tokenHint,
7556
+ connectorsSrcDir: ctx.connectorsSrcDir
7557
+ },
7558
+ ctx.autoWireDeps
7559
+ );
7560
+ if (wire.echo) lines.push("", wire.echo);
7561
+ return { echo: lines.join("\n") + "\n", harnessWired: wire.wired, harnessError: wire.error };
7204
7562
  }
7205
7563
 
7206
7564
  // src/service/fleet.ts
7207
- import { dirname as dirname5 } from "node:path";
7565
+ import { dirname as dirname6 } from "node:path";
7208
7566
  function parseArgFromDef(def, flag) {
7209
7567
  const esc = flag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7210
7568
  const re = new RegExp(`${esc}(?:<\\/string>\\s*<string>|\\s+)([^<\\s]+)`);
@@ -7248,7 +7606,7 @@ function readFleet(deps) {
7248
7606
  let identityId = `unknown:${slug}`, deviceId = "", label = slug, env = "unknown";
7249
7607
  let credDir = null;
7250
7608
  if (credPath) {
7251
- credDir = dirname5(credPath);
7609
+ credDir = dirname6(credPath);
7252
7610
  try {
7253
7611
  const c = JSON.parse(deps.readFile(credPath));
7254
7612
  if (c.identity_id) identityId = c.identity_id;
@@ -7314,81 +7672,6 @@ function renderStatusTable(rows) {
7314
7672
  return [fmt(head), ...body.map(fmt)].join("\n") + "\n";
7315
7673
  }
7316
7674
 
7317
- // src/service/wire.ts
7318
- import { join as join9, dirname as dirname6 } from "node:path";
7319
- import { fileURLToPath } from "node:url";
7320
- function detectHarness(home, statDir) {
7321
- if (statDir(`${home}/.openclaw`)) return "openclaw";
7322
- if (statDir(`${home}/.hermes`)) return "hermes";
7323
- return null;
7324
- }
7325
- function connectorTargetDir(home, harness) {
7326
- if (harness === "openclaw") return `${home}/.openclaw/local-plugins/securecoms`;
7327
- return `${home}/.hermes/local-plugins/securecoms`;
7328
- }
7329
- function connectorsSrcDir(importMetaUrl) {
7330
- const pkgRoot = dirname6(dirname6(fileURLToPath(importMetaUrl)));
7331
- return join9(pkgRoot, "connectors", "openclaw");
7332
- }
7333
- function parseArg(argv, flag) {
7334
- const i = argv.indexOf(flag);
7335
- return i >= 0 ? argv[i + 1] : void 0;
7336
- }
7337
- function resolveEntry(deps) {
7338
- const identityArg = parseArg(deps.argv, "--identity");
7339
- if (!identityArg) {
7340
- deps.log("wire: missing --identity <id|slug>");
7341
- return null;
7342
- }
7343
- const entry = deps.readFleet().find((e) => e.identityId === identityArg || e.slug === identityArg);
7344
- if (!entry) {
7345
- deps.log(`wire: unknown identity "${identityArg}" (not found in the local fleet)`);
7346
- return null;
7347
- }
7348
- const harnessArg = parseArg(deps.argv, "--harness");
7349
- if (harnessArg !== "openclaw" && harnessArg !== "hermes") {
7350
- deps.log("wire: missing/invalid --harness <openclaw|hermes> (auto-detection did not resolve one)");
7351
- return null;
7352
- }
7353
- return { entry, harness: harnessArg };
7354
- }
7355
- function runWire(deps) {
7356
- const resolved = resolveEntry(deps);
7357
- if (!resolved) return 1;
7358
- const { entry, harness } = resolved;
7359
- if (harness === "hermes") {
7360
- deps.log(
7361
- "[securecoms] Hermes connector is not available yet (fast-follow); detection and target dir only. Nothing installed."
7362
- );
7363
- return 1;
7364
- }
7365
- const targetDir = connectorTargetDir(deps.home, harness);
7366
- deps.copyConnector(deps.connectorsSrcDir, targetDir);
7367
- deps.writeBinding(targetDir, entry.identityId);
7368
- if (typeof entry.port === "number") {
7369
- const h = deps.getHealth(entry.port);
7370
- if (h && (h.agent_sessions ?? 0) >= 1) {
7371
- deps.log(`wire: ${entry.slug} attached (agent_sessions=${h.agent_sessions})`);
7372
- } else {
7373
- deps.log(`wire: connector installed but not yet attached \u2014 restart the harness to pick it up.`);
7374
- }
7375
- } else {
7376
- deps.log(`wire: connector installed (no bridge port known yet) \u2014 restart the harness to pick it up.`);
7377
- }
7378
- return 0;
7379
- }
7380
- function runUnwire(deps) {
7381
- const resolved = resolveEntry(deps);
7382
- if (!resolved) return 1;
7383
- const { harness } = resolved;
7384
- if (harness === "hermes") {
7385
- deps.log("[securecoms] Hermes connector is not available yet (fast-follow); nothing to remove.");
7386
- return 0;
7387
- }
7388
- deps.removeBinding(connectorTargetDir(deps.home, harness));
7389
- return 0;
7390
- }
7391
-
7392
7675
  // src/service/connection-file.ts
7393
7676
  import {
7394
7677
  chmodSync,
@@ -7606,15 +7889,18 @@ function runStatus() {
7606
7889
  else process.stdout.write(renderStatusTable(rows));
7607
7890
  process.exit(0);
7608
7891
  }
7609
- function realCopyConnector(srcDir, dstDir) {
7892
+ function realCopyConnector(srcDir, dstDir, harness) {
7893
+ const allowedExt = CONNECTOR_FILE_EXTENSIONS[harness];
7610
7894
  mkdirSync2(dstDir, { recursive: true });
7611
7895
  for (const name of readdirSync(srcDir)) {
7612
7896
  const srcPath = join11(srcDir, name);
7613
7897
  if (statSync2(srcPath).isDirectory()) {
7614
- realCopyConnector(srcPath, join11(dstDir, name));
7898
+ if (CONNECTOR_SKIP_DIRS.has(name)) continue;
7899
+ realCopyConnector(srcPath, join11(dstDir, name), harness);
7615
7900
  continue;
7616
7901
  }
7617
- if (!/\.(js|json)$/.test(name)) continue;
7902
+ if (name.includes(".test.")) continue;
7903
+ if (!allowedExt.some((ext) => name.endsWith(ext))) continue;
7618
7904
  writeFileSync3(join11(dstDir, name), readFileSync7(srcPath));
7619
7905
  }
7620
7906
  }
@@ -7622,6 +7908,14 @@ function realWriteBinding(dstDir, identityId) {
7622
7908
  mkdirSync2(dstDir, { recursive: true });
7623
7909
  writeFileSync3(join11(dstDir, "securecoms.binding.json"), JSON.stringify({ identity_id: identityId }) + "\n", { mode: 384 });
7624
7910
  }
7911
+ function realReadBinding(dstDir) {
7912
+ try {
7913
+ const parsed = JSON.parse(readFileSync7(join11(dstDir, "securecoms.binding.json"), "utf8"));
7914
+ return parsed.identity_id ?? null;
7915
+ } catch {
7916
+ return null;
7917
+ }
7918
+ }
7625
7919
  function realRemoveBinding(dstDir) {
7626
7920
  rmSync(join11(dstDir, "securecoms.binding.json"), { force: true });
7627
7921
  }
@@ -7632,6 +7926,19 @@ function realStatDir(p) {
7632
7926
  return false;
7633
7927
  }
7634
7928
  }
7929
+ function realReadHermesConfig(p) {
7930
+ try {
7931
+ return readFileSync7(p, "utf8");
7932
+ } catch {
7933
+ return null;
7934
+ }
7935
+ }
7936
+ function realBackupHermesConfig(p, originalText) {
7937
+ writeFileSync3(hermesConfigBackupPath(p, Date.now(), process.pid), originalText, "utf8");
7938
+ }
7939
+ function realWriteHermesConfig(p, newText) {
7940
+ writeFileSync3(p, newText, "utf8");
7941
+ }
7635
7942
  function realWireArgv() {
7636
7943
  if (process.argv.includes("--harness")) return process.argv;
7637
7944
  const detected = detectHarness(homedir2(), realStatDir);
@@ -7648,8 +7955,12 @@ function realWireDeps() {
7648
7955
  getHealth: realGetHealth,
7649
7956
  log: (s) => process.stdout.write(s + "\n"),
7650
7957
  // bin.ts's OWN import.meta.url — see connectorsSrcDir's doc comment in
7651
- // wire.ts for why it must be the entry point's URL, not wire.ts's.
7652
- connectorsSrcDir: connectorsSrcDir(import.meta.url)
7958
+ // wire.ts for why it must be the entry point's URL, not wire.ts's. Keyed
7959
+ // by harness (Task 6) so one WireDeps instance can wire either harness.
7960
+ connectorsSrcDir: (h) => connectorsSrcDir(import.meta.url, h),
7961
+ readHermesConfig: realReadHermesConfig,
7962
+ backupHermesConfig: realBackupHermesConfig,
7963
+ writeHermesConfig: realWriteHermesConfig
7653
7964
  };
7654
7965
  }
7655
7966
  function runWireVerb(verb) {
@@ -7678,12 +7989,27 @@ async function runServiceVerb(verb) {
7678
7989
  argOrEnv("--exec-path", "SECURECOMS_EXEC_PATH"),
7679
7990
  realResolveNodeDeps()
7680
7991
  );
7992
+ const installHarness = argOrEnv("--harness", "SECURECOMS_HARNESS");
7681
7993
  const r = await orchestrateInstall({
7682
7994
  credentialAbsPath: credAbs,
7683
7995
  label,
7684
7996
  explicitPort: portStr ? Number(portStr) : void 0,
7685
- harness: argOrEnv("--harness", "SECURECOMS_HARNESS"),
7997
+ harness: installHarness,
7686
7998
  home: homedir2(),
7999
+ connectorsSrcDir: connectorsSrcDir(import.meta.url, installHarness === "hermes" ? "hermes" : "openclaw"),
8000
+ autoWireDeps: {
8001
+ statDir: realStatDir,
8002
+ // reuse bin.ts:266
8003
+ readBinding: realReadBinding,
8004
+ copyConnector: realCopyConnector,
8005
+ // bin.ts:225 — one copy impl
8006
+ writeBinding: realWriteBinding,
8007
+ // bin.ts:245 — writes {identity_id}
8008
+ readHermesConfig: realReadHermesConfig,
8009
+ backupHermesConfig: realBackupHermesConfig,
8010
+ writeHermesConfig: realWriteHermesConfig,
8011
+ log: (s) => process.stderr.write(s + "\n")
8012
+ },
7687
8013
  prepDeps: realPrepDeps(),
7688
8014
  // O-1: the real installService is 2-arg; partially apply the deps so
7689
8015
  // orchestrateInstall only ever sees the 1-arg seam.
@@ -7701,6 +8027,7 @@ async function runServiceVerb(verb) {
7701
8027
  })
7702
8028
  });
7703
8029
  process.stdout.write(r.echo);
8030
+ if (r.harnessError) process.exit(1);
7704
8031
  } else {
7705
8032
  const input = {
7706
8033
  credentialPath: credentialPath ?? "",
@@ -7827,6 +8154,7 @@ export {
7827
8154
  chooseInstallExecPath,
7828
8155
  nodeMajorOk,
7829
8156
  parseVerb,
8157
+ realCopyConnector,
7830
8158
  slugFromCredentialPath,
7831
8159
  tokenFingerprint
7832
8160
  };
@@ -0,0 +1,22 @@
1
+ import { type HermesConfigIoDeps, type Harness } from "./wire.js";
2
+ export interface AutoWireInput {
3
+ harness: string | undefined;
4
+ identityId: string;
5
+ slug: string;
6
+ port: number | null;
7
+ home: string;
8
+ tokenFilePath: string;
9
+ connectorsSrcDir: string;
10
+ }
11
+ export interface AutoWireDeps extends HermesConfigIoDeps {
12
+ statDir: (p: string) => boolean;
13
+ readBinding: (dstDir: string) => string | null;
14
+ copyConnector: (srcDir: string, dstDir: string, harness: Harness) => void;
15
+ writeBinding: (dstDir: string, identityId: string) => void;
16
+ log: (s: string) => void;
17
+ }
18
+ export declare function autoWireHarness(input: AutoWireInput, deps: AutoWireDeps): {
19
+ echo: string;
20
+ wired: boolean;
21
+ error: boolean;
22
+ };