@securecoms/agent-bridge 0.38.0 → 0.40.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 +33 -0
- package/connectors/display-name-fixtures.json +605 -0
- package/connectors/hermes/__pycache__/securecoms_gate.cpython-310.pyc +0 -0
- package/connectors/hermes/__pycache__/securecoms_transport.cpython-310.pyc +0 -0
- package/connectors/hermes/adapter.py +417 -13
- package/connectors/hermes/securecoms_authz.py +112 -11
- package/connectors/hermes/securecoms_transport.py +109 -0
- package/connectors/hermes/tests/test_adapter_wiring.py +683 -7
- package/connectors/hermes/tests/test_busy_session.py +1436 -0
- package/connectors/hermes/tests/test_securecoms_authz.py +194 -0
- package/connectors/hermes/tests/test_securecoms_transport.py +29 -0
- package/connectors/openclaw/openclaw-entry.inbound.test.ts +136 -4
- package/connectors/openclaw/openclaw-entry.js +151 -4
- package/connectors/openclaw/openclaw-entry.outbound.test.ts +217 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +65 -3
- package/dist/bridge-agent.d.ts +5 -0
- package/dist/http-server.d.ts +1 -0
- package/dist/index.js +17 -1
- package/dist/service/wire.d.ts +24 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// packages/connect-agent-bridge/connectors/openclaw/openclaw-entry.outbound.test.ts
|
|
2
|
+
//
|
|
3
|
+
// Item A (proactive delivery): the OpenClaw connector advertises the outbound
|
|
4
|
+
// targets "owner" and "default". They are not channel ids. They resolve through
|
|
5
|
+
// the bridge's bearer-gated GET /v1/owner-dm (the owner DM from the signed
|
|
6
|
+
// manifest in force), and fail closed: a 404, any non-2xx, a network error or a
|
|
7
|
+
// malformed body posts NOTHING and returns {ok:false, error: OWNER_DM_UNKNOWN_ERROR}.
|
|
8
|
+
// Any other `to` keeps the explicit-channel behaviour.
|
|
9
|
+
//
|
|
10
|
+
// Drives the REAL plugin.gateway.startAccount path with a fake loopback
|
|
11
|
+
// WebSocket and a fake fetch (same harness shape as openclaw-entry.inbound.test.ts).
|
|
12
|
+
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
14
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { buildConnectionFile, writeConnectionFile } from "../../src/service/connection-file.js";
|
|
18
|
+
|
|
19
|
+
const BOUND_ID = "agent-identity";
|
|
20
|
+
const PORT = 7097;
|
|
21
|
+
const TOKEN_VALUE = "outbound-test-token";
|
|
22
|
+
const ORIGINAL_CONN_FILE_ENV = process.env.SECURECOMS_CONNECTION_FILE;
|
|
23
|
+
|
|
24
|
+
function waitFor(predicate: () => boolean, timeout = 3000): Promise<void> {
|
|
25
|
+
const start = Date.now();
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const check = () => {
|
|
28
|
+
if (predicate()) return resolve();
|
|
29
|
+
if (Date.now() - start > timeout) return reject(new Error("waitFor: condition never became true"));
|
|
30
|
+
setTimeout(check, 5);
|
|
31
|
+
};
|
|
32
|
+
check();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeFakeWebSocket() {
|
|
37
|
+
const instances: any[] = [];
|
|
38
|
+
class FakeWebSocket {
|
|
39
|
+
listeners: Record<string, Array<(ev?: unknown) => void>> = {};
|
|
40
|
+
constructor(public url: string) { instances.push(this); }
|
|
41
|
+
addEventListener(type: string, cb: (ev?: unknown) => void) { (this.listeners[type] ??= []).push(cb); }
|
|
42
|
+
send() {}
|
|
43
|
+
close() { this._emit("close"); }
|
|
44
|
+
_emit(type: string, ev?: unknown) { for (const cb of this.listeners[type] ?? []) cb(ev ?? {}); }
|
|
45
|
+
}
|
|
46
|
+
return { FakeWebSocket, instances };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type OwnerDm =
|
|
50
|
+
| { kind: "ok"; body: unknown }
|
|
51
|
+
| { kind: "status"; status: number; body?: unknown }
|
|
52
|
+
| { kind: "network" }
|
|
53
|
+
| { kind: "badjson" };
|
|
54
|
+
|
|
55
|
+
describe("openclaw-entry: outbound owner/default targets resolve through /v1/owner-dm", () => {
|
|
56
|
+
let home: string;
|
|
57
|
+
let controller: AbortController;
|
|
58
|
+
let sends: Array<Record<string, unknown>>;
|
|
59
|
+
let ownerDmCalls: Array<{ auth: string | null }>;
|
|
60
|
+
let ownerDm: OwnerDm;
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
home = mkdtempSync(join(tmpdir(), "sc-outbound-"));
|
|
64
|
+
const tokenFile = join(home, "bridge-token");
|
|
65
|
+
writeFileSync(tokenFile, TOKEN_VALUE, { mode: 0o600 });
|
|
66
|
+
writeConnectionFile(home, buildConnectionFile({
|
|
67
|
+
identityId: BOUND_ID, port: PORT, tokenFile, slug: "outbound-test", label: "Outbound Test",
|
|
68
|
+
instanceId: "outbound-test-instance", now: new Date().toISOString(),
|
|
69
|
+
}));
|
|
70
|
+
process.env.SECURECOMS_CONNECTION_FILE = join(home, ".securecoms", "connections", `${BOUND_ID}.json`);
|
|
71
|
+
sends = [];
|
|
72
|
+
ownerDmCalls = [];
|
|
73
|
+
ownerDm = { kind: "ok", body: { channel_id: "dm-1" } };
|
|
74
|
+
vi.stubGlobal("fetch", vi.fn(async (url: string, init?: RequestInit) => {
|
|
75
|
+
const u = String(url);
|
|
76
|
+
if (u.endsWith("/healthz")) return { ok: true, json: async () => ({ identity_id: BOUND_ID, joined_channels: 1 }) };
|
|
77
|
+
if (u.endsWith("/v1/owner-dm")) {
|
|
78
|
+
const h = (init?.headers ?? {}) as Record<string, string>;
|
|
79
|
+
ownerDmCalls.push({ auth: h.authorization ?? null });
|
|
80
|
+
if (ownerDm.kind === "network") throw new Error("ECONNREFUSED");
|
|
81
|
+
if (ownerDm.kind === "badjson") {
|
|
82
|
+
return { ok: true, status: 200, json: async () => { throw new SyntaxError("bad json"); }, text: async () => "<html>" };
|
|
83
|
+
}
|
|
84
|
+
if (ownerDm.kind === "status") {
|
|
85
|
+
return { ok: false, status: ownerDm.status, json: async () => ownerDm.kind === "status" ? ownerDm.body ?? {} : {}, text: async () => "" };
|
|
86
|
+
}
|
|
87
|
+
return { ok: true, status: 200, json: async () => ownerDm.body };
|
|
88
|
+
}
|
|
89
|
+
if (u.endsWith("/v1/send")) {
|
|
90
|
+
sends.push(JSON.parse(String(init?.body)));
|
|
91
|
+
return { ok: true, json: async () => ({ message_id: "m-1" }) };
|
|
92
|
+
}
|
|
93
|
+
if (u.endsWith("/v1/status")) return { ok: true, json: async () => ({ ok: true }) };
|
|
94
|
+
throw new Error(`unexpected fetch: ${u}`);
|
|
95
|
+
}));
|
|
96
|
+
controller = new AbortController();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterEach(async () => {
|
|
100
|
+
controller.abort();
|
|
101
|
+
vi.unstubAllGlobals();
|
|
102
|
+
vi.unmock("node:os");
|
|
103
|
+
if (ORIGINAL_CONN_FILE_ENV === undefined) delete process.env.SECURECOMS_CONNECTION_FILE;
|
|
104
|
+
else process.env.SECURECOMS_CONNECTION_FILE = ORIGINAL_CONN_FILE_ENV;
|
|
105
|
+
vi.resetModules();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
async function start() {
|
|
109
|
+
vi.resetModules();
|
|
110
|
+
vi.doMock("node:os", async (importOriginal) => {
|
|
111
|
+
const actual = await importOriginal<typeof import("node:os")>();
|
|
112
|
+
return { ...actual, homedir: () => home };
|
|
113
|
+
});
|
|
114
|
+
const mod = await import("./openclaw-entry.js");
|
|
115
|
+
let plugin: any;
|
|
116
|
+
mod.default.register({
|
|
117
|
+
runtime: { channel: {}, error: () => {} },
|
|
118
|
+
on: () => {},
|
|
119
|
+
registerTrustedToolPolicy: () => {},
|
|
120
|
+
registerChannel: ({ plugin: p }: { plugin: unknown }) => { plugin = p; },
|
|
121
|
+
});
|
|
122
|
+
const { FakeWebSocket, instances } = makeFakeWebSocket();
|
|
123
|
+
vi.stubGlobal("WebSocket", FakeWebSocket);
|
|
124
|
+
const account = plugin.config.resolveAccount({}, "default");
|
|
125
|
+
plugin.gateway.startAccount({ account, cfg: {}, log: () => {}, abortSignal: controller.signal }).catch(() => {});
|
|
126
|
+
await waitFor(() => instances.length >= 1);
|
|
127
|
+
instances[0]._emit("open");
|
|
128
|
+
return { plugin, mod };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const advertised = ["owner", "default"] as const;
|
|
132
|
+
|
|
133
|
+
it("keeps the advertised target labels unchanged", async () => {
|
|
134
|
+
const { plugin } = await start();
|
|
135
|
+
expect(plugin.outbound.targets).toEqual([
|
|
136
|
+
{ id: "owner", label: "SecureComs Owner", accountId: "default" },
|
|
137
|
+
{ id: "default", label: "SecureComs Owner (default)", accountId: "default" },
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
for (const to of advertised) {
|
|
142
|
+
it(`(a) to="${to}": resolves via /v1/owner-dm with the bearer and sends once to the resolved channel`, async () => {
|
|
143
|
+
const { plugin } = await start();
|
|
144
|
+
const res = await plugin.outbound.sendText({ text: "x", to, accountId: "default" });
|
|
145
|
+
expect(res).toEqual({ ok: true });
|
|
146
|
+
expect(ownerDmCalls).toEqual([{ auth: `Bearer ${TOKEN_VALUE}` }]);
|
|
147
|
+
expect(sends).toHaveLength(1);
|
|
148
|
+
expect(sends[0]).toEqual({ channel_id: "dm-1", text: "x" }); // no in_reply_to, no idempotency_key
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it(`(b) to="${to}": a 404 posts nothing and returns the fixed error`, async () => {
|
|
152
|
+
const { plugin, mod } = await start();
|
|
153
|
+
ownerDm = { kind: "status", status: 404, body: { error: "owner_dm_unknown" } };
|
|
154
|
+
const res = await plugin.outbound.sendText({ text: "secret body", to, accountId: "default" });
|
|
155
|
+
expect(sends).toHaveLength(0);
|
|
156
|
+
expect(res.ok).toBe(false);
|
|
157
|
+
expect(res.error).toBe(mod.OWNER_DM_UNKNOWN_ERROR);
|
|
158
|
+
expect(res.error.length).toBeGreaterThan(0);
|
|
159
|
+
expect(res.error).not.toContain("secret body");
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const failures: Array<[string, OwnerDm]> = [
|
|
164
|
+
["a 500", { kind: "status", status: 500 }],
|
|
165
|
+
["a 401", { kind: "status", status: 401 }],
|
|
166
|
+
["a network error", { kind: "network" }],
|
|
167
|
+
["a non-JSON 200 body", { kind: "badjson" }],
|
|
168
|
+
["a 200 with no channel_id", { kind: "ok", body: {} }],
|
|
169
|
+
["a 200 with an empty channel_id", { kind: "ok", body: { channel_id: "" } }],
|
|
170
|
+
["a 200 with a non-string channel_id", { kind: "ok", body: { channel_id: 7 } }],
|
|
171
|
+
["a 200 that resolves to the literal 'owner'", { kind: "ok", body: { channel_id: "owner" } }],
|
|
172
|
+
["a 200 that resolves to the literal 'default'", { kind: "ok", body: { channel_id: "default" } }],
|
|
173
|
+
];
|
|
174
|
+
for (const [name, mode] of failures) {
|
|
175
|
+
it(`(b) ${name} fails closed: no send, fixed error`, async () => {
|
|
176
|
+
const { plugin, mod } = await start();
|
|
177
|
+
ownerDm = mode;
|
|
178
|
+
const res = await plugin.outbound.sendText({ text: "x", to: "owner", accountId: "default" });
|
|
179
|
+
expect(sends).toHaveLength(0);
|
|
180
|
+
expect(res).toEqual({ ok: false, error: mod.OWNER_DM_UNKNOWN_ERROR });
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
it("(c) in no case does a send carry channel_id 'owner' or 'default'", async () => {
|
|
185
|
+
const { plugin } = await start();
|
|
186
|
+
const modes: OwnerDm[] = [
|
|
187
|
+
{ kind: "ok", body: { channel_id: "dm-1" } },
|
|
188
|
+
{ kind: "status", status: 404, body: { error: "owner_dm_unknown" } },
|
|
189
|
+
{ kind: "status", status: 500 },
|
|
190
|
+
{ kind: "network" },
|
|
191
|
+
{ kind: "badjson" },
|
|
192
|
+
{ kind: "ok", body: {} },
|
|
193
|
+
];
|
|
194
|
+
for (const m of modes) {
|
|
195
|
+
ownerDm = m;
|
|
196
|
+
for (const to of advertised) await plugin.outbound.sendText({ text: "x", to, accountId: "default" });
|
|
197
|
+
}
|
|
198
|
+
expect(sends.length).toBeGreaterThan(0); // vacuity control: the ok mode did send
|
|
199
|
+
expect(sends.some((s) => s.channel_id === "owner" || s.channel_id === "default")).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("any other `to` keeps the explicit-channel behaviour and never calls /v1/owner-dm", async () => {
|
|
203
|
+
const { plugin } = await start();
|
|
204
|
+
const res = await plugin.outbound.sendText({ text: "x", to: "chan-uuid-1", accountId: "default" });
|
|
205
|
+
expect(res).toEqual({ ok: true });
|
|
206
|
+
expect(ownerDmCalls).toHaveLength(0);
|
|
207
|
+
expect(sends).toEqual([{ channel_id: "chan-uuid-1", text: "x" }]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("no `to` still errors without a send or a lookup", async () => {
|
|
211
|
+
const { plugin } = await start();
|
|
212
|
+
const res = await plugin.outbound.sendText({ text: "x", accountId: "default" });
|
|
213
|
+
expect(res.ok).toBe(false);
|
|
214
|
+
expect(sends).toHaveLength(0);
|
|
215
|
+
expect(ownerDmCalls).toHaveLength(0);
|
|
216
|
+
});
|
|
217
|
+
});
|
package/dist/bin.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export declare function slugFromCredentialPath(credentialPath: string): string;
|
|
|
29
29
|
export declare function chooseInstallExecPath(runningExecPath: string, override: string | undefined, deps: ResolveNodeDeps): string;
|
|
30
30
|
export declare function realGetHealth(port: number): DaemonHealth | null;
|
|
31
31
|
export declare function realCopyConnector(srcDir: string, dstDir: string, harness: Harness): void;
|
|
32
|
+
export declare function realReadHermesEnv(p: string): string | null;
|
|
33
|
+
export declare function realAppendHermesEnv(p: string, text: string): void;
|
|
32
34
|
/** The stderr line (with newline) a failed service verb prints. */
|
|
33
35
|
export declare function formatServiceError(verb: string, e: unknown): string;
|
|
34
36
|
/**
|
package/dist/bin.js
CHANGED
|
@@ -44763,6 +44763,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
44763
44763
|
async consumeUploaded(read) {
|
|
44764
44764
|
if (!this.outboxPin || !await consumeOutboxFile(this.outboxPin, read)) writeServiceLog("warn", "vault_outbox.consume_failed");
|
|
44765
44765
|
}
|
|
44766
|
+
/**
|
|
44767
|
+
* Item A (OQ-A1): the owner DM channel id of the signed manifest in force, else null. In force means the owner pin is
|
|
44768
|
+
* set (same guard as applyManifest). No channelOwner, no listChannels heuristic, no fallback.
|
|
44769
|
+
*/
|
|
44770
|
+
ownerDmChannelId() {
|
|
44771
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
44772
|
+
const inForce = pin !== null ? this.toolGrantManifest : null;
|
|
44773
|
+
const id = inForce?.owner_dm_channel_id;
|
|
44774
|
+
return typeof id === "string" && id !== "" ? id : null;
|
|
44775
|
+
}
|
|
44766
44776
|
/** Audit 2026-10-30 MED 3: the pinned outbox path, served only on the authenticated GET /v1/vault-outbox. */
|
|
44767
44777
|
vaultOutboxPath() {
|
|
44768
44778
|
return this.outboxPin?.dir ?? null;
|
|
@@ -50172,7 +50182,9 @@ function capabilities(opts) {
|
|
|
50172
50182
|
// version this daemon parses into BridgeMessage.vault_refs (never fetched
|
|
50173
50183
|
// on receipt) and opens with the R4 pins on /v1/vault-read. A daemon
|
|
50174
50184
|
// without this field skips and acks a link, so the agent never sees it.
|
|
50175
|
-
vault_ref: 1
|
|
50185
|
+
vault_ref: 1,
|
|
50186
|
+
// Item A: GET /v1/owner-dm resolves the literal `owner`/`default` cron target from the signed manifest in force.
|
|
50187
|
+
owner_dm_route: 1
|
|
50176
50188
|
};
|
|
50177
50189
|
}
|
|
50178
50190
|
var SEND_CAP = 64 * 1024;
|
|
@@ -50356,6 +50368,10 @@ function createHttpServer(agent, opts) {
|
|
|
50356
50368
|
if (method === "GET" && path === "/v1/vault-outbox") {
|
|
50357
50369
|
return json(res, 200, { path: agent.vaultOutboxPath() });
|
|
50358
50370
|
}
|
|
50371
|
+
if (method === "GET" && path === "/v1/owner-dm") {
|
|
50372
|
+
const channelId = agent.ownerDmChannelId();
|
|
50373
|
+
return channelId === null ? json(res, 404, { error: "owner_dm_unknown" }) : json(res, 200, { channel_id: channelId });
|
|
50374
|
+
}
|
|
50359
50375
|
if (method === "POST" && path === "/v1/send-attachment") {
|
|
50360
50376
|
const parsed = sendAttachmentSchema.safeParse(JSON.parse(await readBody(req, OTHER_CAP)));
|
|
50361
50377
|
if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
|
|
@@ -51138,7 +51154,7 @@ async function handleInbound(sock, agent, data, inflight, flush, session) {
|
|
|
51138
51154
|
var LOWER_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
51139
51155
|
|
|
51140
51156
|
// src/bin.ts
|
|
51141
|
-
import { readFileSync as readFileSync17, mkdirSync as mkdirSync9, writeFileSync as writeFileSync5, rmSync as rmSync8, existsSync as existsSync7, realpathSync as realpathSync4, readdirSync as readdirSync4, statSync as statSync8, chmodSync as chmodSync7 } from "node:fs";
|
|
51157
|
+
import { readFileSync as readFileSync17, mkdirSync as mkdirSync9, writeFileSync as writeFileSync5, appendFileSync, rmSync as rmSync8, existsSync as existsSync7, realpathSync as realpathSync4, readdirSync as readdirSync4, statSync as statSync8, chmodSync as chmodSync7 } from "node:fs";
|
|
51142
51158
|
import { join as join27, dirname as dirname14, isAbsolute as isAbsolute3, resolve as resolve4 } from "node:path";
|
|
51143
51159
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
51144
51160
|
import { homedir as homedir5, userInfo } from "node:os";
|
|
@@ -51587,6 +51603,36 @@ function enableHermesConfig(home, deps) {
|
|
|
51587
51603
|
message: `enabled ${HERMES_PLUGIN_NAME} in ${path} (the original was backed up alongside it as config.yaml.bak-wire-<ts>-<pid>).`
|
|
51588
51604
|
};
|
|
51589
51605
|
}
|
|
51606
|
+
var HERMES_HOME_CHANNEL_KEY = "SECURECOMS_HOME_CHANNEL";
|
|
51607
|
+
var HERMES_HOME_CHANNEL_LINE = `${HERMES_HOME_CHANNEL_KEY}=owner`;
|
|
51608
|
+
function hermesEnvPath(home) {
|
|
51609
|
+
return `${home}/.hermes/.env`;
|
|
51610
|
+
}
|
|
51611
|
+
var HOME_CHANNEL_ASSIGNMENT = new RegExp(`^\\s*(?:export\\s+)?${HERMES_HOME_CHANNEL_KEY}\\s*=`, "m");
|
|
51612
|
+
function homeChannelEnvAppendix(text) {
|
|
51613
|
+
const current = text ?? "";
|
|
51614
|
+
if (HOME_CHANNEL_ASSIGNMENT.test(current)) return null;
|
|
51615
|
+
const sep = current === "" || current.endsWith("\n") ? "" : "\n";
|
|
51616
|
+
return `${sep}${HERMES_HOME_CHANNEL_LINE}
|
|
51617
|
+
`;
|
|
51618
|
+
}
|
|
51619
|
+
function ensureHermesHomeChannel(home, deps) {
|
|
51620
|
+
const path = hermesEnvPath(home);
|
|
51621
|
+
try {
|
|
51622
|
+
const appendix = homeChannelEnvAppendix(deps.readHermesEnv(path));
|
|
51623
|
+
if (appendix === null) {
|
|
51624
|
+
deps.log(`[securecoms] ${HERMES_HOME_CHANNEL_KEY} is already set in ${path}; kept as is.`);
|
|
51625
|
+
return;
|
|
51626
|
+
}
|
|
51627
|
+
deps.appendHermesEnv(path, appendix);
|
|
51628
|
+
deps.log(`[securecoms] set ${HERMES_HOME_CHANNEL_LINE} in ${path} (scheduled messages go to the owner's DM).`);
|
|
51629
|
+
} catch {
|
|
51630
|
+
deps.log(
|
|
51631
|
+
`wire: could not update ${path}; scheduled messages need it. Add this line manually:
|
|
51632
|
+
${HERMES_HOME_CHANNEL_LINE}`
|
|
51633
|
+
);
|
|
51634
|
+
}
|
|
51635
|
+
}
|
|
51590
51636
|
function parseArg(argv, flag) {
|
|
51591
51637
|
const i = argv.indexOf(flag);
|
|
51592
51638
|
return i >= 0 ? argv[i + 1] : void 0;
|
|
@@ -51635,6 +51681,7 @@ function runWire(deps) {
|
|
|
51635
51681
|
);
|
|
51636
51682
|
return 1;
|
|
51637
51683
|
}
|
|
51684
|
+
ensureHermesHomeChannel(deps.home, deps);
|
|
51638
51685
|
}
|
|
51639
51686
|
if (typeof entry.port === "number") {
|
|
51640
51687
|
const h = deps.getHealth(entry.port);
|
|
@@ -52883,6 +52930,17 @@ function realBackupHermesConfig(p, originalText) {
|
|
|
52883
52930
|
function realWriteHermesConfig(p, newText) {
|
|
52884
52931
|
writeFileSync5(p, newText, "utf8");
|
|
52885
52932
|
}
|
|
52933
|
+
function realReadHermesEnv(p) {
|
|
52934
|
+
try {
|
|
52935
|
+
return readFileSync17(p, "utf8");
|
|
52936
|
+
} catch (e) {
|
|
52937
|
+
if (e?.code === "ENOENT") return null;
|
|
52938
|
+
throw e;
|
|
52939
|
+
}
|
|
52940
|
+
}
|
|
52941
|
+
function realAppendHermesEnv(p, text) {
|
|
52942
|
+
appendFileSync(p, text, { encoding: "utf8", mode: 384 });
|
|
52943
|
+
}
|
|
52886
52944
|
function realWireArgv() {
|
|
52887
52945
|
if (process.argv.includes("--harness")) return process.argv;
|
|
52888
52946
|
const detected = detectHarness(homedir5(), realStatDir);
|
|
@@ -52906,7 +52964,9 @@ function realWireDeps() {
|
|
|
52906
52964
|
connectorsSrcDir: (h) => connectorsSrcDir(import.meta.url, h),
|
|
52907
52965
|
readHermesConfig: realReadHermesConfig,
|
|
52908
52966
|
backupHermesConfig: realBackupHermesConfig,
|
|
52909
|
-
writeHermesConfig: realWriteHermesConfig
|
|
52967
|
+
writeHermesConfig: realWriteHermesConfig,
|
|
52968
|
+
readHermesEnv: realReadHermesEnv,
|
|
52969
|
+
appendHermesEnv: realAppendHermesEnv
|
|
52910
52970
|
};
|
|
52911
52971
|
}
|
|
52912
52972
|
function runWireVerb(verb) {
|
|
@@ -53365,8 +53425,10 @@ export {
|
|
|
53365
53425
|
formatServiceError,
|
|
53366
53426
|
nodeMajorOk,
|
|
53367
53427
|
parseVerb,
|
|
53428
|
+
realAppendHermesEnv,
|
|
53368
53429
|
realCopyConnector,
|
|
53369
53430
|
realGetHealth,
|
|
53431
|
+
realReadHermesEnv,
|
|
53370
53432
|
runMode,
|
|
53371
53433
|
slugFromCredentialPath,
|
|
53372
53434
|
startThenServe,
|
package/dist/bridge-agent.d.ts
CHANGED
|
@@ -2539,6 +2539,11 @@ export declare class BridgeAgent {
|
|
|
2539
2539
|
}>;
|
|
2540
2540
|
/** Audit 2026-10-30 MED 5: an uploaded file leaves the outbox (no plaintext lingers). Content-free log. */
|
|
2541
2541
|
private consumeUploaded;
|
|
2542
|
+
/**
|
|
2543
|
+
* Item A (OQ-A1): the owner DM channel id of the signed manifest in force, else null. In force means the owner pin is
|
|
2544
|
+
* set (same guard as applyManifest). No channelOwner, no listChannels heuristic, no fallback.
|
|
2545
|
+
*/
|
|
2546
|
+
ownerDmChannelId(): string | null;
|
|
2542
2547
|
/** Audit 2026-10-30 MED 3: the pinned outbox path, served only on the authenticated GET /v1/vault-outbox. */
|
|
2543
2548
|
vaultOutboxPath(): string | null;
|
|
2544
2549
|
/** Download + verify + decrypt a vault file node. Returns the plaintext
|
package/dist/http-server.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -44477,6 +44477,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
44477
44477
|
async consumeUploaded(read) {
|
|
44478
44478
|
if (!this.outboxPin || !await consumeOutboxFile(this.outboxPin, read)) writeServiceLog2("warn", "vault_outbox.consume_failed");
|
|
44479
44479
|
}
|
|
44480
|
+
/**
|
|
44481
|
+
* Item A (OQ-A1): the owner DM channel id of the signed manifest in force, else null. In force means the owner pin is
|
|
44482
|
+
* set (same guard as applyManifest). No channelOwner, no listChannels heuristic, no fallback.
|
|
44483
|
+
*/
|
|
44484
|
+
ownerDmChannelId() {
|
|
44485
|
+
const pin = this.cfg.ownerGrantPin?.pin ?? null;
|
|
44486
|
+
const inForce = pin !== null ? this.toolGrantManifest : null;
|
|
44487
|
+
const id = inForce?.owner_dm_channel_id;
|
|
44488
|
+
return typeof id === "string" && id !== "" ? id : null;
|
|
44489
|
+
}
|
|
44480
44490
|
/** Audit 2026-10-30 MED 3: the pinned outbox path, served only on the authenticated GET /v1/vault-outbox. */
|
|
44481
44491
|
vaultOutboxPath() {
|
|
44482
44492
|
return this.outboxPin?.dir ?? null;
|
|
@@ -50032,7 +50042,9 @@ function capabilities(opts) {
|
|
|
50032
50042
|
// version this daemon parses into BridgeMessage.vault_refs (never fetched
|
|
50033
50043
|
// on receipt) and opens with the R4 pins on /v1/vault-read. A daemon
|
|
50034
50044
|
// without this field skips and acks a link, so the agent never sees it.
|
|
50035
|
-
vault_ref: 1
|
|
50045
|
+
vault_ref: 1,
|
|
50046
|
+
// Item A: GET /v1/owner-dm resolves the literal `owner`/`default` cron target from the signed manifest in force.
|
|
50047
|
+
owner_dm_route: 1
|
|
50036
50048
|
};
|
|
50037
50049
|
}
|
|
50038
50050
|
var SEND_CAP = 64 * 1024;
|
|
@@ -50216,6 +50228,10 @@ function createHttpServer(agent, opts) {
|
|
|
50216
50228
|
if (method === "GET" && path === "/v1/vault-outbox") {
|
|
50217
50229
|
return json(res, 200, { path: agent.vaultOutboxPath() });
|
|
50218
50230
|
}
|
|
50231
|
+
if (method === "GET" && path === "/v1/owner-dm") {
|
|
50232
|
+
const channelId = agent.ownerDmChannelId();
|
|
50233
|
+
return channelId === null ? json(res, 404, { error: "owner_dm_unknown" }) : json(res, 200, { channel_id: channelId });
|
|
50234
|
+
}
|
|
50219
50235
|
if (method === "POST" && path === "/v1/send-attachment") {
|
|
50220
50236
|
const parsed = sendAttachmentSchema.safeParse(JSON.parse(await readBody(req, OTHER_CAP)));
|
|
50221
50237
|
if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
|
package/dist/service/wire.d.ts
CHANGED
|
@@ -60,6 +60,14 @@ export interface WireDeps extends HermesConfigIoDeps {
|
|
|
60
60
|
* tests and never inspects the path for its own sake).
|
|
61
61
|
*/
|
|
62
62
|
connectorsSrcDir: (harness: Harness) => string;
|
|
63
|
+
/** The Hermes profile env file's text; null when the file does not exist.
|
|
64
|
+
* Throws on any other read failure, so an unreadable file is never
|
|
65
|
+
* mistaken for an absent key (see ensureHermesHomeChannel). */
|
|
66
|
+
readHermesEnv: (path: string) => string | null;
|
|
67
|
+
/** Appends `text` to the env file (creating it 0600 when missing). An
|
|
68
|
+
* append never rewrites another line and never changes an existing
|
|
69
|
+
* file's mode. */
|
|
70
|
+
appendHermesEnv: (path: string, text: string) => void;
|
|
63
71
|
}
|
|
64
72
|
/**
|
|
65
73
|
* Detects an installed harness by probing well-known home-relative dirs.
|
|
@@ -136,6 +144,22 @@ export declare function enableHermesConfig(home: string, deps: HermesConfigIoDep
|
|
|
136
144
|
ok: boolean;
|
|
137
145
|
message: string;
|
|
138
146
|
};
|
|
147
|
+
/** The Hermes cron home-channel env var (the connector registers it as
|
|
148
|
+
* `cron_deliver_env_var`) and the value `wire` sets: the literal `owner`,
|
|
149
|
+
* which the connector resolves only through GET /v1/owner-dm (Item A). */
|
|
150
|
+
export declare const HERMES_HOME_CHANNEL_KEY = "SECURECOMS_HOME_CHANNEL";
|
|
151
|
+
export declare const HERMES_HOME_CHANNEL_LINE = "SECURECOMS_HOME_CHANNEL=owner";
|
|
152
|
+
/** The env file Hermes loads for the profile `wire` targets: `<HERMES_HOME>/.env`,
|
|
153
|
+
* with HERMES_HOME the default `~/.hermes` (the same profile scope as
|
|
154
|
+
* `hermesConfigPath`). */
|
|
155
|
+
export declare function hermesEnvPath(home: string): string;
|
|
156
|
+
/** The text to append so the env file sets the home channel, or null when the
|
|
157
|
+
* key is already assigned (any value, including an owner-set channel id or an
|
|
158
|
+
* empty one: `wire` never overwrites it). Pure. */
|
|
159
|
+
export declare function homeChannelEnvAppendix(text: string | null): string | null;
|
|
160
|
+
/** Sets the Hermes cron home channel when absent. Never fatal to `wire`: the
|
|
161
|
+
* connector is wired without it, and a failure prints the manual line. */
|
|
162
|
+
export declare function ensureHermesHomeChannel(home: string, deps: Pick<WireDeps, "readHermesEnv" | "appendHermesEnv" | "log">): void;
|
|
139
163
|
/**
|
|
140
164
|
* Installs the reference connector into the harness's plugin dir and drops
|
|
141
165
|
* an explicit securecoms.binding.json naming the resolved agent's identity.
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const BRIDGE_VERSION = "0.
|
|
1
|
+
export declare const BRIDGE_VERSION = "0.40.0";
|