@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.
- 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 +285 -27
- package/dist/service/harness-wire.d.ts +3 -2
- package/dist/service/hermes-config.d.ts +51 -0
- package/dist/service/wire.d.ts +97 -40
- 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/
|
|
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",
|
|
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(
|
|
7116
|
-
|
|
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
|
-
|
|
7171
|
-
|
|
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
|
-
|
|
7898
|
+
if (CONNECTOR_SKIP_DIRS.has(name)) continue;
|
|
7899
|
+
realCopyConnector(srcPath, join11(dstDir, name), harness);
|
|
7665
7900
|
continue;
|
|
7666
7901
|
}
|
|
7667
|
-
if (
|
|
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
|
-
|
|
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:
|
|
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:
|
|
8002
|
+
// reuse bin.ts:266
|
|
7749
8003
|
readBinding: realReadBinding,
|
|
7750
8004
|
copyConnector: realCopyConnector,
|
|
7751
|
-
// bin.ts:
|
|
8005
|
+
// bin.ts:225 — one copy impl
|
|
7752
8006
|
writeBinding: realWriteBinding,
|
|
7753
|
-
// bin.ts:
|
|
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;
|
package/dist/service/wire.d.ts
CHANGED
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
import type { AgentFleetEntry } from "./fleet.js";
|
|
2
2
|
export type Harness = "openclaw" | "hermes";
|
|
3
|
-
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* (
|
|
23
|
-
*
|
|
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.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
|
48
|
-
* shipped SOURCE (not compiled — H2), so this
|
|
49
|
-
* file's location back to the package root,
|
|
50
|
-
* their OWN import.meta.url (bin.ts's), not
|
|
51
|
-
* wire.ts's code into dist/bin.js, so only the
|
|
52
|
-
* is guaranteed to sit exactly one directory
|
|
53
|
-
* dev (src/bin.ts) and the built artifact
|
|
54
|
-
* resolve-bin.ts's loadPackageMeta (`dist/bin.js ->
|
|
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
|
|
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
|
|
64
|
-
* `
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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
|
|
71
|
-
* the plugin (that stays
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
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;
|