@securecoms/agent-bridge 0.15.0 → 0.16.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 +1 -0
- package/connectors/hermes/adapter.py +11 -1
- package/connectors/hermes/tests/test_adapter_wiring.py +33 -0
- package/dist/bin.js +45 -9
- package/dist/index.js +1 -3
- package/dist/service/harness-wire.d.ts +4 -0
- package/dist/service/install-prep.d.ts +1 -0
- package/dist/service/wire.d.ts +30 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -359,6 +359,7 @@ pm2 start "npx @agentvault/connect-agent-bridge --credential ./agent.json" \
|
|
|
359
359
|
- Install **verifies the daemon actually came up** and fails loudly with the log path if it didn't — no silent crash-loops.
|
|
360
360
|
- Install echoes the credential's `device_id`, `issued_at`, and a redacted api-key fingerprint so you can confirm you installed the newest download. **Re-running install is the only way to update a live (e.g. rotated) credential** — replacing the downloaded file alone does nothing.
|
|
361
361
|
- Optional `--harness openclaw|hermes|custom` prints a paste-ready config snippet with the chosen port and the token-file path (the token itself never appears in output).
|
|
362
|
+
- **One harness gateway serves one agent.** The connector registers a single platform and resolves a single bound identity for the life of the gateway process, so a connector directory (`~/.hermes/plugins/securecoms`, `~/.openclaw/local-plugins/securecoms`) holds exactly one agent. Install and `wire` therefore **refuse** to overwrite a binding that names a different agent — doing so would take the harness away from that agent, not add one alongside it. To run two agents on one host, give each its own harness profile with its own connector directory and binding. To deliberately move a harness to a different agent, pass `--rebind`.
|
|
362
363
|
|
|
363
364
|
---
|
|
364
365
|
|
|
@@ -132,7 +132,17 @@ class SecureComsAdapter(BasePlatformAdapter):
|
|
|
132
132
|
|
|
133
133
|
# ── Connection lifecycle ──────────────────────────────────────────────
|
|
134
134
|
|
|
135
|
-
async def connect(self) -> bool:
|
|
135
|
+
async def connect(self, *, is_reconnect: bool = False) -> bool:
|
|
136
|
+
"""Connect to the local daemon.
|
|
137
|
+
|
|
138
|
+
``is_reconnect`` mirrors BasePlatformAdapter.connect's signature —
|
|
139
|
+
gateway/run.py passes it as a keyword on EVERY call, cold boot
|
|
140
|
+
included, so an adapter that omits it raises TypeError and the
|
|
141
|
+
platform never attaches. SecureComs holds no server-side update
|
|
142
|
+
queue to preserve across an outage (the daemon is a local WS the
|
|
143
|
+
supervisor re-opens), so the flag carries no behaviour here.
|
|
144
|
+
"""
|
|
145
|
+
del is_reconnect # accepted for contract parity; nothing to preserve
|
|
136
146
|
self._stopped = False
|
|
137
147
|
|
|
138
148
|
# BINDING-STABILITY fold: resolve bound_identity here, ONCE, for the
|
|
@@ -539,3 +539,36 @@ class TestCheckRequirementsWiring:
|
|
|
539
539
|
monkeypatch.setenv("SECURECOMS_CONNECTION_FILE", str(tmp_path / "does-not-exist.json"))
|
|
540
540
|
|
|
541
541
|
assert adapter.check_requirements() is False
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
# ---------------------------------------------------------------------------
|
|
545
|
+
# (6) connect() signature contract with BasePlatformAdapter
|
|
546
|
+
# ---------------------------------------------------------------------------
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
class TestConnectSignatureContract:
|
|
550
|
+
"""gateway/run.py ALWAYS calls `adapter.connect(is_reconnect=...)` — on the
|
|
551
|
+
cold first boot as well as on every reconnect. An adapter whose connect()
|
|
552
|
+
does not accept that keyword raises TypeError inside the gateway's
|
|
553
|
+
connect helper, which the reconnect watcher reports as a platform error
|
|
554
|
+
and retries forever: the daemon looks connected to the SecureComs server
|
|
555
|
+
while the harness side never attaches and the agent never answers.
|
|
556
|
+
"""
|
|
557
|
+
|
|
558
|
+
def test_connect_accepts_is_reconnect_keyword(self):
|
|
559
|
+
import inspect
|
|
560
|
+
|
|
561
|
+
sig = inspect.signature(adapter.SecureComsAdapter.connect)
|
|
562
|
+
param = sig.parameters.get("is_reconnect")
|
|
563
|
+
assert param is not None, "connect() must accept the is_reconnect keyword"
|
|
564
|
+
assert param.kind is inspect.Parameter.KEYWORD_ONLY
|
|
565
|
+
assert param.default is False
|
|
566
|
+
|
|
567
|
+
@pytest.mark.asyncio
|
|
568
|
+
async def test_connect_is_reconnect_true_reaches_the_unwired_path(self, monkeypatch):
|
|
569
|
+
a, _ = make_adapter(monkeypatch, files={}) # no binding file -> unwired
|
|
570
|
+
ok = await a.connect(is_reconnect=True)
|
|
571
|
+
assert ok is False
|
|
572
|
+
code, _message, retryable = a.fatal_errors[-1]
|
|
573
|
+
assert code == "unwired"
|
|
574
|
+
assert retryable is False
|
package/dist/bin.js
CHANGED
|
@@ -1409,9 +1409,7 @@ var SUITE2 = "xchacha20poly1305";
|
|
|
1409
1409
|
var AAD = new TextEncoder().encode("kekset");
|
|
1410
1410
|
var enc = new TextEncoder();
|
|
1411
1411
|
var dec = new TextDecoder();
|
|
1412
|
-
|
|
1413
|
-
return new Uint8Array(Buffer.from(s, "base64"));
|
|
1414
|
-
}
|
|
1412
|
+
var unb64 = base64ToBytes;
|
|
1415
1413
|
function headerObject(h) {
|
|
1416
1414
|
return {
|
|
1417
1415
|
enclaveId: h.enclaveId,
|
|
@@ -11000,6 +10998,16 @@ function connectorTargetDir(home, harness) {
|
|
|
11000
10998
|
if (harness === "openclaw") return `${home}/.openclaw/local-plugins/securecoms`;
|
|
11001
10999
|
return `${home}/.hermes/plugins/securecoms`;
|
|
11002
11000
|
}
|
|
11001
|
+
function checkBindingConflict(prev, next, allowRebind) {
|
|
11002
|
+
if (!prev || prev === next) return { ok: true, rebindFrom: null };
|
|
11003
|
+
if (allowRebind) return { ok: true, rebindFrom: prev };
|
|
11004
|
+
return {
|
|
11005
|
+
ok: false,
|
|
11006
|
+
message: `refusing to rebind: this harness connector is already bound to ${prev}, not ${next}. A harness gateway serves ONE agent, so this would take the harness away from ${prev} rather than add ${next} alongside it.
|
|
11007
|
+
To run both agents, give each one its own harness profile (its own connector dir + binding).
|
|
11008
|
+
To deliberately move THIS harness to ${next}, re-run with --rebind.`
|
|
11009
|
+
};
|
|
11010
|
+
}
|
|
11003
11011
|
function connectorsSrcDir(importMetaUrl, harness) {
|
|
11004
11012
|
const pkgRoot = dirname5(dirname5(fileURLToPath(importMetaUrl)));
|
|
11005
11013
|
return join9(pkgRoot, "connectors", harness);
|
|
@@ -11061,8 +11069,18 @@ function runWire(deps) {
|
|
|
11061
11069
|
if (!resolved) return 1;
|
|
11062
11070
|
const { entry, harness } = resolved;
|
|
11063
11071
|
const targetDir = connectorTargetDir(deps.home, harness);
|
|
11072
|
+
const guard = checkBindingConflict(
|
|
11073
|
+
deps.readBinding(targetDir),
|
|
11074
|
+
entry.identityId,
|
|
11075
|
+
deps.argv.includes("--rebind")
|
|
11076
|
+
);
|
|
11077
|
+
if (!guard.ok) {
|
|
11078
|
+
deps.log(`wire: ${guard.message}`);
|
|
11079
|
+
return 1;
|
|
11080
|
+
}
|
|
11064
11081
|
deps.copyConnector(deps.connectorsSrcDir(harness), targetDir, harness);
|
|
11065
11082
|
deps.writeBinding(targetDir, entry.identityId);
|
|
11083
|
+
if (guard.rebindFrom) deps.log(`wire: rebind: ${guard.rebindFrom} -> ${entry.identityId}`);
|
|
11066
11084
|
if (harness === "hermes") {
|
|
11067
11085
|
const enabled = enableHermesConfig(deps.home, deps);
|
|
11068
11086
|
deps.log(`[securecoms] ${enabled.message}`);
|
|
@@ -11129,12 +11147,19 @@ function autoWireHarness(input, deps) {
|
|
|
11129
11147
|
};
|
|
11130
11148
|
}
|
|
11131
11149
|
const targetDir = connectorTargetDir(input.home, "openclaw");
|
|
11150
|
+
const guard = checkBindingConflict(deps.readBinding(targetDir), input.identityId, input.allowRebind ?? false);
|
|
11151
|
+
if (!guard.ok) {
|
|
11152
|
+
return {
|
|
11153
|
+
echo: `[securecoms] the bridge service is installed, but the OpenClaw connector was NOT wired: ${guard.message}`,
|
|
11154
|
+
wired: false,
|
|
11155
|
+
error: true
|
|
11156
|
+
};
|
|
11157
|
+
}
|
|
11132
11158
|
try {
|
|
11133
|
-
const prev = deps.readBinding(targetDir);
|
|
11134
11159
|
deps.copyConnector(input.connectorsSrcDir, targetDir, "openclaw");
|
|
11135
11160
|
deps.writeBinding(targetDir, input.identityId);
|
|
11136
|
-
const rebind =
|
|
11137
|
-
rebind: ${
|
|
11161
|
+
const rebind = guard.rebindFrom ? `
|
|
11162
|
+
rebind: ${guard.rebindFrom} \u2192 ${input.identityId}` : "";
|
|
11138
11163
|
return {
|
|
11139
11164
|
echo: `[securecoms] OpenClaw connector installed and bound to identity ${input.identityId}.${rebind}
|
|
11140
11165
|
Now restart OpenClaw to pick up the connector.`,
|
|
@@ -11163,8 +11188,15 @@ function autoWireHarness(input, deps) {
|
|
|
11163
11188
|
};
|
|
11164
11189
|
}
|
|
11165
11190
|
const targetDir = connectorTargetDir(input.home, "hermes");
|
|
11191
|
+
const guard = checkBindingConflict(deps.readBinding(targetDir), input.identityId, input.allowRebind ?? false);
|
|
11192
|
+
if (!guard.ok) {
|
|
11193
|
+
return {
|
|
11194
|
+
echo: `[securecoms] the bridge service is installed, but the Hermes connector was NOT wired: ${guard.message}`,
|
|
11195
|
+
wired: false,
|
|
11196
|
+
error: true
|
|
11197
|
+
};
|
|
11198
|
+
}
|
|
11166
11199
|
try {
|
|
11167
|
-
const prev = deps.readBinding(targetDir);
|
|
11168
11200
|
deps.copyConnector(input.connectorsSrcDir, targetDir, "hermes");
|
|
11169
11201
|
deps.writeBinding(targetDir, input.identityId);
|
|
11170
11202
|
const enabled = enableHermesConfig(input.home, deps);
|
|
@@ -11176,8 +11208,8 @@ function autoWireHarness(input, deps) {
|
|
|
11176
11208
|
error: true
|
|
11177
11209
|
};
|
|
11178
11210
|
}
|
|
11179
|
-
const rebind =
|
|
11180
|
-
rebind: ${
|
|
11211
|
+
const rebind = guard.rebindFrom ? `
|
|
11212
|
+
rebind: ${guard.rebindFrom} \u2192 ${input.identityId}` : "";
|
|
11181
11213
|
return {
|
|
11182
11214
|
echo: `[securecoms] Hermes connector installed and bound to identity ${input.identityId}.${rebind}
|
|
11183
11215
|
${enabled.message}
|
|
@@ -11342,6 +11374,7 @@ async function orchestrateInstall(ctx) {
|
|
|
11342
11374
|
identityId: parsed.identityId,
|
|
11343
11375
|
slug,
|
|
11344
11376
|
port,
|
|
11377
|
+
allowRebind: ctx.allowRebind,
|
|
11345
11378
|
home: ctx.home,
|
|
11346
11379
|
tokenFilePath: result.tokenHint,
|
|
11347
11380
|
connectorsSrcDir: ctx.connectorsSrcDir
|
|
@@ -11741,6 +11774,8 @@ function realWireDeps() {
|
|
|
11741
11774
|
argv: realWireArgv(),
|
|
11742
11775
|
readFleet: realReadFleet,
|
|
11743
11776
|
copyConnector: realCopyConnector,
|
|
11777
|
+
readBinding: realReadBinding,
|
|
11778
|
+
// bin.ts:252 — feeds checkBindingConflict
|
|
11744
11779
|
writeBinding: realWriteBinding,
|
|
11745
11780
|
removeBinding: realRemoveBinding,
|
|
11746
11781
|
getHealth: realGetHealth,
|
|
@@ -11786,6 +11821,7 @@ async function runServiceVerb(verb) {
|
|
|
11786
11821
|
label,
|
|
11787
11822
|
explicitPort: portStr ? Number(portStr) : void 0,
|
|
11788
11823
|
harness: installHarness,
|
|
11824
|
+
allowRebind: process.argv.includes("--rebind"),
|
|
11789
11825
|
home: homedir2(),
|
|
11790
11826
|
connectorsSrcDir: connectorsSrcDir(import.meta.url, installHarness === "hermes" ? "hermes" : "openclaw"),
|
|
11791
11827
|
autoWireDeps: {
|
package/dist/index.js
CHANGED
|
@@ -1121,9 +1121,7 @@ var SUITE2 = "xchacha20poly1305";
|
|
|
1121
1121
|
var AAD = new TextEncoder().encode("kekset");
|
|
1122
1122
|
var enc = new TextEncoder();
|
|
1123
1123
|
var dec = new TextDecoder();
|
|
1124
|
-
|
|
1125
|
-
return new Uint8Array(Buffer.from(s, "base64"));
|
|
1126
|
-
}
|
|
1124
|
+
var unb64 = base64ToBytes;
|
|
1127
1125
|
function headerObject(h) {
|
|
1128
1126
|
return {
|
|
1129
1127
|
enclaveId: h.enclaveId,
|
|
@@ -7,6 +7,10 @@ export interface AutoWireInput {
|
|
|
7
7
|
home: string;
|
|
8
8
|
tokenFilePath: string;
|
|
9
9
|
connectorsSrcDir: string;
|
|
10
|
+
/** Deliberate consent to move an already-bound harness to this identity
|
|
11
|
+
* (`install --rebind`). Absent/false, a conflicting binding REFUSES —
|
|
12
|
+
* see checkBindingConflict in wire.ts for why. */
|
|
13
|
+
allowRebind?: boolean;
|
|
10
14
|
}
|
|
11
15
|
export interface AutoWireDeps extends HermesConfigIoDeps {
|
|
12
16
|
statDir: (p: string) => boolean;
|
package/dist/service/wire.d.ts
CHANGED
|
@@ -38,6 +38,10 @@ export interface WireDeps extends HermesConfigIoDeps {
|
|
|
38
38
|
argv: string[];
|
|
39
39
|
readFleet: () => AgentFleetEntry[];
|
|
40
40
|
copyConnector: (srcDir: string, dstDir: string, harness: Harness) => void;
|
|
41
|
+
/** The identity the target dir is ALREADY bound to, or null when nothing
|
|
42
|
+
* is bound / the file is missing or malformed. Read BEFORE any write so
|
|
43
|
+
* `checkBindingConflict` can refuse without having touched disk. */
|
|
44
|
+
readBinding: (dstDir: string) => string | null;
|
|
41
45
|
writeBinding: (dstDir: string, identityId: string) => void;
|
|
42
46
|
removeBinding: (dstDir: string) => void;
|
|
43
47
|
getHealth: (port: number) => {
|
|
@@ -72,6 +76,32 @@ export declare function detectHarness(home: string, statDir: (p: string) => bool
|
|
|
72
76
|
* placeholder value, never the real one).
|
|
73
77
|
*/
|
|
74
78
|
export declare function connectorTargetDir(home: string, harness: Harness): string;
|
|
79
|
+
/**
|
|
80
|
+
* The binding guard, shared by `wire` and by `install`'s auto-wire.
|
|
81
|
+
*
|
|
82
|
+
* A harness connector registers ONE platform and resolves ONE bound identity
|
|
83
|
+
* for the life of the gateway process (connectors/hermes/adapter.py's
|
|
84
|
+
* `register` + `_bound_identity`), so a connector directory holds exactly one
|
|
85
|
+
* agent. Writing a different identity over an existing binding therefore does
|
|
86
|
+
* not ADD an agent — it silently takes the harness away from whichever agent
|
|
87
|
+
* was already using it, leaving that agent dark while two gateways race for
|
|
88
|
+
* one daemon socket. Observed in the field 2026-09-08: installing a second
|
|
89
|
+
* Hermes agent rebound `~/.hermes/plugins/securecoms` away from the agent the
|
|
90
|
+
* default-profile gateway was serving.
|
|
91
|
+
*
|
|
92
|
+
* So an overwrite must be a deliberate act (`--rebind`), exactly like the
|
|
93
|
+
* binding itself is a deliberate act (audit C1). A first wire and an
|
|
94
|
+
* idempotent same-identity re-wire are always allowed.
|
|
95
|
+
*
|
|
96
|
+
* Pure: callers do the reading and the refusing.
|
|
97
|
+
*/
|
|
98
|
+
export declare function checkBindingConflict(prev: string | null, next: string, allowRebind: boolean): {
|
|
99
|
+
ok: true;
|
|
100
|
+
rebindFrom: string | null;
|
|
101
|
+
} | {
|
|
102
|
+
ok: false;
|
|
103
|
+
message: string;
|
|
104
|
+
};
|
|
75
105
|
/**
|
|
76
106
|
* Resolves a connector's SOURCE dir at runtime, keyed by harness (`connectors/
|
|
77
107
|
* <harness>`) — `connectors/` is shipped SOURCE (not compiled — H2), so this
|