@securecoms/agent-bridge 0.16.0 → 0.18.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 +60 -1
- package/dist/bin.js +314 -81
- package/dist/bridge-agent.d.ts +24 -0
- package/dist/index.js +231 -13
- package/dist/service/runtime-config-dir.d.ts +44 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -305,6 +305,65 @@ Pick one receive mode — WebSocket, SSE, or long-poll — and stick with it. Th
|
|
|
305
305
|
|
|
306
306
|
---
|
|
307
307
|
|
|
308
|
+
## Bring your own agent
|
|
309
|
+
|
|
310
|
+
The daemon has no approved-framework list. It checks a bearer token and nothing else, so there is exactly one requirement:
|
|
311
|
+
|
|
312
|
+
> **You must be able to add code to a process that stays running.**
|
|
313
|
+
|
|
314
|
+
That is the whole test. If your agent is a program you control, it can hold a SecureComs channel in about fifty lines — see [`examples/ws-agent-client.mjs`](./examples/ws-agent-client.mjs) and [`ws-agent-client.py`](./examples/ws-agent-client.py), which are runnable as-is.
|
|
315
|
+
|
|
316
|
+
To check your own connector against every rule in this section, run [`examples/byo-conformance.mjs`](./examples/byo-conformance.mjs) — a zero-dependency agent that follows the section literally and prints a pass/fail line per rule:
|
|
317
|
+
|
|
318
|
+
```sh
|
|
319
|
+
node examples/byo-conformance.mjs <your agent's identity_id>
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**This fits.** Anything you build yourself, in any language: LangChain, LangGraph, CrewAI, AutoGen, Pydantic AI, the OpenAI or Anthropic agent SDKs, LlamaIndex — or a plain script calling an LLM API with no framework at all. You already own the process; you add a socket to it.
|
|
323
|
+
|
|
324
|
+
**This does not fit.** Consumer chat products (claude.ai, ChatGPT, the Gemini app) have no extension point. Session-based coding CLIs have no always-on loop to attach to. A fully hosted agent you cannot run code beside cannot reach the daemon.
|
|
325
|
+
|
|
326
|
+
**A platform you run but did not write** (Dify, Flowise, n8n, and the like) fits only if it lets you run persistent custom code. If it does, what you are writing is a plugin for that platform — the same job the bundled `openclaw` and `hermes` connectors do.
|
|
327
|
+
|
|
328
|
+
Install with `--harness universal` to skip connector wiring and print the details your agent needs. (`--harness custom` is the old name for the same thing and still works.) Running `install` with no `--harness` at all does the same thing when neither OpenClaw nor Hermes is present.
|
|
329
|
+
|
|
330
|
+
### Where your agent runs
|
|
331
|
+
|
|
332
|
+
Same machine by default — the daemon binds `127.0.0.1`. Another machine works too, over TLS (see **Cross-host use** under Security). One caveat: a delivered attachment gives you a **local file path** on the daemon's machine, so an off-host agent can receive text, status, vault and A2A traffic but cannot read attachment bytes.
|
|
333
|
+
|
|
334
|
+
### The five rules
|
|
335
|
+
|
|
336
|
+
1. **Ack everything.** Delivery is at-least-once. Send `{type:"ack", message_id}` once you have durably handled a message, and dedup by `message_id`.
|
|
337
|
+
2. **One consumer per daemon.** A second `/v1/ws` closes the first. Two clients will evict each other in a loop.
|
|
338
|
+
3. **Answer the pings.** The daemon pings every 30s and reaps a socket that misses a pong. Standard WebSocket clients do this for you.
|
|
339
|
+
4. **Read the token from its file, never from an environment value.** An env *value* is inherited by child processes and visible in `ps eww` and crash dumps. A path is not.
|
|
340
|
+
5. **Check the identity gate before you attach.** Details below.
|
|
341
|
+
|
|
342
|
+
### The identity gate
|
|
343
|
+
|
|
344
|
+
Skip this and a single-agent host still works. The failure appears later, when a second agent is installed on the same machine, and it is silent.
|
|
345
|
+
|
|
346
|
+
The daemon rewrites `~/.securecoms/connections/<identity_id>.json` on every start, giving you its current port and token-file path — so a moved port is picked up automatically. But if your connector reads the *wrong* agent's file, everything looks consistent: it authenticates successfully to that agent's daemon, drives that agent's identity, and evicts that agent's real connector.
|
|
347
|
+
|
|
348
|
+
The bearer token does not catch this. It rejects a caller with *no* valid token; it does nothing against a caller holding a *valid but wrong* pair.
|
|
349
|
+
|
|
350
|
+
So, before opening the WebSocket:
|
|
351
|
+
|
|
352
|
+
1. Know which agent you are meant to serve — a fixed `identity_id`, from your own config. **Never** infer it by scanning the connections directory: on a multi-agent host a slow-starting daemon can leave exactly one file present at the moment you look, and you will bind to the wrong agent.
|
|
353
|
+
2. Read that agent's connection file for `bridge_url` and `token_file`.
|
|
354
|
+
3. `GET /healthz` (no token needed) and require **both**: the file's `identity_id` and your own fixed `identity_id` each equal `healthz.identity_id`.
|
|
355
|
+
4. Any mismatch: do not open the socket. Back off with jitter and retry from step 2 — the port may have moved.
|
|
356
|
+
|
|
357
|
+
On reconnect, re-read the connection file and the token, but **not** your identity. Re-deriving it on every reconnect lets a file appearing or disappearing silently re-bind a running connector to a different agent.
|
|
358
|
+
|
|
359
|
+
The bundled `openclaw` and `hermes` connectors implement all of this; read either one as a worked example.
|
|
360
|
+
|
|
361
|
+
### Separate compartments need separate OS uids
|
|
362
|
+
|
|
363
|
+
The gate prevents honest mistakes. It is not a tenant boundary. The hop from daemon to agent carries plaintext, and the token and sealed store are readable by anyone sharing the uid — so any agent under the same uid can already drive another's daemon directly. **Agents in different orgs or enclaves that must not cross MUST run under different OS uids, or on different hosts.**
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
308
367
|
## Run as a service
|
|
309
368
|
|
|
310
369
|
For an autonomous agent running on a server, keep the daemon up with systemd so it restarts on failure and starts on boot. A sleeping or crashed daemon means its WebSocket/SSE connections drop and messages queue on the server until the daemon reconnects; they replay losslessly on wake. The daemon can't wake a sleeping machine.
|
|
@@ -358,7 +417,7 @@ pm2 start "npx @agentvault/connect-agent-bridge --credential ./agent.json" \
|
|
|
358
417
|
- A **free port is auto-selected** from `7077-7276` (lowest free wins; sibling service definitions and live binds both count as taken). Pass `--port N` to override; the explicit port is honored as-is.
|
|
359
418
|
- Install **verifies the daemon actually came up** and fails loudly with the log path if it didn't — no silent crash-loops.
|
|
360
419
|
- 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
|
-
- Optional `--harness openclaw|hermes|
|
|
420
|
+
- Optional `--harness openclaw|hermes|universal` prints a paste-ready config snippet with the chosen port and the token-file path (the token itself never appears in output). (`--harness custom` is an accepted alias for `universal`.)
|
|
362
421
|
- **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`.
|
|
363
422
|
|
|
364
423
|
---
|
package/dist/bin.js
CHANGED
|
@@ -4834,6 +4834,9 @@ var FRAME_PARSERS = {
|
|
|
4834
4834
|
}),
|
|
4835
4835
|
voice_settings_changed: (d) => ({
|
|
4836
4836
|
voiceSettingsChanged: { identityId: str(d?.identity_id), version: Number(d?.version ?? 0) }
|
|
4837
|
+
}),
|
|
4838
|
+
runtime_config_changed: (d) => ({
|
|
4839
|
+
runtimeConfigChanged: { identityId: str(d?.identity_id), version: Number(d?.version ?? 0) }
|
|
4837
4840
|
})
|
|
4838
4841
|
};
|
|
4839
4842
|
var S2C_FRAME_NAMES = new Set(Object.keys(FRAME_PARSERS));
|
|
@@ -6388,7 +6391,7 @@ var SqliteMlsStateStore = class {
|
|
|
6388
6391
|
|
|
6389
6392
|
// src/bridge-agent.ts
|
|
6390
6393
|
import { decryptFile as decryptFile2, encryptFile as encryptFile2, computeFileDigest as computeFileDigest2, base64ToBytes as base64ToBytes4, bytesToBase64 as bytesToBase644 } from "@agentvault/crypto";
|
|
6391
|
-
import { createHash } from "node:crypto";
|
|
6394
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
6392
6395
|
|
|
6393
6396
|
// src/service/resolve-bin.ts
|
|
6394
6397
|
import { join as join2, dirname as dirname2 } from "node:path";
|
|
@@ -6441,7 +6444,7 @@ function loadPackageMeta(importMetaUrl) {
|
|
|
6441
6444
|
|
|
6442
6445
|
// src/bridge-agent.ts
|
|
6443
6446
|
import { promises as fsp } from "node:fs";
|
|
6444
|
-
import { join as
|
|
6447
|
+
import { join as join9, dirname as dirname3 } from "node:path";
|
|
6445
6448
|
|
|
6446
6449
|
// src/latency.ts
|
|
6447
6450
|
var DEFAULT_CAPACITY = 1024;
|
|
@@ -7461,7 +7464,135 @@ async function runSummarize(opts) {
|
|
|
7461
7464
|
}
|
|
7462
7465
|
}
|
|
7463
7466
|
|
|
7467
|
+
// src/service/runtime-config-dir.ts
|
|
7468
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7469
|
+
import {
|
|
7470
|
+
chmodSync,
|
|
7471
|
+
existsSync,
|
|
7472
|
+
mkdirSync,
|
|
7473
|
+
readFileSync as readFileSync6,
|
|
7474
|
+
renameSync,
|
|
7475
|
+
statSync as statSync3,
|
|
7476
|
+
unlinkSync,
|
|
7477
|
+
writeFileSync
|
|
7478
|
+
} from "node:fs";
|
|
7479
|
+
import { basename as basename2, join as join8 } from "node:path";
|
|
7480
|
+
var MEMORY_FILE = "MEMORY.md";
|
|
7481
|
+
var REAL_FS = {
|
|
7482
|
+
mkdir: (p, mode) => mkdirSync(p, { recursive: true, mode }),
|
|
7483
|
+
chmod: (p, mode) => chmodSync(p, mode),
|
|
7484
|
+
writeFile: (p, data, mode) => writeFileSync(p, data, { mode }),
|
|
7485
|
+
rename: (a, b) => renameSync(a, b),
|
|
7486
|
+
readFile: (p) => readFileSync6(p, "utf8"),
|
|
7487
|
+
unlink: (p) => unlinkSync(p)
|
|
7488
|
+
};
|
|
7489
|
+
function runtimeConfigDir(home, identityId) {
|
|
7490
|
+
return join8(home, ".securecoms", "runtime-config", identityId);
|
|
7491
|
+
}
|
|
7492
|
+
function assertManagedName(name) {
|
|
7493
|
+
if (!name || name.includes("/") || name.includes("\\") || name === "." || name === ".." || name !== basename2(name)) {
|
|
7494
|
+
throw new Error(`refusing managed config file name ${JSON.stringify(name)}: not a plain filename`);
|
|
7495
|
+
}
|
|
7496
|
+
}
|
|
7497
|
+
function atomicWrite(path, body, deps) {
|
|
7498
|
+
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
7499
|
+
deps.writeFile(tmp, body, 384);
|
|
7500
|
+
deps.chmod(tmp, 384);
|
|
7501
|
+
try {
|
|
7502
|
+
deps.rename(tmp, path);
|
|
7503
|
+
} catch (err) {
|
|
7504
|
+
try {
|
|
7505
|
+
deps.unlink(tmp);
|
|
7506
|
+
} catch {
|
|
7507
|
+
}
|
|
7508
|
+
throw err;
|
|
7509
|
+
}
|
|
7510
|
+
}
|
|
7511
|
+
function writeManagedFiles(home, identityId, files, deps = REAL_FS) {
|
|
7512
|
+
const entries = Object.entries(files);
|
|
7513
|
+
for (const [name] of entries) assertManagedName(name);
|
|
7514
|
+
const parent2 = join8(home, ".securecoms", "runtime-config");
|
|
7515
|
+
const dir = runtimeConfigDir(home, identityId);
|
|
7516
|
+
for (const d of [join8(home, ".securecoms"), parent2, dir]) {
|
|
7517
|
+
deps.mkdir(d, 448);
|
|
7518
|
+
deps.chmod(d, 448);
|
|
7519
|
+
}
|
|
7520
|
+
for (const [name, body] of entries) atomicWrite(join8(dir, name), body, deps);
|
|
7521
|
+
}
|
|
7522
|
+
function checkConfiguredPaths(skillsJson, statFn = (p) => {
|
|
7523
|
+
const s = statSync3(p);
|
|
7524
|
+
return { mode: s.mode, size: s.size };
|
|
7525
|
+
}) {
|
|
7526
|
+
const paths = [];
|
|
7527
|
+
const endpoints = skillsJson.endpoints ?? {};
|
|
7528
|
+
for (const e of Object.values(endpoints)) if (e?.headersFile) paths.push(e.headersFile);
|
|
7529
|
+
const databases = skillsJson.databases ?? {};
|
|
7530
|
+
for (const d of Object.values(databases)) {
|
|
7531
|
+
if (d?.sqlite) paths.push(d.sqlite);
|
|
7532
|
+
if (d?.postgresUrlFile) paths.push(d.postgresUrlFile);
|
|
7533
|
+
}
|
|
7534
|
+
return paths.map((path) => {
|
|
7535
|
+
let st;
|
|
7536
|
+
try {
|
|
7537
|
+
st = statFn(path);
|
|
7538
|
+
} catch {
|
|
7539
|
+
return { path, ok: false, why: "not found" };
|
|
7540
|
+
}
|
|
7541
|
+
if ((st.mode & 63) !== 0) {
|
|
7542
|
+
return { path, ok: false, why: "readable by other users on this machine \u2014 tighten its permissions to owner-only" };
|
|
7543
|
+
}
|
|
7544
|
+
if (st.size === 0) return { path, ok: false, why: "the file is empty" };
|
|
7545
|
+
return { path, ok: true, why: "" };
|
|
7546
|
+
});
|
|
7547
|
+
}
|
|
7548
|
+
function lineHash(line) {
|
|
7549
|
+
return createHash("sha256").update(line.trim(), "utf8").digest("hex");
|
|
7550
|
+
}
|
|
7551
|
+
var LOCAL_REPORT_FILE = "local-config.json";
|
|
7552
|
+
function readLocalReport(home, identityId) {
|
|
7553
|
+
const p = join8(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
|
|
7554
|
+
if (!existsSync(p)) return null;
|
|
7555
|
+
let doc;
|
|
7556
|
+
try {
|
|
7557
|
+
doc = JSON.parse(readFileSync6(p, "utf8"));
|
|
7558
|
+
} catch {
|
|
7559
|
+
return null;
|
|
7560
|
+
}
|
|
7561
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
|
|
7562
|
+
const hasSearchKey = doc.has_search_key === true;
|
|
7563
|
+
const raw = doc.persona;
|
|
7564
|
+
const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
|
|
7565
|
+
Object.entries(raw).filter(([, v]) => typeof v === "string")
|
|
7566
|
+
) : {};
|
|
7567
|
+
return { persona, has_search_key: hasSearchKey };
|
|
7568
|
+
}
|
|
7569
|
+
function readManagedMemory(home, identityId) {
|
|
7570
|
+
const p = join8(runtimeConfigDir(home, identityId), MEMORY_FILE);
|
|
7571
|
+
if (!existsSync(p)) return null;
|
|
7572
|
+
try {
|
|
7573
|
+
const body = readFileSync6(p, "utf8").trim();
|
|
7574
|
+
return body || null;
|
|
7575
|
+
} catch {
|
|
7576
|
+
return null;
|
|
7577
|
+
}
|
|
7578
|
+
}
|
|
7579
|
+
function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
7580
|
+
const dir = runtimeConfigDir(home, identityId);
|
|
7581
|
+
const path = join8(dir, MEMORY_FILE);
|
|
7582
|
+
if (!existsSync(path)) return 0;
|
|
7583
|
+
const drop = new Set(hashes);
|
|
7584
|
+
const before = deps.readFile(path).split("\n");
|
|
7585
|
+
const kept = before.filter((l) => l.trim() === "" || !drop.has(lineHash(l)));
|
|
7586
|
+
const removed = before.filter((l) => l.trim() !== "" && drop.has(lineHash(l))).length;
|
|
7587
|
+
if (removed === 0) return 0;
|
|
7588
|
+
const body = kept.filter((l, i) => l.trim() !== "" || i < kept.length - 1).join("\n").trimEnd();
|
|
7589
|
+
atomicWrite(path, body ? `${body}
|
|
7590
|
+
` : "", deps);
|
|
7591
|
+
return removed;
|
|
7592
|
+
}
|
|
7593
|
+
|
|
7464
7594
|
// src/bridge-agent.ts
|
|
7595
|
+
import { homedir as homedir2 } from "node:os";
|
|
7465
7596
|
var VOICE_MODE_GUIDANCE = "Reply immediately and conversationally, one short sentence per message; never bundle multiple points into one message.";
|
|
7466
7597
|
var FAST_PATH_SPOKEN_GUIDANCE = "A one-sentence spoken acknowledgement has already been sent for this turn. Reply only with information it did not contain, in one or two short sentences; if there is nothing to add, send nothing.";
|
|
7467
7598
|
var ROOM_VOICE_GUIDANCE_MENTION_ONLY = "You are in a group voice room. Reply ONLY when a message @mentions your display name. Keep replies to one short sentence. When you should not reply, send nothing.";
|
|
@@ -7516,7 +7647,7 @@ function resolveMessageId(messageId) {
|
|
|
7516
7647
|
return messageId;
|
|
7517
7648
|
}
|
|
7518
7649
|
function fastReplyId(messageId) {
|
|
7519
|
-
const h =
|
|
7650
|
+
const h = createHash2("sha256").update(`fast:${messageId}`).digest();
|
|
7520
7651
|
h[6] = h[6] & 15 | 64;
|
|
7521
7652
|
h[8] = h[8] & 63 | 128;
|
|
7522
7653
|
const x = h.subarray(0, 16).toString("hex");
|
|
@@ -7646,6 +7777,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
7646
7777
|
onSupersedeUnopenable: (channelId, groupId) => this.healSupersedeUnopenable(channelId, groupId)
|
|
7647
7778
|
});
|
|
7648
7779
|
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
7780
|
+
this.home = deps.home ?? homedir2();
|
|
7649
7781
|
this.injectedFastLlm = !!deps.fastLlm;
|
|
7650
7782
|
this.fileVoiceConfig = cfg.voice;
|
|
7651
7783
|
this.fastLlm = deps.fastLlm ?? (cfg.voice?.fastPath.enabled ? new FastLlmClient(cfg.voice.fastPath, this.fetchImpl) : void 0);
|
|
@@ -7678,8 +7810,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
7678
7810
|
const j = await res.json();
|
|
7679
7811
|
return String(j.attachment_id);
|
|
7680
7812
|
};
|
|
7681
|
-
this.attachmentsDirImpl = deps.attachmentsDir ??
|
|
7682
|
-
this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(
|
|
7813
|
+
this.attachmentsDirImpl = deps.attachmentsDir ?? join9(dirname3(cfg.storePath), "attachments");
|
|
7814
|
+
this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join9(dirname3(cfg.storePath), "vault-owner-pins.json"));
|
|
7683
7815
|
this.action = {
|
|
7684
7816
|
begin: (channelId) => this.actionBegin(channelId),
|
|
7685
7817
|
start: (opts) => this.actionStart(opts),
|
|
@@ -7695,6 +7827,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
7695
7827
|
// stored for peek-then-join in ingestOnce
|
|
7696
7828
|
custody;
|
|
7697
7829
|
fetchImpl;
|
|
7830
|
+
/** Round 5: home for the managed runtime-config directory (test seam). */
|
|
7831
|
+
home;
|
|
7698
7832
|
joinState = /* @__PURE__ */ new Map();
|
|
7699
7833
|
/**
|
|
7700
7834
|
* Phase 2: lazy channel-metadata cache (channel_id → { is_dm, name, at }) used
|
|
@@ -8036,6 +8170,84 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8036
8170
|
if (this.voiceSettingsSource === "backend") return;
|
|
8037
8171
|
process.stderr.write(
|
|
8038
8172
|
`[bridge] voice settings unavailable (${e.message}); using ${this.voiceSettingsSource}
|
|
8173
|
+
`
|
|
8174
|
+
);
|
|
8175
|
+
}
|
|
8176
|
+
}
|
|
8177
|
+
/**
|
|
8178
|
+
* Fetch this agent's runtime config and apply it to the managed directory the
|
|
8179
|
+
* runtime reads (spec §8). Safe to call any time — at start(), or from the
|
|
8180
|
+
* receive pump on a `runtime_config_changed` ping — and never throws: a save
|
|
8181
|
+
* the owner already made must not be undone by a network blip here, and no
|
|
8182
|
+
* shape the backend can send may take the daemon down.
|
|
8183
|
+
*
|
|
8184
|
+
* Order matters. Prunes are applied BEFORE the memory is read back, so the
|
|
8185
|
+
* report the owner sees next is the file as it now stands.
|
|
8186
|
+
*
|
|
8187
|
+
* Every cycle rewrites, even at an unchanged version: directory permissions
|
|
8188
|
+
* are repaired only on a write, so skipping would leave a loosened directory
|
|
8189
|
+
* loose until the next save.
|
|
8190
|
+
*/
|
|
8191
|
+
async refetchRuntimeConfig() {
|
|
8192
|
+
const base = `${this.cfg.apiBase}/api/v1/agents/${encodeURIComponent(this.cfg.identityId)}/runtime-config`;
|
|
8193
|
+
try {
|
|
8194
|
+
const res = await this.fetchImpl(`${base}/files`, {
|
|
8195
|
+
headers: { authorization: `Bearer ${this.cfg.apiKey}` }
|
|
8196
|
+
});
|
|
8197
|
+
if (!res.ok) throw new Error(`runtime-config ${res.status}`);
|
|
8198
|
+
const payload = await res.json();
|
|
8199
|
+
if (!payload || !payload.version) {
|
|
8200
|
+
process.stderr.write(
|
|
8201
|
+
`[bridge] runtime config not applied (version ${JSON.stringify(payload?.version ?? null)} \u2014 no saved config)
|
|
8202
|
+
`
|
|
8203
|
+
);
|
|
8204
|
+
return;
|
|
8205
|
+
}
|
|
8206
|
+
const skillsJson = payload.skills_json ?? {};
|
|
8207
|
+
writeManagedFiles(this.home, this.cfg.identityId, {
|
|
8208
|
+
...payload.files ?? {},
|
|
8209
|
+
"skills.json": `${JSON.stringify(skillsJson, null, 2)}
|
|
8210
|
+
`
|
|
8211
|
+
});
|
|
8212
|
+
if (payload.memory_prunes?.length) {
|
|
8213
|
+
pruneManagedMemory(this.home, this.cfg.identityId, payload.memory_prunes);
|
|
8214
|
+
}
|
|
8215
|
+
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
8216
|
+
const report = {
|
|
8217
|
+
applied_version: payload.version,
|
|
8218
|
+
paths: checkConfiguredPaths(skillsJson),
|
|
8219
|
+
// Read AFTER the prunes above, so the owner's next view of memory is
|
|
8220
|
+
// the file as it now stands, not the one they just pruned from.
|
|
8221
|
+
memory: readManagedMemory(this.home, this.cfg.identityId),
|
|
8222
|
+
// E13: pass through what the runtime reported. The daemon does not
|
|
8223
|
+
// interpret any of it.
|
|
8224
|
+
local_persona: local?.persona ?? null,
|
|
8225
|
+
// Only the runtime knows whether a search key file is configured — it
|
|
8226
|
+
// is an environment value on that process, invisible to the daemon and
|
|
8227
|
+
// to the backend. The backend needs it to generate a truthful tool
|
|
8228
|
+
// paragraph, so it rides along here.
|
|
8229
|
+
has_search_key: local?.has_search_key ?? false
|
|
8230
|
+
};
|
|
8231
|
+
const posted = await this.fetchImpl(`${base}/report`, {
|
|
8232
|
+
method: "POST",
|
|
8233
|
+
headers: {
|
|
8234
|
+
authorization: `Bearer ${this.cfg.apiKey}`,
|
|
8235
|
+
"content-type": "application/json"
|
|
8236
|
+
},
|
|
8237
|
+
body: JSON.stringify(report)
|
|
8238
|
+
});
|
|
8239
|
+
if (!posted.ok) {
|
|
8240
|
+
process.stderr.write(
|
|
8241
|
+
`[bridge] runtime config v${payload.version} written, but reporting it failed (${posted.status}) \u2014 the console will keep showing it as not applied until the next cycle
|
|
8242
|
+
`
|
|
8243
|
+
);
|
|
8244
|
+
return;
|
|
8245
|
+
}
|
|
8246
|
+
process.stderr.write(`[bridge] runtime config v${payload.version} applied
|
|
8247
|
+
`);
|
|
8248
|
+
} catch (e) {
|
|
8249
|
+
process.stderr.write(
|
|
8250
|
+
`[bridge] runtime config refetch failed: ${e?.message ?? e}
|
|
8039
8251
|
`
|
|
8040
8252
|
);
|
|
8041
8253
|
}
|
|
@@ -8067,6 +8279,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
8067
8279
|
}
|
|
8068
8280
|
await this.provider.publishKeyPackage();
|
|
8069
8281
|
await this.refetchVoiceSettings();
|
|
8282
|
+
await this.refetchRuntimeConfig();
|
|
8070
8283
|
this.startReceivePump();
|
|
8071
8284
|
if (this.selfHealSweepEnabled) {
|
|
8072
8285
|
this.bootSweepTimer = setTimeout(
|
|
@@ -9551,11 +9764,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9551
9764
|
this.pingsReceived++;
|
|
9552
9765
|
if (ping?.reconnected) {
|
|
9553
9766
|
process.stderr.write("[bridge] delivery-ping WS (re)connected \u2014 push inbound active\n");
|
|
9767
|
+
await this.refetchRuntimeConfig();
|
|
9554
9768
|
}
|
|
9555
9769
|
if (ping?.voiceSettingsChanged) {
|
|
9556
9770
|
await this.refetchVoiceSettings();
|
|
9557
9771
|
continue;
|
|
9558
9772
|
}
|
|
9773
|
+
if (ping?.runtimeConfigChanged) {
|
|
9774
|
+
await this.refetchRuntimeConfig();
|
|
9775
|
+
continue;
|
|
9776
|
+
}
|
|
9559
9777
|
await this.ingestOnce();
|
|
9560
9778
|
}
|
|
9561
9779
|
} catch {
|
|
@@ -9994,13 +10212,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9994
10212
|
|
|
9995
10213
|
// src/auth.ts
|
|
9996
10214
|
import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
|
|
9997
|
-
import { existsSync, readFileSync as
|
|
10215
|
+
import { existsSync as existsSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
|
|
9998
10216
|
function resolveToken(opts) {
|
|
9999
10217
|
if (opts.token) return opts.token;
|
|
10000
10218
|
const f = opts.tokenFile;
|
|
10001
|
-
if (f &&
|
|
10219
|
+
if (f && existsSync2(f)) return readFileSync7(f, "utf8").trim();
|
|
10002
10220
|
const tok = randomBytes3(32).toString("base64url");
|
|
10003
|
-
if (f)
|
|
10221
|
+
if (f) writeFileSync2(f, tok, { mode: 384 });
|
|
10004
10222
|
return tok;
|
|
10005
10223
|
}
|
|
10006
10224
|
function tokenMatches(provided, expected) {
|
|
@@ -10621,17 +10839,17 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
10621
10839
|
}
|
|
10622
10840
|
|
|
10623
10841
|
// src/bin.ts
|
|
10624
|
-
import { readFileSync as
|
|
10625
|
-
import { join as
|
|
10842
|
+
import { readFileSync as readFileSync9, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, rmSync, existsSync as existsSync3, realpathSync, readdirSync as readdirSync2, statSync as statSync4 } from "node:fs";
|
|
10843
|
+
import { join as join13, dirname as dirname7, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
|
|
10626
10844
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10627
|
-
import { homedir as
|
|
10845
|
+
import { homedir as homedir3, userInfo } from "node:os";
|
|
10628
10846
|
import { spawnSync } from "node:child_process";
|
|
10629
10847
|
|
|
10630
10848
|
// src/service/install.ts
|
|
10631
10849
|
import { dirname as dirname4 } from "node:path";
|
|
10632
10850
|
|
|
10633
10851
|
// src/service/config.ts
|
|
10634
|
-
import { basename as
|
|
10852
|
+
import { basename as basename3, extname, isAbsolute, resolve } from "node:path";
|
|
10635
10853
|
|
|
10636
10854
|
// src/service/resolve-node.ts
|
|
10637
10855
|
var REQUIRED_NODE_MAJOR = 24;
|
|
@@ -10709,7 +10927,7 @@ function deriveServiceConfig(input) {
|
|
|
10709
10927
|
`install: refusing to bake a version-embedded Node path into the service definition: ${input.execPath}. Re-run install under a system/Homebrew Node, or pass --exec-path <stable-path>.`
|
|
10710
10928
|
);
|
|
10711
10929
|
}
|
|
10712
|
-
const stem = input.label ??
|
|
10930
|
+
const stem = input.label ?? basename3(input.credentialPath, extname(input.credentialPath));
|
|
10713
10931
|
const slug = slugify(stem);
|
|
10714
10932
|
const cred = isAbsolute(input.credentialPath) ? input.credentialPath : resolve(input.credentialPath);
|
|
10715
10933
|
const args2 = ["--credential", cred];
|
|
@@ -10831,10 +11049,10 @@ function uninstallService(input, deps) {
|
|
|
10831
11049
|
}
|
|
10832
11050
|
|
|
10833
11051
|
// src/service/install-prep.ts
|
|
10834
|
-
import { join as
|
|
11052
|
+
import { join as join11 } from "node:path";
|
|
10835
11053
|
|
|
10836
11054
|
// src/service/wire.ts
|
|
10837
|
-
import { join as
|
|
11055
|
+
import { join as join10, dirname as dirname5 } from "node:path";
|
|
10838
11056
|
import { fileURLToPath } from "node:url";
|
|
10839
11057
|
|
|
10840
11058
|
// src/service/hermes-config.ts
|
|
@@ -11010,7 +11228,7 @@ function checkBindingConflict(prev, next, allowRebind) {
|
|
|
11010
11228
|
}
|
|
11011
11229
|
function connectorsSrcDir(importMetaUrl, harness) {
|
|
11012
11230
|
const pkgRoot = dirname5(dirname5(fileURLToPath(importMetaUrl)));
|
|
11013
|
-
return
|
|
11231
|
+
return join10(pkgRoot, "connectors", harness);
|
|
11014
11232
|
}
|
|
11015
11233
|
function hermesConfigPath(home) {
|
|
11016
11234
|
return `${home}/.hermes/config.yaml`;
|
|
@@ -11125,15 +11343,30 @@ function runUnwire(deps) {
|
|
|
11125
11343
|
function customFacts(input) {
|
|
11126
11344
|
const ws = `ws://127.0.0.1:${input.port ?? "<PORT>"}/v1/ws`;
|
|
11127
11345
|
return [
|
|
11128
|
-
` bridge WS:
|
|
11129
|
-
` token file:
|
|
11346
|
+
` bridge WS: ${ws}`,
|
|
11347
|
+
` token file: ${input.tokenFilePath}`,
|
|
11348
|
+
` agent id: ${input.identityId}`,
|
|
11349
|
+
` discovery: ~/.securecoms/connections/${input.identityId}.json (rewritten on every daemon start)`,
|
|
11130
11350
|
` Bring your own connector: open the WS with the bearer token read from that file.`,
|
|
11351
|
+
` Ack every message you receive; delivery is at-least-once, so dedup by message_id.`,
|
|
11352
|
+
` Before you attach, check the identity gate: the daemon's /healthz identity_id MUST equal`,
|
|
11353
|
+
` the agent id above. Skipping it means a second agent on this host can silently`,
|
|
11354
|
+
` drive the wrong daemon. See "Bring your own agent" in the agent-bridge README.`,
|
|
11131
11355
|
` Protocol reference: the "WebSocket" section of the agent-bridge README.`
|
|
11132
11356
|
].join("\n");
|
|
11133
11357
|
}
|
|
11134
11358
|
function autoWireHarness(input, deps) {
|
|
11135
11359
|
const h = input.harness;
|
|
11136
|
-
if (
|
|
11360
|
+
if (h === "none") return { echo: "", wired: false, error: false };
|
|
11361
|
+
if (!h) {
|
|
11362
|
+
return {
|
|
11363
|
+
echo: `[securecoms] No agent harness detected (no ~/.openclaw, no ~/.hermes) \u2014 the bridge service is installed and running, but nothing is attached to it yet.
|
|
11364
|
+
Bring your own agent:
|
|
11365
|
+
${customFacts(input)}`,
|
|
11366
|
+
wired: false,
|
|
11367
|
+
error: false
|
|
11368
|
+
};
|
|
11369
|
+
}
|
|
11137
11370
|
if (h === "openclaw") {
|
|
11138
11371
|
const retry = `agent-bridge wire --harness openclaw --identity ${input.slug}`;
|
|
11139
11372
|
const parent2 = `${input.home}/.openclaw`;
|
|
@@ -11141,7 +11374,7 @@ function autoWireHarness(input, deps) {
|
|
|
11141
11374
|
return {
|
|
11142
11375
|
echo: `[securecoms] OpenClaw not found at ~/.openclaw \u2014 the bridge service is installed, but the harness was NOT wired.
|
|
11143
11376
|
Install OpenClaw, then re-run install, or run: ${retry}
|
|
11144
|
-
(or re-run install with --harness
|
|
11377
|
+
(or re-run install with --harness universal).`,
|
|
11145
11378
|
wired: false,
|
|
11146
11379
|
error: true
|
|
11147
11380
|
};
|
|
@@ -11182,7 +11415,7 @@ function autoWireHarness(input, deps) {
|
|
|
11182
11415
|
return {
|
|
11183
11416
|
echo: `[securecoms] Hermes not found at ~/.hermes \u2014 the bridge service is installed, but the harness was NOT wired.
|
|
11184
11417
|
Install Hermes, then re-run install, or run: ${retry}
|
|
11185
|
-
(or re-run install with --harness
|
|
11418
|
+
(or re-run install with --harness universal).`,
|
|
11186
11419
|
wired: false,
|
|
11187
11420
|
error: true
|
|
11188
11421
|
};
|
|
@@ -11226,7 +11459,7 @@ function autoWireHarness(input, deps) {
|
|
|
11226
11459
|
};
|
|
11227
11460
|
}
|
|
11228
11461
|
}
|
|
11229
|
-
return { echo: `[securecoms]
|
|
11462
|
+
return { echo: `[securecoms] Universal \u2014 bring your own agent:
|
|
11230
11463
|
${customFacts(input)}`, wired: false, error: false };
|
|
11231
11464
|
}
|
|
11232
11465
|
|
|
@@ -11253,10 +11486,10 @@ function assertReadableCredential(absPath, deps) {
|
|
|
11253
11486
|
return { deviceId: json2.device_id, identityId: json2.identity_id, issuedAt };
|
|
11254
11487
|
}
|
|
11255
11488
|
function durableDir(home, slug) {
|
|
11256
|
-
return
|
|
11489
|
+
return join11(home, `securecoms-agent-${slug}`);
|
|
11257
11490
|
}
|
|
11258
11491
|
function durableCredentialPath(home, slug) {
|
|
11259
|
-
return
|
|
11492
|
+
return join11(durableDir(home, slug), `${slug}.json`);
|
|
11260
11493
|
}
|
|
11261
11494
|
function takeCustody(args2, deps) {
|
|
11262
11495
|
const dir = durableDir(args2.home, args2.slug);
|
|
@@ -11303,7 +11536,7 @@ function readSiblingDefs(dir, prefix, deps) {
|
|
|
11303
11536
|
for (const n of names) {
|
|
11304
11537
|
if (!n.startsWith(prefix)) continue;
|
|
11305
11538
|
try {
|
|
11306
|
-
out.push(deps.readFile(
|
|
11539
|
+
out.push(deps.readFile(join11(dir, n)));
|
|
11307
11540
|
} catch {
|
|
11308
11541
|
}
|
|
11309
11542
|
}
|
|
@@ -11498,34 +11731,34 @@ function renderStatusTable(rows) {
|
|
|
11498
11731
|
|
|
11499
11732
|
// src/service/connection-file.ts
|
|
11500
11733
|
import {
|
|
11501
|
-
chmodSync,
|
|
11502
|
-
mkdirSync,
|
|
11503
|
-
readFileSync as
|
|
11504
|
-
renameSync,
|
|
11505
|
-
unlinkSync,
|
|
11506
|
-
writeFileSync as
|
|
11734
|
+
chmodSync as chmodSync2,
|
|
11735
|
+
mkdirSync as mkdirSync2,
|
|
11736
|
+
readFileSync as readFileSync8,
|
|
11737
|
+
renameSync as renameSync2,
|
|
11738
|
+
unlinkSync as unlinkSync2,
|
|
11739
|
+
writeFileSync as writeFileSync3
|
|
11507
11740
|
} from "node:fs";
|
|
11508
|
-
import { randomUUID } from "node:crypto";
|
|
11509
|
-
import { join as
|
|
11510
|
-
var
|
|
11511
|
-
mkdir: (p, mode) =>
|
|
11512
|
-
chmod: (p, mode) =>
|
|
11513
|
-
writeFile: (p, data, mode) =>
|
|
11514
|
-
rename: (a, b) =>
|
|
11515
|
-
readFile: (p) =>
|
|
11516
|
-
unlink: (p) =>
|
|
11741
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
11742
|
+
import { join as join12 } from "node:path";
|
|
11743
|
+
var REAL_FS2 = {
|
|
11744
|
+
mkdir: (p, mode) => mkdirSync2(p, { recursive: true, mode }),
|
|
11745
|
+
chmod: (p, mode) => chmodSync2(p, mode),
|
|
11746
|
+
writeFile: (p, data, mode) => writeFileSync3(p, data, { mode }),
|
|
11747
|
+
rename: (a, b) => renameSync2(a, b),
|
|
11748
|
+
readFile: (p) => readFileSync8(p, "utf8"),
|
|
11749
|
+
unlink: (p) => unlinkSync2(p)
|
|
11517
11750
|
};
|
|
11518
11751
|
function securecomsDir(home) {
|
|
11519
|
-
return
|
|
11752
|
+
return join12(home, ".securecoms");
|
|
11520
11753
|
}
|
|
11521
11754
|
function connectionsDir(home) {
|
|
11522
|
-
return
|
|
11755
|
+
return join12(securecomsDir(home), "connections");
|
|
11523
11756
|
}
|
|
11524
11757
|
function connectionFilePath(home, identityId) {
|
|
11525
|
-
return
|
|
11758
|
+
return join12(connectionsDir(home), `${identityId}.json`);
|
|
11526
11759
|
}
|
|
11527
11760
|
function newInstanceId() {
|
|
11528
|
-
return
|
|
11761
|
+
return randomUUID2();
|
|
11529
11762
|
}
|
|
11530
11763
|
function buildConnectionFile(args2) {
|
|
11531
11764
|
return {
|
|
@@ -11539,18 +11772,18 @@ function buildConnectionFile(args2) {
|
|
|
11539
11772
|
written_at: args2.now
|
|
11540
11773
|
};
|
|
11541
11774
|
}
|
|
11542
|
-
function writeConnectionFile(home, doc, deps =
|
|
11775
|
+
function writeConnectionFile(home, doc, deps = REAL_FS2) {
|
|
11543
11776
|
deps.mkdir(securecomsDir(home), 448);
|
|
11544
11777
|
deps.chmod(securecomsDir(home), 448);
|
|
11545
11778
|
deps.mkdir(connectionsDir(home), 448);
|
|
11546
11779
|
deps.chmod(connectionsDir(home), 448);
|
|
11547
11780
|
const path = connectionFilePath(home, doc.identity_id);
|
|
11548
|
-
const tmp = `${path}.${process.pid}.${
|
|
11781
|
+
const tmp = `${path}.${process.pid}.${randomUUID2()}.tmp`;
|
|
11549
11782
|
deps.writeFile(tmp, JSON.stringify(doc, null, 2) + "\n", 384);
|
|
11550
11783
|
deps.chmod(tmp, 384);
|
|
11551
11784
|
deps.rename(tmp, path);
|
|
11552
11785
|
}
|
|
11553
|
-
function removeConnectionFileIfOwned(home, identityId, instanceId, deps =
|
|
11786
|
+
function removeConnectionFileIfOwned(home, identityId, instanceId, deps = REAL_FS2) {
|
|
11554
11787
|
const path = connectionFilePath(home, identityId);
|
|
11555
11788
|
try {
|
|
11556
11789
|
const cur = JSON.parse(deps.readFile(path));
|
|
@@ -11591,10 +11824,10 @@ function realServiceDeps() {
|
|
|
11591
11824
|
const r = spawnSync(cmd, args2, { encoding: "utf8" });
|
|
11592
11825
|
return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
11593
11826
|
},
|
|
11594
|
-
writeFile: (p, data) =>
|
|
11827
|
+
writeFile: (p, data) => writeFileSync4(p, data, { mode: 420 }),
|
|
11595
11828
|
removeFile: (p) => rmSync(p, { force: true }),
|
|
11596
|
-
mkdirp: (p) =>
|
|
11597
|
-
fileExists: (p) =>
|
|
11829
|
+
mkdirp: (p) => mkdirSync3(p, { recursive: true }),
|
|
11830
|
+
fileExists: (p) => existsSync3(p),
|
|
11598
11831
|
user: userInfo().username
|
|
11599
11832
|
};
|
|
11600
11833
|
}
|
|
@@ -11617,7 +11850,7 @@ function realResolveNodeDeps() {
|
|
|
11617
11850
|
};
|
|
11618
11851
|
return {
|
|
11619
11852
|
realpath: (p) => realpathSync(p),
|
|
11620
|
-
fileExists: (p) =>
|
|
11853
|
+
fileExists: (p) => existsSync3(p),
|
|
11621
11854
|
nodeMajorOf: majorOf,
|
|
11622
11855
|
arch: () => process.arch,
|
|
11623
11856
|
archOf: (p) => {
|
|
@@ -11632,17 +11865,17 @@ function realResolveDeps() {
|
|
|
11632
11865
|
const r = spawnSync(cmd, args2, { encoding: "utf8" });
|
|
11633
11866
|
return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
11634
11867
|
},
|
|
11635
|
-
fileExists: (p) =>
|
|
11868
|
+
fileExists: (p) => existsSync3(p)
|
|
11636
11869
|
};
|
|
11637
11870
|
}
|
|
11638
11871
|
function realPrepDeps() {
|
|
11639
11872
|
return {
|
|
11640
|
-
readFile: (p) =>
|
|
11641
|
-
writeFileMode: (p, data, mode) =>
|
|
11873
|
+
readFile: (p) => readFileSync9(p, "utf8"),
|
|
11874
|
+
writeFileMode: (p, data, mode) => writeFileSync4(p, data, { mode }),
|
|
11642
11875
|
mkdirp: (p, mode) => {
|
|
11643
|
-
|
|
11876
|
+
mkdirSync3(p, { recursive: true, mode: mode ?? 493 });
|
|
11644
11877
|
},
|
|
11645
|
-
fileExists: (p) =>
|
|
11878
|
+
fileExists: (p) => existsSync3(p),
|
|
11646
11879
|
realpath: (p) => realpathSync(p),
|
|
11647
11880
|
listDir: (p) => {
|
|
11648
11881
|
try {
|
|
@@ -11685,7 +11918,7 @@ function realGetHealth(port) {
|
|
|
11685
11918
|
}
|
|
11686
11919
|
function realReadFleet() {
|
|
11687
11920
|
return readFleet({
|
|
11688
|
-
home:
|
|
11921
|
+
home: homedir3(),
|
|
11689
11922
|
listDir: (p) => {
|
|
11690
11923
|
try {
|
|
11691
11924
|
return readdirSync2(p);
|
|
@@ -11693,7 +11926,7 @@ function realReadFleet() {
|
|
|
11693
11926
|
return [];
|
|
11694
11927
|
}
|
|
11695
11928
|
},
|
|
11696
|
-
readFile: (p) =>
|
|
11929
|
+
readFile: (p) => readFileSync9(p, "utf8")
|
|
11697
11930
|
});
|
|
11698
11931
|
}
|
|
11699
11932
|
function runStatus() {
|
|
@@ -11715,62 +11948,62 @@ function runStatus() {
|
|
|
11715
11948
|
}
|
|
11716
11949
|
function realCopyConnector(srcDir, dstDir, harness) {
|
|
11717
11950
|
const allowedExt = CONNECTOR_FILE_EXTENSIONS[harness];
|
|
11718
|
-
|
|
11951
|
+
mkdirSync3(dstDir, { recursive: true });
|
|
11719
11952
|
for (const name of readdirSync2(srcDir)) {
|
|
11720
|
-
const srcPath =
|
|
11721
|
-
if (
|
|
11953
|
+
const srcPath = join13(srcDir, name);
|
|
11954
|
+
if (statSync4(srcPath).isDirectory()) {
|
|
11722
11955
|
if (CONNECTOR_SKIP_DIRS.has(name)) continue;
|
|
11723
|
-
realCopyConnector(srcPath,
|
|
11956
|
+
realCopyConnector(srcPath, join13(dstDir, name), harness);
|
|
11724
11957
|
continue;
|
|
11725
11958
|
}
|
|
11726
11959
|
if (name.includes(".test.")) continue;
|
|
11727
11960
|
if (!allowedExt.some((ext) => name.endsWith(ext))) continue;
|
|
11728
|
-
|
|
11961
|
+
writeFileSync4(join13(dstDir, name), readFileSync9(srcPath));
|
|
11729
11962
|
}
|
|
11730
11963
|
}
|
|
11731
11964
|
function realWriteBinding(dstDir, identityId) {
|
|
11732
|
-
|
|
11733
|
-
|
|
11965
|
+
mkdirSync3(dstDir, { recursive: true });
|
|
11966
|
+
writeFileSync4(join13(dstDir, "securecoms.binding.json"), JSON.stringify({ identity_id: identityId }) + "\n", { mode: 384 });
|
|
11734
11967
|
}
|
|
11735
11968
|
function realReadBinding(dstDir) {
|
|
11736
11969
|
try {
|
|
11737
|
-
const parsed = JSON.parse(
|
|
11970
|
+
const parsed = JSON.parse(readFileSync9(join13(dstDir, "securecoms.binding.json"), "utf8"));
|
|
11738
11971
|
return parsed.identity_id ?? null;
|
|
11739
11972
|
} catch {
|
|
11740
11973
|
return null;
|
|
11741
11974
|
}
|
|
11742
11975
|
}
|
|
11743
11976
|
function realRemoveBinding(dstDir) {
|
|
11744
|
-
rmSync(
|
|
11977
|
+
rmSync(join13(dstDir, "securecoms.binding.json"), { force: true });
|
|
11745
11978
|
}
|
|
11746
11979
|
function realStatDir(p) {
|
|
11747
11980
|
try {
|
|
11748
|
-
return
|
|
11981
|
+
return statSync4(p).isDirectory();
|
|
11749
11982
|
} catch {
|
|
11750
11983
|
return false;
|
|
11751
11984
|
}
|
|
11752
11985
|
}
|
|
11753
11986
|
function realReadHermesConfig(p) {
|
|
11754
11987
|
try {
|
|
11755
|
-
return
|
|
11988
|
+
return readFileSync9(p, "utf8");
|
|
11756
11989
|
} catch {
|
|
11757
11990
|
return null;
|
|
11758
11991
|
}
|
|
11759
11992
|
}
|
|
11760
11993
|
function realBackupHermesConfig(p, originalText) {
|
|
11761
|
-
|
|
11994
|
+
writeFileSync4(hermesConfigBackupPath(p, Date.now(), process.pid), originalText, "utf8");
|
|
11762
11995
|
}
|
|
11763
11996
|
function realWriteHermesConfig(p, newText) {
|
|
11764
|
-
|
|
11997
|
+
writeFileSync4(p, newText, "utf8");
|
|
11765
11998
|
}
|
|
11766
11999
|
function realWireArgv() {
|
|
11767
12000
|
if (process.argv.includes("--harness")) return process.argv;
|
|
11768
|
-
const detected = detectHarness(
|
|
12001
|
+
const detected = detectHarness(homedir3(), realStatDir);
|
|
11769
12002
|
return detected ? [...process.argv, "--harness", detected] : process.argv;
|
|
11770
12003
|
}
|
|
11771
12004
|
function realWireDeps() {
|
|
11772
12005
|
return {
|
|
11773
|
-
home:
|
|
12006
|
+
home: homedir3(),
|
|
11774
12007
|
argv: realWireArgv(),
|
|
11775
12008
|
readFleet: realReadFleet,
|
|
11776
12009
|
copyConnector: realCopyConnector,
|
|
@@ -11797,7 +12030,7 @@ async function runServiceVerb(verb) {
|
|
|
11797
12030
|
const label = argOrEnv("--label", "SECURECOMS_LABEL");
|
|
11798
12031
|
if (verb === "install" ? !credentialPath : !credentialPath && !label) {
|
|
11799
12032
|
const execPathUsage = verb === "install" ? " [--exec-path <path>]" : "";
|
|
11800
|
-
process.stderr.write(`usage: agent-bridge ${verb} --credential <path> [--label <name>] [--store <path>] [--port N] [--harness openclaw|hermes|
|
|
12033
|
+
process.stderr.write(`usage: agent-bridge ${verb} --credential <path> [--label <name>] [--store <path>] [--port N] [--harness openclaw|hermes|universal]${execPathUsage}
|
|
11801
12034
|
`);
|
|
11802
12035
|
process.exit(2);
|
|
11803
12036
|
}
|
|
@@ -11822,7 +12055,7 @@ async function runServiceVerb(verb) {
|
|
|
11822
12055
|
explicitPort: portStr ? Number(portStr) : void 0,
|
|
11823
12056
|
harness: installHarness,
|
|
11824
12057
|
allowRebind: process.argv.includes("--rebind"),
|
|
11825
|
-
home:
|
|
12058
|
+
home: homedir3(),
|
|
11826
12059
|
connectorsSrcDir: connectorsSrcDir(import.meta.url, installHarness === "hermes" ? "hermes" : "openclaw"),
|
|
11827
12060
|
autoWireDeps: {
|
|
11828
12061
|
statDir: realStatDir,
|
|
@@ -11849,7 +12082,7 @@ async function runServiceVerb(verb) {
|
|
|
11849
12082
|
label,
|
|
11850
12083
|
execPath: installExecPath,
|
|
11851
12084
|
binScript,
|
|
11852
|
-
home:
|
|
12085
|
+
home: homedir3(),
|
|
11853
12086
|
uid: userInfo().uid
|
|
11854
12087
|
})
|
|
11855
12088
|
});
|
|
@@ -11863,7 +12096,7 @@ async function runServiceVerb(verb) {
|
|
|
11863
12096
|
label,
|
|
11864
12097
|
execPath: process.execPath,
|
|
11865
12098
|
binScript,
|
|
11866
|
-
home:
|
|
12099
|
+
home: homedir3(),
|
|
11867
12100
|
uid: userInfo().uid
|
|
11868
12101
|
};
|
|
11869
12102
|
const r = uninstallService(input, realServiceDeps());
|
|
@@ -11914,11 +12147,11 @@ async function main() {
|
|
|
11914
12147
|
`);
|
|
11915
12148
|
process.exit(1);
|
|
11916
12149
|
}
|
|
11917
|
-
const tokenFile = daemon.tokenFile ??
|
|
12150
|
+
const tokenFile = daemon.tokenFile ?? join13(dirname7(credentialPath), "bridge-token");
|
|
11918
12151
|
const token = resolveToken({ tokenFile, token: daemon.token });
|
|
11919
12152
|
const agent = new BridgeAgent(cfg);
|
|
11920
12153
|
await agent.start();
|
|
11921
|
-
const tls = daemon.tls ? { cert:
|
|
12154
|
+
const tls = daemon.tls ? { cert: readFileSync9(daemon.tlsCert, "utf8"), key: readFileSync9(daemon.tlsKey, "utf8") } : void 0;
|
|
11922
12155
|
const { server, closeAllSseStreams } = createHttpServer(agent, { token, allowOrigins: daemon.allowOrigins, tls, websocket: true });
|
|
11923
12156
|
const agentWs = attachAgentWebSocket(server, agent, { token, allowOrigins: daemon.allowOrigins });
|
|
11924
12157
|
let down = false;
|
|
@@ -11940,7 +12173,7 @@ async function main() {
|
|
|
11940
12173
|
const instanceId = newInstanceId();
|
|
11941
12174
|
const identityId = agent.health().identity_id;
|
|
11942
12175
|
const slug = slugFromCredentialPath(credentialPath);
|
|
11943
|
-
process.once("exit", () => removeConnectionFileIfOwned(
|
|
12176
|
+
process.once("exit", () => removeConnectionFileIfOwned(homedir3(), identityId, instanceId));
|
|
11944
12177
|
server.listen(daemon.port, daemon.bind, () => {
|
|
11945
12178
|
const scheme = tls ? "https" : "http";
|
|
11946
12179
|
const wsScheme = tls ? "wss" : "ws";
|
|
@@ -11951,7 +12184,7 @@ async function main() {
|
|
|
11951
12184
|
const addr = server.address();
|
|
11952
12185
|
const boundPort = typeof addr === "object" && addr ? addr.port : daemon.port;
|
|
11953
12186
|
try {
|
|
11954
|
-
writeConnectionFile(
|
|
12187
|
+
writeConnectionFile(homedir3(), buildConnectionFile({
|
|
11955
12188
|
identityId,
|
|
11956
12189
|
port: boundPort,
|
|
11957
12190
|
tokenFile,
|
package/dist/bridge-agent.d.ts
CHANGED
|
@@ -144,6 +144,13 @@ export interface BridgeAgentDeps {
|
|
|
144
144
|
transport?: any;
|
|
145
145
|
backend?: any;
|
|
146
146
|
fetchImpl?: typeof fetch;
|
|
147
|
+
/**
|
|
148
|
+
* Round 5 (runtime config delivery): the home directory under which the
|
|
149
|
+
* managed runtime-config directory lives. Injected only by tests, so they can
|
|
150
|
+
* write into a temp dir instead of the developer's real ~; production leaves
|
|
151
|
+
* it undefined and gets homedir().
|
|
152
|
+
*/
|
|
153
|
+
home?: string;
|
|
147
154
|
/** Injectable so tests can assert recorded stages; defaults to a fresh one. */
|
|
148
155
|
latency?: LatencyRecorder;
|
|
149
156
|
/**
|
|
@@ -341,6 +348,8 @@ export declare class BridgeAgent {
|
|
|
341
348
|
private readonly backend;
|
|
342
349
|
private readonly custody;
|
|
343
350
|
private readonly fetchImpl;
|
|
351
|
+
/** Round 5: home for the managed runtime-config directory (test seam). */
|
|
352
|
+
private readonly home;
|
|
344
353
|
private readonly joinState;
|
|
345
354
|
/**
|
|
346
355
|
* Phase 2: lazy channel-metadata cache (channel_id → { is_dm, name, at }) used
|
|
@@ -642,6 +651,21 @@ export declare class BridgeAgent {
|
|
|
642
651
|
* `voice_settings_changed` ping); never throws.
|
|
643
652
|
*/
|
|
644
653
|
refetchVoiceSettings(): Promise<void>;
|
|
654
|
+
/**
|
|
655
|
+
* Fetch this agent's runtime config and apply it to the managed directory the
|
|
656
|
+
* runtime reads (spec §8). Safe to call any time — at start(), or from the
|
|
657
|
+
* receive pump on a `runtime_config_changed` ping — and never throws: a save
|
|
658
|
+
* the owner already made must not be undone by a network blip here, and no
|
|
659
|
+
* shape the backend can send may take the daemon down.
|
|
660
|
+
*
|
|
661
|
+
* Order matters. Prunes are applied BEFORE the memory is read back, so the
|
|
662
|
+
* report the owner sees next is the file as it now stands.
|
|
663
|
+
*
|
|
664
|
+
* Every cycle rewrites, even at an unchanged version: directory permissions
|
|
665
|
+
* are repaired only on a write, so skipping would leave a loosened directory
|
|
666
|
+
* loose until the next save.
|
|
667
|
+
*/
|
|
668
|
+
refetchRuntimeConfig(): Promise<void>;
|
|
645
669
|
/**
|
|
646
670
|
* Enforce the inbound-buffer cap with a DROP-OLDEST policy.
|
|
647
671
|
*
|
package/dist/index.js
CHANGED
|
@@ -4546,6 +4546,9 @@ var FRAME_PARSERS = {
|
|
|
4546
4546
|
}),
|
|
4547
4547
|
voice_settings_changed: (d) => ({
|
|
4548
4548
|
voiceSettingsChanged: { identityId: str(d?.identity_id), version: Number(d?.version ?? 0) }
|
|
4549
|
+
}),
|
|
4550
|
+
runtime_config_changed: (d) => ({
|
|
4551
|
+
runtimeConfigChanged: { identityId: str(d?.identity_id), version: Number(d?.version ?? 0) }
|
|
4549
4552
|
})
|
|
4550
4553
|
};
|
|
4551
4554
|
var S2C_FRAME_NAMES = new Set(Object.keys(FRAME_PARSERS));
|
|
@@ -6100,7 +6103,7 @@ var SqliteMlsStateStore = class {
|
|
|
6100
6103
|
|
|
6101
6104
|
// src/bridge-agent.ts
|
|
6102
6105
|
import { decryptFile as decryptFile2, encryptFile as encryptFile2, computeFileDigest as computeFileDigest2, base64ToBytes as base64ToBytes4, bytesToBase64 as bytesToBase644 } from "@agentvault/crypto";
|
|
6103
|
-
import { createHash } from "node:crypto";
|
|
6106
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
6104
6107
|
|
|
6105
6108
|
// src/service/resolve-bin.ts
|
|
6106
6109
|
import { createRequire } from "node:module";
|
|
@@ -6112,7 +6115,7 @@ function loadPackageMeta(importMetaUrl) {
|
|
|
6112
6115
|
|
|
6113
6116
|
// src/bridge-agent.ts
|
|
6114
6117
|
import { promises as fsp } from "node:fs";
|
|
6115
|
-
import { join as
|
|
6118
|
+
import { join as join7, dirname } from "node:path";
|
|
6116
6119
|
|
|
6117
6120
|
// src/latency.ts
|
|
6118
6121
|
var DEFAULT_CAPACITY = 1024;
|
|
@@ -7308,7 +7311,135 @@ async function runSummarize(opts) {
|
|
|
7308
7311
|
}
|
|
7309
7312
|
}
|
|
7310
7313
|
|
|
7314
|
+
// src/service/runtime-config-dir.ts
|
|
7315
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7316
|
+
import {
|
|
7317
|
+
chmodSync,
|
|
7318
|
+
existsSync,
|
|
7319
|
+
mkdirSync,
|
|
7320
|
+
readFileSync as readFileSync5,
|
|
7321
|
+
renameSync,
|
|
7322
|
+
statSync as statSync2,
|
|
7323
|
+
unlinkSync,
|
|
7324
|
+
writeFileSync
|
|
7325
|
+
} from "node:fs";
|
|
7326
|
+
import { basename as basename2, join as join6 } from "node:path";
|
|
7327
|
+
var MEMORY_FILE = "MEMORY.md";
|
|
7328
|
+
var REAL_FS = {
|
|
7329
|
+
mkdir: (p, mode) => mkdirSync(p, { recursive: true, mode }),
|
|
7330
|
+
chmod: (p, mode) => chmodSync(p, mode),
|
|
7331
|
+
writeFile: (p, data, mode) => writeFileSync(p, data, { mode }),
|
|
7332
|
+
rename: (a, b) => renameSync(a, b),
|
|
7333
|
+
readFile: (p) => readFileSync5(p, "utf8"),
|
|
7334
|
+
unlink: (p) => unlinkSync(p)
|
|
7335
|
+
};
|
|
7336
|
+
function runtimeConfigDir(home, identityId) {
|
|
7337
|
+
return join6(home, ".securecoms", "runtime-config", identityId);
|
|
7338
|
+
}
|
|
7339
|
+
function assertManagedName(name) {
|
|
7340
|
+
if (!name || name.includes("/") || name.includes("\\") || name === "." || name === ".." || name !== basename2(name)) {
|
|
7341
|
+
throw new Error(`refusing managed config file name ${JSON.stringify(name)}: not a plain filename`);
|
|
7342
|
+
}
|
|
7343
|
+
}
|
|
7344
|
+
function atomicWrite(path, body, deps) {
|
|
7345
|
+
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
7346
|
+
deps.writeFile(tmp, body, 384);
|
|
7347
|
+
deps.chmod(tmp, 384);
|
|
7348
|
+
try {
|
|
7349
|
+
deps.rename(tmp, path);
|
|
7350
|
+
} catch (err) {
|
|
7351
|
+
try {
|
|
7352
|
+
deps.unlink(tmp);
|
|
7353
|
+
} catch {
|
|
7354
|
+
}
|
|
7355
|
+
throw err;
|
|
7356
|
+
}
|
|
7357
|
+
}
|
|
7358
|
+
function writeManagedFiles(home, identityId, files, deps = REAL_FS) {
|
|
7359
|
+
const entries = Object.entries(files);
|
|
7360
|
+
for (const [name] of entries) assertManagedName(name);
|
|
7361
|
+
const parent2 = join6(home, ".securecoms", "runtime-config");
|
|
7362
|
+
const dir = runtimeConfigDir(home, identityId);
|
|
7363
|
+
for (const d of [join6(home, ".securecoms"), parent2, dir]) {
|
|
7364
|
+
deps.mkdir(d, 448);
|
|
7365
|
+
deps.chmod(d, 448);
|
|
7366
|
+
}
|
|
7367
|
+
for (const [name, body] of entries) atomicWrite(join6(dir, name), body, deps);
|
|
7368
|
+
}
|
|
7369
|
+
function checkConfiguredPaths(skillsJson, statFn = (p) => {
|
|
7370
|
+
const s = statSync2(p);
|
|
7371
|
+
return { mode: s.mode, size: s.size };
|
|
7372
|
+
}) {
|
|
7373
|
+
const paths = [];
|
|
7374
|
+
const endpoints = skillsJson.endpoints ?? {};
|
|
7375
|
+
for (const e of Object.values(endpoints)) if (e?.headersFile) paths.push(e.headersFile);
|
|
7376
|
+
const databases = skillsJson.databases ?? {};
|
|
7377
|
+
for (const d of Object.values(databases)) {
|
|
7378
|
+
if (d?.sqlite) paths.push(d.sqlite);
|
|
7379
|
+
if (d?.postgresUrlFile) paths.push(d.postgresUrlFile);
|
|
7380
|
+
}
|
|
7381
|
+
return paths.map((path) => {
|
|
7382
|
+
let st;
|
|
7383
|
+
try {
|
|
7384
|
+
st = statFn(path);
|
|
7385
|
+
} catch {
|
|
7386
|
+
return { path, ok: false, why: "not found" };
|
|
7387
|
+
}
|
|
7388
|
+
if ((st.mode & 63) !== 0) {
|
|
7389
|
+
return { path, ok: false, why: "readable by other users on this machine \u2014 tighten its permissions to owner-only" };
|
|
7390
|
+
}
|
|
7391
|
+
if (st.size === 0) return { path, ok: false, why: "the file is empty" };
|
|
7392
|
+
return { path, ok: true, why: "" };
|
|
7393
|
+
});
|
|
7394
|
+
}
|
|
7395
|
+
function lineHash(line) {
|
|
7396
|
+
return createHash("sha256").update(line.trim(), "utf8").digest("hex");
|
|
7397
|
+
}
|
|
7398
|
+
var LOCAL_REPORT_FILE = "local-config.json";
|
|
7399
|
+
function readLocalReport(home, identityId) {
|
|
7400
|
+
const p = join6(runtimeConfigDir(home, identityId), LOCAL_REPORT_FILE);
|
|
7401
|
+
if (!existsSync(p)) return null;
|
|
7402
|
+
let doc;
|
|
7403
|
+
try {
|
|
7404
|
+
doc = JSON.parse(readFileSync5(p, "utf8"));
|
|
7405
|
+
} catch {
|
|
7406
|
+
return null;
|
|
7407
|
+
}
|
|
7408
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
|
|
7409
|
+
const hasSearchKey = doc.has_search_key === true;
|
|
7410
|
+
const raw = doc.persona;
|
|
7411
|
+
const persona = raw && typeof raw === "object" && !Array.isArray(raw) ? Object.fromEntries(
|
|
7412
|
+
Object.entries(raw).filter(([, v]) => typeof v === "string")
|
|
7413
|
+
) : {};
|
|
7414
|
+
return { persona, has_search_key: hasSearchKey };
|
|
7415
|
+
}
|
|
7416
|
+
function readManagedMemory(home, identityId) {
|
|
7417
|
+
const p = join6(runtimeConfigDir(home, identityId), MEMORY_FILE);
|
|
7418
|
+
if (!existsSync(p)) return null;
|
|
7419
|
+
try {
|
|
7420
|
+
const body = readFileSync5(p, "utf8").trim();
|
|
7421
|
+
return body || null;
|
|
7422
|
+
} catch {
|
|
7423
|
+
return null;
|
|
7424
|
+
}
|
|
7425
|
+
}
|
|
7426
|
+
function pruneManagedMemory(home, identityId, hashes, deps = REAL_FS) {
|
|
7427
|
+
const dir = runtimeConfigDir(home, identityId);
|
|
7428
|
+
const path = join6(dir, MEMORY_FILE);
|
|
7429
|
+
if (!existsSync(path)) return 0;
|
|
7430
|
+
const drop = new Set(hashes);
|
|
7431
|
+
const before = deps.readFile(path).split("\n");
|
|
7432
|
+
const kept = before.filter((l) => l.trim() === "" || !drop.has(lineHash(l)));
|
|
7433
|
+
const removed = before.filter((l) => l.trim() !== "" && drop.has(lineHash(l))).length;
|
|
7434
|
+
if (removed === 0) return 0;
|
|
7435
|
+
const body = kept.filter((l, i) => l.trim() !== "" || i < kept.length - 1).join("\n").trimEnd();
|
|
7436
|
+
atomicWrite(path, body ? `${body}
|
|
7437
|
+
` : "", deps);
|
|
7438
|
+
return removed;
|
|
7439
|
+
}
|
|
7440
|
+
|
|
7311
7441
|
// src/bridge-agent.ts
|
|
7442
|
+
import { homedir as homedir2 } from "node:os";
|
|
7312
7443
|
var VOICE_MODE_GUIDANCE = "Reply immediately and conversationally, one short sentence per message; never bundle multiple points into one message.";
|
|
7313
7444
|
var FAST_PATH_SPOKEN_GUIDANCE = "A one-sentence spoken acknowledgement has already been sent for this turn. Reply only with information it did not contain, in one or two short sentences; if there is nothing to add, send nothing.";
|
|
7314
7445
|
var ROOM_VOICE_GUIDANCE_MENTION_ONLY = "You are in a group voice room. Reply ONLY when a message @mentions your display name. Keep replies to one short sentence. When you should not reply, send nothing.";
|
|
@@ -7363,7 +7494,7 @@ function resolveMessageId(messageId) {
|
|
|
7363
7494
|
return messageId;
|
|
7364
7495
|
}
|
|
7365
7496
|
function fastReplyId(messageId) {
|
|
7366
|
-
const h =
|
|
7497
|
+
const h = createHash2("sha256").update(`fast:${messageId}`).digest();
|
|
7367
7498
|
h[6] = h[6] & 15 | 64;
|
|
7368
7499
|
h[8] = h[8] & 63 | 128;
|
|
7369
7500
|
const x = h.subarray(0, 16).toString("hex");
|
|
@@ -7493,6 +7624,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
7493
7624
|
onSupersedeUnopenable: (channelId, groupId) => this.healSupersedeUnopenable(channelId, groupId)
|
|
7494
7625
|
});
|
|
7495
7626
|
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
7627
|
+
this.home = deps.home ?? homedir2();
|
|
7496
7628
|
this.injectedFastLlm = !!deps.fastLlm;
|
|
7497
7629
|
this.fileVoiceConfig = cfg.voice;
|
|
7498
7630
|
this.fastLlm = deps.fastLlm ?? (cfg.voice?.fastPath.enabled ? new FastLlmClient(cfg.voice.fastPath, this.fetchImpl) : void 0);
|
|
@@ -7525,8 +7657,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
7525
7657
|
const j = await res.json();
|
|
7526
7658
|
return String(j.attachment_id);
|
|
7527
7659
|
};
|
|
7528
|
-
this.attachmentsDirImpl = deps.attachmentsDir ??
|
|
7529
|
-
this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(
|
|
7660
|
+
this.attachmentsDirImpl = deps.attachmentsDir ?? join7(dirname(cfg.storePath), "attachments");
|
|
7661
|
+
this.vaultOwnerStoreImpl = deps.vaultOwnerStore ?? new FileOwnerAuthorityStore(join7(dirname(cfg.storePath), "vault-owner-pins.json"));
|
|
7530
7662
|
this.action = {
|
|
7531
7663
|
begin: (channelId) => this.actionBegin(channelId),
|
|
7532
7664
|
start: (opts) => this.actionStart(opts),
|
|
@@ -7542,6 +7674,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
7542
7674
|
// stored for peek-then-join in ingestOnce
|
|
7543
7675
|
custody;
|
|
7544
7676
|
fetchImpl;
|
|
7677
|
+
/** Round 5: home for the managed runtime-config directory (test seam). */
|
|
7678
|
+
home;
|
|
7545
7679
|
joinState = /* @__PURE__ */ new Map();
|
|
7546
7680
|
/**
|
|
7547
7681
|
* Phase 2: lazy channel-metadata cache (channel_id → { is_dm, name, at }) used
|
|
@@ -7883,6 +8017,84 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
7883
8017
|
if (this.voiceSettingsSource === "backend") return;
|
|
7884
8018
|
process.stderr.write(
|
|
7885
8019
|
`[bridge] voice settings unavailable (${e.message}); using ${this.voiceSettingsSource}
|
|
8020
|
+
`
|
|
8021
|
+
);
|
|
8022
|
+
}
|
|
8023
|
+
}
|
|
8024
|
+
/**
|
|
8025
|
+
* Fetch this agent's runtime config and apply it to the managed directory the
|
|
8026
|
+
* runtime reads (spec §8). Safe to call any time — at start(), or from the
|
|
8027
|
+
* receive pump on a `runtime_config_changed` ping — and never throws: a save
|
|
8028
|
+
* the owner already made must not be undone by a network blip here, and no
|
|
8029
|
+
* shape the backend can send may take the daemon down.
|
|
8030
|
+
*
|
|
8031
|
+
* Order matters. Prunes are applied BEFORE the memory is read back, so the
|
|
8032
|
+
* report the owner sees next is the file as it now stands.
|
|
8033
|
+
*
|
|
8034
|
+
* Every cycle rewrites, even at an unchanged version: directory permissions
|
|
8035
|
+
* are repaired only on a write, so skipping would leave a loosened directory
|
|
8036
|
+
* loose until the next save.
|
|
8037
|
+
*/
|
|
8038
|
+
async refetchRuntimeConfig() {
|
|
8039
|
+
const base = `${this.cfg.apiBase}/api/v1/agents/${encodeURIComponent(this.cfg.identityId)}/runtime-config`;
|
|
8040
|
+
try {
|
|
8041
|
+
const res = await this.fetchImpl(`${base}/files`, {
|
|
8042
|
+
headers: { authorization: `Bearer ${this.cfg.apiKey}` }
|
|
8043
|
+
});
|
|
8044
|
+
if (!res.ok) throw new Error(`runtime-config ${res.status}`);
|
|
8045
|
+
const payload = await res.json();
|
|
8046
|
+
if (!payload || !payload.version) {
|
|
8047
|
+
process.stderr.write(
|
|
8048
|
+
`[bridge] runtime config not applied (version ${JSON.stringify(payload?.version ?? null)} \u2014 no saved config)
|
|
8049
|
+
`
|
|
8050
|
+
);
|
|
8051
|
+
return;
|
|
8052
|
+
}
|
|
8053
|
+
const skillsJson = payload.skills_json ?? {};
|
|
8054
|
+
writeManagedFiles(this.home, this.cfg.identityId, {
|
|
8055
|
+
...payload.files ?? {},
|
|
8056
|
+
"skills.json": `${JSON.stringify(skillsJson, null, 2)}
|
|
8057
|
+
`
|
|
8058
|
+
});
|
|
8059
|
+
if (payload.memory_prunes?.length) {
|
|
8060
|
+
pruneManagedMemory(this.home, this.cfg.identityId, payload.memory_prunes);
|
|
8061
|
+
}
|
|
8062
|
+
const local = readLocalReport(this.home, this.cfg.identityId);
|
|
8063
|
+
const report = {
|
|
8064
|
+
applied_version: payload.version,
|
|
8065
|
+
paths: checkConfiguredPaths(skillsJson),
|
|
8066
|
+
// Read AFTER the prunes above, so the owner's next view of memory is
|
|
8067
|
+
// the file as it now stands, not the one they just pruned from.
|
|
8068
|
+
memory: readManagedMemory(this.home, this.cfg.identityId),
|
|
8069
|
+
// E13: pass through what the runtime reported. The daemon does not
|
|
8070
|
+
// interpret any of it.
|
|
8071
|
+
local_persona: local?.persona ?? null,
|
|
8072
|
+
// Only the runtime knows whether a search key file is configured — it
|
|
8073
|
+
// is an environment value on that process, invisible to the daemon and
|
|
8074
|
+
// to the backend. The backend needs it to generate a truthful tool
|
|
8075
|
+
// paragraph, so it rides along here.
|
|
8076
|
+
has_search_key: local?.has_search_key ?? false
|
|
8077
|
+
};
|
|
8078
|
+
const posted = await this.fetchImpl(`${base}/report`, {
|
|
8079
|
+
method: "POST",
|
|
8080
|
+
headers: {
|
|
8081
|
+
authorization: `Bearer ${this.cfg.apiKey}`,
|
|
8082
|
+
"content-type": "application/json"
|
|
8083
|
+
},
|
|
8084
|
+
body: JSON.stringify(report)
|
|
8085
|
+
});
|
|
8086
|
+
if (!posted.ok) {
|
|
8087
|
+
process.stderr.write(
|
|
8088
|
+
`[bridge] runtime config v${payload.version} written, but reporting it failed (${posted.status}) \u2014 the console will keep showing it as not applied until the next cycle
|
|
8089
|
+
`
|
|
8090
|
+
);
|
|
8091
|
+
return;
|
|
8092
|
+
}
|
|
8093
|
+
process.stderr.write(`[bridge] runtime config v${payload.version} applied
|
|
8094
|
+
`);
|
|
8095
|
+
} catch (e) {
|
|
8096
|
+
process.stderr.write(
|
|
8097
|
+
`[bridge] runtime config refetch failed: ${e?.message ?? e}
|
|
7886
8098
|
`
|
|
7887
8099
|
);
|
|
7888
8100
|
}
|
|
@@ -7914,6 +8126,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
7914
8126
|
}
|
|
7915
8127
|
await this.provider.publishKeyPackage();
|
|
7916
8128
|
await this.refetchVoiceSettings();
|
|
8129
|
+
await this.refetchRuntimeConfig();
|
|
7917
8130
|
this.startReceivePump();
|
|
7918
8131
|
if (this.selfHealSweepEnabled) {
|
|
7919
8132
|
this.bootSweepTimer = setTimeout(
|
|
@@ -9398,11 +9611,16 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9398
9611
|
this.pingsReceived++;
|
|
9399
9612
|
if (ping?.reconnected) {
|
|
9400
9613
|
process.stderr.write("[bridge] delivery-ping WS (re)connected \u2014 push inbound active\n");
|
|
9614
|
+
await this.refetchRuntimeConfig();
|
|
9401
9615
|
}
|
|
9402
9616
|
if (ping?.voiceSettingsChanged) {
|
|
9403
9617
|
await this.refetchVoiceSettings();
|
|
9404
9618
|
continue;
|
|
9405
9619
|
}
|
|
9620
|
+
if (ping?.runtimeConfigChanged) {
|
|
9621
|
+
await this.refetchRuntimeConfig();
|
|
9622
|
+
continue;
|
|
9623
|
+
}
|
|
9406
9624
|
await this.ingestOnce();
|
|
9407
9625
|
}
|
|
9408
9626
|
} catch {
|
|
@@ -9840,13 +10058,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
9840
10058
|
};
|
|
9841
10059
|
|
|
9842
10060
|
// src/config.ts
|
|
9843
|
-
import { readFileSync as
|
|
9844
|
-
import { dirname as dirname2, join as
|
|
10061
|
+
import { readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
|
|
10062
|
+
import { dirname as dirname2, join as join8 } from "node:path";
|
|
9845
10063
|
var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
|
|
9846
10064
|
function loadConfig(opts) {
|
|
9847
|
-
const raw =
|
|
10065
|
+
const raw = readFileSync6(opts.credentialPath, "utf8");
|
|
9848
10066
|
try {
|
|
9849
|
-
const mode =
|
|
10067
|
+
const mode = statSync3(opts.credentialPath).mode & 511;
|
|
9850
10068
|
if (mode & 63) process.stderr.write(`[bridge] WARNING: credential file ${opts.credentialPath} permission ${mode.toString(8)} is not 0600 \u2014 tighten it (chmod 600).
|
|
9851
10069
|
`);
|
|
9852
10070
|
} catch {
|
|
@@ -9858,7 +10076,7 @@ function loadConfig(opts) {
|
|
|
9858
10076
|
throw new Error("credential file is not valid JSON");
|
|
9859
10077
|
}
|
|
9860
10078
|
for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new Error(`credential file missing required field: ${k}`);
|
|
9861
|
-
const storePath = opts.storePath ??
|
|
10079
|
+
const storePath = opts.storePath ?? join8(dirname2(opts.credentialPath), "bridge-state.db");
|
|
9862
10080
|
const agentName = typeof json2.agent_name === "string" && json2.agent_name ? json2.agent_name : typeof json2.display_name === "string" && json2.display_name ? json2.display_name : void 0;
|
|
9863
10081
|
const rateLimitCapacity = typeof json2.rate_limit_capacity === "number" ? json2.rate_limit_capacity : 20;
|
|
9864
10082
|
const rateLimitRefillPerMinute = typeof json2.rate_limit_refill_per_minute === "number" ? json2.rate_limit_refill_per_minute : 20;
|
|
@@ -9954,13 +10172,13 @@ import { z as z3 } from "zod";
|
|
|
9954
10172
|
|
|
9955
10173
|
// src/auth.ts
|
|
9956
10174
|
import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
|
|
9957
|
-
import { existsSync, readFileSync as
|
|
10175
|
+
import { existsSync as existsSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
|
|
9958
10176
|
function resolveToken(opts) {
|
|
9959
10177
|
if (opts.token) return opts.token;
|
|
9960
10178
|
const f = opts.tokenFile;
|
|
9961
|
-
if (f &&
|
|
10179
|
+
if (f && existsSync2(f)) return readFileSync7(f, "utf8").trim();
|
|
9962
10180
|
const tok = randomBytes3(32).toString("base64url");
|
|
9963
|
-
if (f)
|
|
10181
|
+
if (f) writeFileSync2(f, tok, { mode: 384 });
|
|
9964
10182
|
return tok;
|
|
9965
10183
|
}
|
|
9966
10184
|
function tokenMatches(provided, expected) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export declare const MEMORY_FILE = "MEMORY.md";
|
|
2
|
+
export interface PathCheck {
|
|
3
|
+
path: string;
|
|
4
|
+
ok: boolean;
|
|
5
|
+
why: string;
|
|
6
|
+
}
|
|
7
|
+
export interface FsDeps {
|
|
8
|
+
mkdir: (p: string, mode: number) => void;
|
|
9
|
+
chmod: (p: string, mode: number) => void;
|
|
10
|
+
writeFile: (p: string, data: string, mode: number) => void;
|
|
11
|
+
rename: (a: string, b: string) => void;
|
|
12
|
+
readFile: (p: string) => string;
|
|
13
|
+
unlink: (p: string) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function runtimeConfigDir(home: string, identityId: string): string;
|
|
16
|
+
export declare function writeManagedFiles(home: string, identityId: string, files: Record<string, string>, deps?: FsDeps): void;
|
|
17
|
+
/** Every path skills.json names, checked on disk. Contents are never read. */
|
|
18
|
+
export declare function checkConfiguredPaths(skillsJson: Record<string, unknown>, statFn?: (p: string) => {
|
|
19
|
+
mode: number;
|
|
20
|
+
size: number;
|
|
21
|
+
}): PathCheck[];
|
|
22
|
+
export declare function lineHash(line: string): string;
|
|
23
|
+
export declare const LOCAL_REPORT_FILE = "local-config.json";
|
|
24
|
+
/**
|
|
25
|
+
* What the runtime says its OWN persona files currently contain (spec E13).
|
|
26
|
+
*
|
|
27
|
+
* The runtime writes this; the daemon only reads it. It exists because the
|
|
28
|
+
* daemon cannot see the runtime's data dir, so without it an owner's
|
|
29
|
+
* hand-written persona would be invisible in the web app and the first save
|
|
30
|
+
* would replace it sight unseen. It is inert: nothing here decides behaviour,
|
|
31
|
+
* and it never affects which file the runtime actually reads.
|
|
32
|
+
*/
|
|
33
|
+
export interface LocalReport {
|
|
34
|
+
persona: Record<string, string>;
|
|
35
|
+
has_search_key: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare function readLocalReport(home: string, identityId: string): LocalReport | null;
|
|
38
|
+
export declare function readManagedMemory(home: string, identityId: string): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Remove exactly the named lines. Line-level, never a whole-file replace: the
|
|
41
|
+
* agent may append a fact between the owner reading and pruning, and that fact
|
|
42
|
+
* must survive (spec §7).
|
|
43
|
+
*/
|
|
44
|
+
export declare function pruneManagedMemory(home: string, identityId: string, hashes: string[], deps?: FsDeps): number;
|