@securecoms/agent-bridge 0.7.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 (29) 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 +285 -27
  26. package/dist/service/harness-wire.d.ts +3 -2
  27. package/dist/service/hermes-config.d.ts +51 -0
  28. package/dist/service/wire.d.ts +97 -40
  29. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -7047,6 +7047,159 @@ import { join as join9 } from "node:path";
7047
7047
  // src/service/wire.ts
7048
7048
  import { join as join8, dirname as dirname5 } from "node:path";
7049
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__"]);
7050
7203
  function detectHarness(home, statDir) {
7051
7204
  if (statDir(`${home}/.openclaw`)) return "openclaw";
7052
7205
  if (statDir(`${home}/.hermes`)) return "hermes";
@@ -7054,11 +7207,41 @@ function detectHarness(home, statDir) {
7054
7207
  }
7055
7208
  function connectorTargetDir(home, harness) {
7056
7209
  if (harness === "openclaw") return `${home}/.openclaw/local-plugins/securecoms`;
7057
- return `${home}/.hermes/local-plugins/securecoms`;
7210
+ return `${home}/.hermes/plugins/securecoms`;
7058
7211
  }
7059
- function connectorsSrcDir(importMetaUrl) {
7212
+ function connectorsSrcDir(importMetaUrl, harness) {
7060
7213
  const pkgRoot = dirname5(dirname5(fileURLToPath(importMetaUrl)));
7061
- return join8(pkgRoot, "connectors", "openclaw");
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
+ };
7062
7245
  }
7063
7246
  function parseArg(argv, flag) {
7064
7247
  const i = argv.indexOf(flag);
@@ -7086,15 +7269,19 @@ function runWire(deps) {
7086
7269
  const resolved = resolveEntry(deps);
7087
7270
  if (!resolved) return 1;
7088
7271
  const { entry, harness } = resolved;
7089
- if (harness === "hermes") {
7090
- deps.log(
7091
- "[securecoms] Hermes connector is not available yet (fast-follow); detection and target dir only. Nothing installed."
7092
- );
7093
- return 1;
7094
- }
7095
7272
  const targetDir = connectorTargetDir(deps.home, harness);
7096
- deps.copyConnector(deps.connectorsSrcDir, targetDir);
7273
+ deps.copyConnector(deps.connectorsSrcDir(harness), targetDir, harness);
7097
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
+ }
7098
7285
  if (typeof entry.port === "number") {
7099
7286
  const h = deps.getHealth(entry.port);
7100
7287
  if (h && (h.agent_sessions ?? 0) >= 1) {
@@ -7105,17 +7292,23 @@ function runWire(deps) {
7105
7292
  } else {
7106
7293
  deps.log(`wire: connector installed (no bridge port known yet) \u2014 restart the harness to pick it up.`);
7107
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
+ }
7108
7300
  return 0;
7109
7301
  }
7110
7302
  function runUnwire(deps) {
7111
7303
  const resolved = resolveEntry(deps);
7112
7304
  if (!resolved) return 1;
7113
7305
  const { harness } = resolved;
7306
+ deps.removeBinding(connectorTargetDir(deps.home, harness));
7114
7307
  if (harness === "hermes") {
7115
- deps.log("[securecoms] Hermes connector is not available yet (fast-follow); nothing to remove.");
7116
- return 0;
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
+ );
7117
7311
  }
7118
- deps.removeBinding(connectorTargetDir(deps.home, harness));
7119
7312
  return 0;
7120
7313
  }
7121
7314
 
@@ -7132,8 +7325,8 @@ function customFacts(input) {
7132
7325
  function autoWireHarness(input, deps) {
7133
7326
  const h = input.harness;
7134
7327
  if (!h || h === "none") return { echo: "", wired: false, error: false };
7135
- const retry = `agent-bridge wire --harness openclaw --identity ${input.slug}`;
7136
7328
  if (h === "openclaw") {
7329
+ const retry = `agent-bridge wire --harness openclaw --identity ${input.slug}`;
7137
7330
  const parent = `${input.home}/.openclaw`;
7138
7331
  if (!deps.statDir(parent)) {
7139
7332
  return {
@@ -7147,7 +7340,7 @@ function autoWireHarness(input, deps) {
7147
7340
  const targetDir = connectorTargetDir(input.home, "openclaw");
7148
7341
  try {
7149
7342
  const prev = deps.readBinding(targetDir);
7150
- deps.copyConnector(input.connectorsSrcDir, targetDir);
7343
+ deps.copyConnector(input.connectorsSrcDir, targetDir, "openclaw");
7151
7344
  deps.writeBinding(targetDir, input.identityId);
7152
7345
  const rebind = prev && prev !== input.identityId ? `
7153
7346
  rebind: ${prev} \u2192 ${input.identityId}` : "";
@@ -7167,8 +7360,48 @@ function autoWireHarness(input, deps) {
7167
7360
  }
7168
7361
  }
7169
7362
  if (h === "hermes") {
7170
- return { echo: `[securecoms] Hermes turnkey connector is coming (Phase 2). For now drive Hermes via the custom WS path:
7171
- ${customFacts(input)}`, wired: false, error: false };
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
+ }
7172
7405
  }
7173
7406
  return { echo: `[securecoms] Custom harness \u2014 bring your own connector:
7174
7407
  ${customFacts(input)}`, wired: false, error: false };
@@ -7656,15 +7889,18 @@ function runStatus() {
7656
7889
  else process.stdout.write(renderStatusTable(rows));
7657
7890
  process.exit(0);
7658
7891
  }
7659
- function realCopyConnector(srcDir, dstDir) {
7892
+ function realCopyConnector(srcDir, dstDir, harness) {
7893
+ const allowedExt = CONNECTOR_FILE_EXTENSIONS[harness];
7660
7894
  mkdirSync2(dstDir, { recursive: true });
7661
7895
  for (const name of readdirSync(srcDir)) {
7662
7896
  const srcPath = join11(srcDir, name);
7663
7897
  if (statSync2(srcPath).isDirectory()) {
7664
- realCopyConnector(srcPath, join11(dstDir, name));
7898
+ if (CONNECTOR_SKIP_DIRS.has(name)) continue;
7899
+ realCopyConnector(srcPath, join11(dstDir, name), harness);
7665
7900
  continue;
7666
7901
  }
7667
- if (!/\.(js|json)$/.test(name)) continue;
7902
+ if (name.includes(".test.")) continue;
7903
+ if (!allowedExt.some((ext) => name.endsWith(ext))) continue;
7668
7904
  writeFileSync3(join11(dstDir, name), readFileSync7(srcPath));
7669
7905
  }
7670
7906
  }
@@ -7690,6 +7926,19 @@ function realStatDir(p) {
7690
7926
  return false;
7691
7927
  }
7692
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
+ }
7693
7942
  function realWireArgv() {
7694
7943
  if (process.argv.includes("--harness")) return process.argv;
7695
7944
  const detected = detectHarness(homedir2(), realStatDir);
@@ -7706,8 +7955,12 @@ function realWireDeps() {
7706
7955
  getHealth: realGetHealth,
7707
7956
  log: (s) => process.stdout.write(s + "\n"),
7708
7957
  // bin.ts's OWN import.meta.url — see connectorsSrcDir's doc comment in
7709
- // wire.ts for why it must be the entry point's URL, not wire.ts's.
7710
- 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
7711
7964
  };
7712
7965
  }
7713
7966
  function runWireVerb(verb) {
@@ -7736,21 +7989,25 @@ async function runServiceVerb(verb) {
7736
7989
  argOrEnv("--exec-path", "SECURECOMS_EXEC_PATH"),
7737
7990
  realResolveNodeDeps()
7738
7991
  );
7992
+ const installHarness = argOrEnv("--harness", "SECURECOMS_HARNESS");
7739
7993
  const r = await orchestrateInstall({
7740
7994
  credentialAbsPath: credAbs,
7741
7995
  label,
7742
7996
  explicitPort: portStr ? Number(portStr) : void 0,
7743
- harness: argOrEnv("--harness", "SECURECOMS_HARNESS"),
7997
+ harness: installHarness,
7744
7998
  home: homedir2(),
7745
- connectorsSrcDir: connectorsSrcDir(import.meta.url),
7999
+ connectorsSrcDir: connectorsSrcDir(import.meta.url, installHarness === "hermes" ? "hermes" : "openclaw"),
7746
8000
  autoWireDeps: {
7747
8001
  statDir: realStatDir,
7748
- // reuse bin.ts:235
8002
+ // reuse bin.ts:266
7749
8003
  readBinding: realReadBinding,
7750
8004
  copyConnector: realCopyConnector,
7751
- // bin.ts:212 — one copy impl
8005
+ // bin.ts:225 — one copy impl
7752
8006
  writeBinding: realWriteBinding,
7753
- // bin.ts:226 — writes {identity_id}
8007
+ // bin.ts:245 — writes {identity_id}
8008
+ readHermesConfig: realReadHermesConfig,
8009
+ backupHermesConfig: realBackupHermesConfig,
8010
+ writeHermesConfig: realWriteHermesConfig,
7754
8011
  log: (s) => process.stderr.write(s + "\n")
7755
8012
  },
7756
8013
  prepDeps: realPrepDeps(),
@@ -7897,6 +8154,7 @@ export {
7897
8154
  chooseInstallExecPath,
7898
8155
  nodeMajorOk,
7899
8156
  parseVerb,
8157
+ realCopyConnector,
7900
8158
  slugFromCredentialPath,
7901
8159
  tokenFingerprint
7902
8160
  };
@@ -1,3 +1,4 @@
1
+ import { type HermesConfigIoDeps, type Harness } from "./wire.js";
1
2
  export interface AutoWireInput {
2
3
  harness: string | undefined;
3
4
  identityId: string;
@@ -7,10 +8,10 @@ export interface AutoWireInput {
7
8
  tokenFilePath: string;
8
9
  connectorsSrcDir: string;
9
10
  }
10
- export interface AutoWireDeps {
11
+ export interface AutoWireDeps extends HermesConfigIoDeps {
11
12
  statDir: (p: string) => boolean;
12
13
  readBinding: (dstDir: string) => string | null;
13
- copyConnector: (srcDir: string, dstDir: string) => void;
14
+ copyConnector: (srcDir: string, dstDir: string, harness: Harness) => void;
14
15
  writeBinding: (dstDir: string, identityId: string) => void;
15
16
  log: (s: string) => void;
16
17
  }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Strict, line-based editor for `~/.hermes/config.yaml`'s `plugins.enabled`
3
+ * block sequence. Hermes only loads user plugins listed there (plugins.py's
4
+ * loader — see spec §4), so `wire --harness hermes` must add our plugin's
5
+ * entry or the copied connector never runs.
6
+ *
7
+ * This module is PURE: it takes the file's text and returns either a noop
8
+ * (already correct / refused) or the edited text. File I/O and the
9
+ * `config.yaml.bak-wire-<ts>-<pid>` backup live with the caller (wire.ts /
10
+ * harness-wire.ts, Task 6).
11
+ *
12
+ * Hardened rules (spec §4, audited against the real production file at
13
+ * `config.yaml:569-573`):
14
+ * 1. The inserted item copies the EXISTING first item's exact column —
15
+ * never `key-indent + 2`. Production shape has items at the SAME
16
+ * indent as the `enabled:` key itself (a legal, if unusual, YAML block
17
+ * sequence style); some configs indent items by +2. Both must work,
18
+ * which is why the column is READ from the first item, never computed.
19
+ * 2. Insertion lands after the LAST consecutive same-column `- ` item —
20
+ * stop scanning at the first line that isn't one (the boundary before
21
+ * a sibling key such as `disabled: []`).
22
+ * 2b. Stopping the scan is not enough on its own: the line the scan
23
+ * stopped at must be a LEGITIMATE list terminator before the insertion
24
+ * is allowed to happen there. Legitimate means one of: (a) a sibling
25
+ * key at the same column as `enabled:` itself (e.g. `disabled:`), (b)
26
+ * end-of-block via blank-line(s) then a genuine dedent below the
27
+ * items' column, or (c) end of file. Anything else that stops the
28
+ * scan — a bare `-`, a mapping-style item (`- ` followed by an
29
+ * indented `key: value` on the next line), a blank line followed by
30
+ * MORE same-column items (ambiguous: did the writer mean to end the
31
+ * list or just add a spacer?), or any other unrecognized content —
32
+ * refuses rather than insert at a spot that was never verified to be
33
+ * the list's actual end.
34
+ * 3. `plugins.disabled` is checked too: an entry listed there wins over
35
+ * `plugins.enabled` in the loader, so refuse rather than add a second,
36
+ * shadowed entry.
37
+ * 4. Any structural surprise — no `plugins:` block, an inline
38
+ * `enabled: [...]` form, an empty enabled list, mixed-indent items, a
39
+ * comment line interleaved with items (ambiguous: it could be a
40
+ * trailing comment on the item above or a leading comment on the item
41
+ * below), or a non-boundary list terminator (see 2b) — refuses with no
42
+ * write and an actionable message. Never reformats, never touches any
43
+ * other key.
44
+ */
45
+ export declare const HERMES_PLUGIN_NAME = "securecoms-platform";
46
+ export interface EnsureHermesPluginEnabledResult {
47
+ action: "noop" | "edited";
48
+ text?: string;
49
+ reason?: string;
50
+ }
51
+ export declare function ensureHermesPluginEnabled(configText: string): EnsureHermesPluginEnabledResult;
@@ -1,10 +1,43 @@
1
1
  import type { AgentFleetEntry } from "./fleet.js";
2
2
  export type Harness = "openclaw" | "hermes";
3
- export interface WireDeps {
3
+ /**
4
+ * Per-harness allow-list of file extensions the copier includes when it
5
+ * installs a connector — openclaw ships a JS runtime, hermes ships Python
6
+ * (F8: a single `.js|.json` filter silently produced an EMPTY hermes install,
7
+ * since none of adapter.py/securecoms_transport.py/securecoms_gate.py/
8
+ * plugin.yaml/__init__.py matched it).
9
+ */
10
+ export declare const CONNECTOR_FILE_EXTENSIONS: Record<Harness, readonly string[]>;
11
+ /**
12
+ * Directory names the copier never descends into, checked AT THE DIRECTORY
13
+ * BRANCH before recursion (F8) — a filename-only extension filter still
14
+ * walks into `tests/` and would copy every `*.py` it finds there, since `.py`
15
+ * is itself an allow-listed hermes extension. `__pycache__` is compiled
16
+ * bytecode, never source, for either harness.
17
+ */
18
+ export declare const CONNECTOR_SKIP_DIRS: ReadonlySet<string>;
19
+ /** The config read/backup/write seams the Hermes `plugins.enabled` auto-edit
20
+ * needs (spec §4, F4). Shared between `WireDeps` (the `wire` verb) and
21
+ * `AutoWireDeps` (the `install` auto-wire path) so `enableHermesConfig`
22
+ * below is the ONE implementation both paths call — never duplicated,
23
+ * never skippable on just one of the two. Unused for the openclaw harness,
24
+ * which has no config file to edit. */
25
+ export interface HermesConfigIoDeps {
26
+ /** Returns null when the file (or its parent ~/.hermes) doesn't exist or
27
+ * isn't readable — treated as a config-edit refusal, same posture as any
28
+ * other structural surprise `ensureHermesPluginEnabled` reports. */
29
+ readHermesConfig: (path: string) => string | null;
30
+ /** Persists the ORIGINAL text under a `hermesConfigBackupPath` name,
31
+ * called BEFORE `writeHermesConfig` — only invoked when an edit is about
32
+ * to happen, never on a noop/refusal. */
33
+ backupHermesConfig: (path: string, originalText: string) => void;
34
+ writeHermesConfig: (path: string, newText: string) => void;
35
+ }
36
+ export interface WireDeps extends HermesConfigIoDeps {
4
37
  home: string;
5
38
  argv: string[];
6
39
  readFleet: () => AgentFleetEntry[];
7
- copyConnector: (srcDir: string, dstDir: string) => void;
40
+ copyConnector: (srcDir: string, dstDir: string, harness: Harness) => void;
8
41
  writeBinding: (dstDir: string, identityId: string) => void;
9
42
  removeBinding: (dstDir: string) => void;
10
43
  getHealth: (port: number) => {
@@ -12,20 +45,17 @@ export interface WireDeps {
12
45
  } | null;
13
46
  log: (s: string) => void;
14
47
  /**
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).
48
+ * Resolves a harness's connector SOURCE dir on demand, keyed by harness —
49
+ * Task 6 makes BOTH openclaw and hermes real installs, so a single
50
+ * WireDeps instance must be able to wire either one. Real callers
51
+ * (bin.ts's realWireDeps) supply `(h) => connectorsSrcDir(import.meta.url,
52
+ * h)` using bin.ts's OWN import.meta.url — see the standalone
53
+ * `connectorsSrcDir` function's doc comment below for why it has to be the
54
+ * entry point's URL, not wire.ts's own. Unit tests must pass a function
55
+ * too (any string-returning stub is fine — `copyConnector` is mocked in
56
+ * tests and never inspects the path for its own sake).
27
57
  */
28
- connectorsSrcDir: string;
58
+ connectorsSrcDir: (harness: Harness) => string;
29
59
  }
30
60
  /**
31
61
  * Detects an installed harness by probing well-known home-relative dirs.
@@ -35,42 +65,69 @@ export interface WireDeps {
35
65
  */
36
66
  export declare function detectHarness(home: string, statDir: (p: string) => boolean): Harness | null;
37
67
  /**
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.
68
+ * Where the connector gets installed for a given harness. Both harnesses are
69
+ * real installs as of Task 6: openclaw at its `local-plugins` dir; hermes at
70
+ * `~/.hermes/plugins/securecoms` — the loader's actual user-plugins dir
71
+ * (verified against the production config; `local-plugins` was a Task 4/5
72
+ * placeholder value, never the real one).
44
73
  */
45
74
  export declare function connectorTargetDir(home: string, harness: Harness): string;
46
75
  /**
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`).
76
+ * Resolves a connector's SOURCE dir at runtime, keyed by harness (`connectors/
77
+ * <harness>`) — `connectors/` is shipped SOURCE (not compiled — H2), so this
78
+ * must resolve from the running file's location back to the package root,
79
+ * never into dist/. Callers pass their OWN import.meta.url (bin.ts's), not
80
+ * wire.ts's: esbuild bundles wire.ts's code into dist/bin.js, so only the
81
+ * entry point's import.meta.url is guaranteed to sit exactly one directory
82
+ * below the package root in BOTH dev (src/bin.ts) and the built artifact
83
+ * (dist/bin.js) — mirrors resolve-bin.ts's loadPackageMeta (`dist/bin.js ->
84
+ * ../package.json`).
85
+ */
86
+ export declare function connectorsSrcDir(importMetaUrl: string, harness: Harness): string;
87
+ /** Path to the Hermes gateway's `config.yaml`, whose `plugins.enabled` block
88
+ * the auto-edit targets (spec §4). v1 targets the default `~/.hermes`
89
+ * profile only — `HERMES_HOME`/secondary profiles are documented as
90
+ * unsupported by `wire` in this round (spec §4 scope limit). */
91
+ export declare function hermesConfigPath(home: string): string;
92
+ /** Backup filename for a config.yaml auto-edit: `<path>.bak-wire-<ts>-<pid>`
93
+ * (spec §4 rule 4) — unique enough per host that two `wire` runs essentially
94
+ * never collide (different timestamp, or different pid at the same ms). */
95
+ export declare function hermesConfigBackupPath(configPath: string, ts: number, pid: number): string;
96
+ /**
97
+ * F4 (load-bearing): the ONE enable helper invoked from BOTH `runWire` (the
98
+ * `wire` verb, below) and `autoWireHarness` (the `install` auto-wire path,
99
+ * harness-wire.ts) — a hermes wire that copies the connector and writes the
100
+ * binding but skips this step is the silent half-wire the spec forbids.
101
+ * Reads config.yaml, runs Task 5's pure `ensureHermesPluginEnabled`, and on
102
+ * an actual edit backs up the ORIGINAL text before writing the new text.
103
+ * Never writes anything on a noop or a refusal.
55
104
  */
56
- export declare function connectorsSrcDir(importMetaUrl: string): string;
105
+ export declare function enableHermesConfig(home: string, deps: HermesConfigIoDeps): {
106
+ ok: boolean;
107
+ message: string;
108
+ };
57
109
  /**
58
110
  * Installs the reference connector into the harness's plugin dir and drops
59
111
  * an explicit securecoms.binding.json naming the resolved agent's identity.
60
112
  * There is NO auto-discovery (audit C1): the binding is written for EVERY
61
113
  * agent, singleton included, every time `wire` runs.
62
114
  *
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.
115
+ * Hermes (Task 6) additionally needs its `~/.hermes/config.yaml`
116
+ * `plugins.enabled` entry (F4's `enableHermesConfig`, shared with
117
+ * autoWireHarness) — Hermes only loads plugins listed there, so the copy +
118
+ * binding alone would leave the connector inert. If that edit refuses (no
119
+ * `plugins:` block, an already-disabled entry, an ambiguous list shape, or a
120
+ * missing config.yaml entirely), the copy and binding REMAIN on disk — they
121
+ * are harmless without the enable — but `wire` exits non-zero with the
122
+ * manual-edit instructions, so callers see harness-NOT-wired rather than a
123
+ * silent half-wire.
67
124
  */
68
125
  export declare function runWire(deps: WireDeps): number;
69
126
  /** 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. */
127
+ * connector files (and, for hermes, the config.yaml plugin entry) in place
128
+ * — unwire un-names the agent, it doesn't uninstall the plugin (that stays
129
+ * a harness-native operation). For hermes specifically: removing the
130
+ * installed files while config.yaml still lists the plugin would make
131
+ * Hermes log load errors at boot, so the copy AND the config entry both
132
+ * deliberately remain — printed as explicit manual-cleanup instructions. */
76
133
  export declare function runUnwire(deps: WireDeps): number;