@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.
- package/README.md +2 -0
- package/connectors/hermes/__init__.py +60 -0
- package/connectors/hermes/__pycache__/__init__.cpython-311.pyc +0 -0
- package/connectors/hermes/__pycache__/adapter.cpython-311.pyc +0 -0
- package/connectors/hermes/__pycache__/gate.cpython-311.pyc +0 -0
- package/connectors/hermes/__pycache__/securecoms_gate.cpython-311.pyc +0 -0
- package/connectors/hermes/__pycache__/securecoms_transport.cpython-311.pyc +0 -0
- package/connectors/hermes/__pycache__/transport.cpython-311.pyc +0 -0
- package/connectors/hermes/adapter.py +470 -0
- package/connectors/hermes/plugin.yaml +25 -0
- package/connectors/hermes/pytest.ini +3 -0
- package/connectors/hermes/securecoms_gate.py +70 -0
- package/connectors/hermes/securecoms_transport.py +257 -0
- package/connectors/hermes/tests/__pycache__/conftest.cpython-311-pytest-8.4.0.pyc +0 -0
- package/connectors/hermes/tests/__pycache__/test_adapter_wiring.cpython-311-pytest-8.4.0.pyc +0 -0
- package/connectors/hermes/tests/__pycache__/test_gate.cpython-311-pytest-8.4.0.pyc +0 -0
- package/connectors/hermes/tests/__pycache__/test_securecoms_gate.cpython-311-pytest-8.4.0.pyc +0 -0
- package/connectors/hermes/tests/__pycache__/test_securecoms_transport.cpython-311-pytest-8.4.0.pyc +0 -0
- package/connectors/hermes/tests/__pycache__/test_transport.cpython-311-pytest-8.4.0.pyc +0 -0
- package/connectors/hermes/tests/conftest.py +23 -0
- package/connectors/hermes/tests/test_adapter_wiring.py +541 -0
- package/connectors/hermes/tests/test_securecoms_gate.py +113 -0
- package/connectors/hermes/tests/test_securecoms_transport.py +608 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +436 -108
- package/dist/service/harness-wire.d.ts +22 -0
- package/dist/service/hermes-config.d.ts +51 -0
- package/dist/service/install-prep.d.ts +5 -8
- package/dist/service/wire.d.ts +97 -40
- 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
|
|
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
|
|
7433
|
+
return join9(home, `securecoms-agent-${slug}`);
|
|
7069
7434
|
}
|
|
7070
7435
|
function durableCredentialPath(home, slug) {
|
|
7071
|
-
return
|
|
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(
|
|
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
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
7898
|
+
if (CONNECTOR_SKIP_DIRS.has(name)) continue;
|
|
7899
|
+
realCopyConnector(srcPath, join11(dstDir, name), harness);
|
|
7615
7900
|
continue;
|
|
7616
7901
|
}
|
|
7617
|
-
if (
|
|
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
|
-
|
|
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:
|
|
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
|
+
};
|