@agentvault/claude-bridge 0.7.16 → 0.8.1
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/dist/bridge.d.ts +9 -0
- package/dist/config.d.ts +44 -0
- package/dist/index.js +1207 -298
- package/dist/log.d.ts +12 -0
- package/dist/pending-invite.d.ts +24 -0
- package/dist/service/backend.d.ts +15 -0
- package/dist/service/backend.d.ts.map +1 -1
- package/dist/service/doctor-cli.d.ts +36 -0
- package/dist/service/doctor-cli.d.ts.map +1 -0
- package/dist/service/doctor.d.ts +93 -0
- package/dist/service/doctor.d.ts.map +1 -0
- package/dist/service/launchd.d.ts +27 -1
- package/dist/service/launchd.d.ts.map +1 -1
- package/dist/service/self-uninstall.d.ts +19 -0
- package/dist/service/self-uninstall.d.ts.map +1 -0
- package/dist/service/subcommand.d.ts +12 -0
- package/dist/service/subcommand.d.ts.map +1 -1
- package/dist/service/systemd.d.ts +19 -1
- package/dist/service/systemd.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,36 +9,138 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// src/pending-invite.ts
|
|
13
|
+
import { existsSync, mkdirSync, chmodSync, readFileSync as readFileSync2, writeFileSync, rmSync } from "node:fs";
|
|
14
|
+
import { join as join5 } from "node:path";
|
|
15
|
+
function pendingInvitePath(dataDir) {
|
|
16
|
+
return join5(dataDir, PENDING_INVITE_FILE);
|
|
17
|
+
}
|
|
18
|
+
function writePendingInvite(dataDir, token) {
|
|
19
|
+
mkdirSync(dataDir, { recursive: true, mode: DIR_MODE2 });
|
|
20
|
+
chmodSync(dataDir, DIR_MODE2);
|
|
21
|
+
const p2 = pendingInvitePath(dataDir);
|
|
22
|
+
writeFileSync(p2, token, { mode: FILE_MODE2 });
|
|
23
|
+
chmodSync(p2, FILE_MODE2);
|
|
24
|
+
}
|
|
25
|
+
function readPendingInvite(dataDir) {
|
|
26
|
+
const p2 = pendingInvitePath(dataDir);
|
|
27
|
+
if (!existsSync(p2)) return void 0;
|
|
28
|
+
try {
|
|
29
|
+
const raw = readFileSync2(p2, "utf-8").trim();
|
|
30
|
+
return raw.length > 0 ? raw : void 0;
|
|
31
|
+
} catch {
|
|
32
|
+
return void 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function clearPendingInvite(dataDir) {
|
|
36
|
+
try {
|
|
37
|
+
rmSync(pendingInvitePath(dataDir), { force: true });
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function clearPendingInviteIfConsumed(dataDir, credsExist) {
|
|
42
|
+
if (!existsSync(pendingInvitePath(dataDir))) return false;
|
|
43
|
+
if (!credsExist(dataDir)) return false;
|
|
44
|
+
clearPendingInvite(dataDir);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
var DIR_MODE2, FILE_MODE2, PENDING_INVITE_FILE;
|
|
48
|
+
var init_pending_invite = __esm({
|
|
49
|
+
"src/pending-invite.ts"() {
|
|
50
|
+
"use strict";
|
|
51
|
+
DIR_MODE2 = 448;
|
|
52
|
+
FILE_MODE2 = 384;
|
|
53
|
+
PENDING_INVITE_FILE = "pending-invite";
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// src/log.ts
|
|
58
|
+
import { format } from "node:util";
|
|
59
|
+
function stamp(message) {
|
|
60
|
+
const ts2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
61
|
+
return message.split("\n").map((line) => `${ts2} ${line}`).join("\n");
|
|
62
|
+
}
|
|
63
|
+
function logLine(...args) {
|
|
64
|
+
console.error(stamp(format(...args)));
|
|
65
|
+
}
|
|
66
|
+
function printLine(text) {
|
|
67
|
+
console.log(text);
|
|
68
|
+
}
|
|
69
|
+
var init_log = __esm({
|
|
70
|
+
"src/log.ts"() {
|
|
71
|
+
"use strict";
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
12
75
|
// src/config.ts
|
|
13
|
-
import { existsSync, readFileSync as
|
|
14
|
-
import { join as
|
|
76
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync2, chmodSync as chmodSync2 } from "node:fs";
|
|
77
|
+
import { join as join6, resolve as resolve2, sep, dirname } from "node:path";
|
|
78
|
+
import { tmpdir } from "node:os";
|
|
79
|
+
function isEphemeralEntrypoint(entrypoint) {
|
|
80
|
+
return entrypoint.includes("/_npx/") || entrypoint.includes("/dlx-") || entrypoint.includes("/.bun/install/cache/") || entrypoint.startsWith(tmpdir() + sep);
|
|
81
|
+
}
|
|
82
|
+
function assertInstallableEntrypoint(entrypoint) {
|
|
83
|
+
if (isEphemeralEntrypoint(entrypoint)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`cannot install a background service from an ephemeral package cache (${entrypoint}) \u2014 it is deleted on cleanup and the service would silently die later. Install globally first:
|
|
86
|
+
npm i -g @agentvault/claude-bridge && agentvault-claude-bridge install`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
15
90
|
function hasRecoverableBackup(dataDir) {
|
|
16
91
|
try {
|
|
17
|
-
const parsed = JSON.parse(
|
|
92
|
+
const parsed = JSON.parse(readFileSync3(join6(dataDir, BACKUP_FILE), "utf-8"));
|
|
18
93
|
return !!(parsed && parsed.deviceId && parsed.deviceJwt && parsed.sessions && Object.keys(parsed.sessions).length > 0);
|
|
19
94
|
} catch {
|
|
20
95
|
return false;
|
|
21
96
|
}
|
|
22
97
|
}
|
|
98
|
+
function readPersistedDeviceId(dataDir) {
|
|
99
|
+
for (const f7 of [...CRED_FILES, BACKUP_FILE]) {
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(readFileSync3(join6(dataDir, f7), "utf-8"));
|
|
102
|
+
const id = parsed?.deviceId;
|
|
103
|
+
if (typeof id === "string" && id) return id;
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
23
109
|
function hasPersistedCreds(dataDir) {
|
|
24
|
-
if (CRED_FILES.some((f7) =>
|
|
110
|
+
if (CRED_FILES.some((f7) => existsSync2(join6(dataDir, f7)))) return true;
|
|
25
111
|
return hasRecoverableBackup(dataDir);
|
|
26
112
|
}
|
|
27
113
|
function slugify2(name) {
|
|
28
114
|
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
29
115
|
}
|
|
30
116
|
function resolveDataDir(env) {
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
117
|
+
const supplied = env.AV_AGENT_NAME?.trim();
|
|
118
|
+
const agentName = supplied || LEGACY_DEFAULT_AGENT_NAME;
|
|
119
|
+
const agentNameSource = supplied ? "supplied" : "defaulted";
|
|
120
|
+
if (env.AV_DATA_DIR)
|
|
121
|
+
return { dataDir: env.AV_DATA_DIR, source: "explicit", agentName, agentNameSource };
|
|
122
|
+
const base = join6(env.HOME ?? "", ".agentvault", "claude-room-bridge");
|
|
123
|
+
const perAgent = join6(base, slugify2(agentName));
|
|
124
|
+
if (hasPersistedCreds(perAgent))
|
|
125
|
+
return { dataDir: perAgent, source: "per-agent", agentName, agentNameSource };
|
|
126
|
+
if (hasPersistedCreds(base))
|
|
127
|
+
return { dataDir: base, source: "legacy", agentName, agentNameSource };
|
|
128
|
+
if (!supplied) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`AV_AGENT_NAME is required \u2014 it names WHICH agent this bridge runs as, and therefore where its credentials live. No credentials were found at ${perAgent} or ${base}, so there is no existing install to infer a name from. Re-run with AV_AGENT_NAME=<agent-name> set.`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return { dataDir: perAgent, source: "per-agent", agentName, agentNameSource };
|
|
37
134
|
}
|
|
38
135
|
function loadConfig(env, argv = []) {
|
|
39
|
-
const { dataDir, source: dataDirSource } = resolveDataDir(env);
|
|
40
|
-
|
|
41
|
-
|
|
136
|
+
const { dataDir, source: dataDirSource, agentName, agentNameSource } = resolveDataDir(env);
|
|
137
|
+
if (agentNameSource === "defaulted") {
|
|
138
|
+
logLine(
|
|
139
|
+
`[config] AV_AGENT_NAME was not set \u2014 running as "${agentName}" from the existing credentials at ${dataDir}. Set AV_AGENT_NAME to name this agent explicitly; a future install will require it.`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const inviteToken = (argv[0] && !argv[0].startsWith("-") ? argv[0] : "") || env.AV_INVITE_TOKEN || readPendingInvite(dataDir) || "";
|
|
143
|
+
const workspaceDir = env.AV_WORKSPACE_DIR || join6(dirname(dataDir), "workspaces", slugify2(agentName));
|
|
42
144
|
const wsReal = resolve2(workspaceDir);
|
|
43
145
|
const ddReal = resolve2(dataDir);
|
|
44
146
|
if (wsReal === ddReal || wsReal.startsWith(ddReal + sep) || ddReal.startsWith(wsReal + sep)) {
|
|
@@ -47,8 +149,8 @@ function loadConfig(env, argv = []) {
|
|
|
47
149
|
);
|
|
48
150
|
}
|
|
49
151
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
152
|
+
mkdirSync2(workspaceDir, { recursive: true, mode: 448 });
|
|
153
|
+
chmodSync2(workspaceDir, 448);
|
|
52
154
|
} catch (e7) {
|
|
53
155
|
throw new Error(
|
|
54
156
|
`Failed to create/secure the agent workspace at ${workspaceDir} (override with AV_WORKSPACE_DIR): ${e7.message}`
|
|
@@ -83,8 +185,10 @@ function loadConfig(env, argv = []) {
|
|
|
83
185
|
apiUrl: env.AV_API_URL ?? "https://api.agentvault.chat",
|
|
84
186
|
// Identity shown to the agent's own session. Set this to the agent's
|
|
85
187
|
// AgentVault name via AV_AGENT_NAME (the native connect command passes it).
|
|
86
|
-
//
|
|
87
|
-
|
|
188
|
+
// Resolved in resolveDataDir so the name, the data dir and the workspace dir
|
|
189
|
+
// can never disagree about who this agent is (#921/#927).
|
|
190
|
+
agentName,
|
|
191
|
+
agentNameSource,
|
|
88
192
|
roomFilter: env.AV_ROOM_ID || void 0,
|
|
89
193
|
model: env.AV_CLAUDE_MODEL || void 0,
|
|
90
194
|
systemPrompt: env.AV_SYSTEM_PROMPT || void 0,
|
|
@@ -94,12 +198,15 @@ function loadConfig(env, argv = []) {
|
|
|
94
198
|
osIsolated
|
|
95
199
|
};
|
|
96
200
|
}
|
|
97
|
-
var CRED_FILES, BACKUP_FILE;
|
|
201
|
+
var CRED_FILES, BACKUP_FILE, LEGACY_DEFAULT_AGENT_NAME;
|
|
98
202
|
var init_config = __esm({
|
|
99
203
|
"src/config.ts"() {
|
|
100
204
|
"use strict";
|
|
205
|
+
init_pending_invite();
|
|
206
|
+
init_log();
|
|
101
207
|
CRED_FILES = ["agentvault.json", "secure-channel.json"];
|
|
102
208
|
BACKUP_FILE = "agentvault.json.bak";
|
|
209
|
+
LEGACY_DEFAULT_AGENT_NAME = "Claude";
|
|
103
210
|
}
|
|
104
211
|
});
|
|
105
212
|
|
|
@@ -227,6 +334,37 @@ ${envEntries}
|
|
|
227
334
|
</plist>
|
|
228
335
|
`;
|
|
229
336
|
}
|
|
337
|
+
function parsePlistEnv(xml) {
|
|
338
|
+
const out = {};
|
|
339
|
+
const keyIdx = xml.indexOf("<key>EnvironmentVariables</key>");
|
|
340
|
+
if (keyIdx < 0) return out;
|
|
341
|
+
const open = xml.indexOf("<dict>", keyIdx);
|
|
342
|
+
if (open < 0) return out;
|
|
343
|
+
let depth = 0;
|
|
344
|
+
let i2 = open;
|
|
345
|
+
let close = -1;
|
|
346
|
+
const tag = /<(\/?)dict>/g;
|
|
347
|
+
tag.lastIndex = open;
|
|
348
|
+
let m6;
|
|
349
|
+
while (m6 = tag.exec(xml)) {
|
|
350
|
+
depth += m6[1] ? -1 : 1;
|
|
351
|
+
if (depth === 0) {
|
|
352
|
+
close = m6.index;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
i2 = m6.index;
|
|
356
|
+
}
|
|
357
|
+
void i2;
|
|
358
|
+
if (close < 0) return out;
|
|
359
|
+
const body = xml.slice(open, close);
|
|
360
|
+
const pair = /<key>([\s\S]*?)<\/key>\s*<string>([\s\S]*?)<\/string>/g;
|
|
361
|
+
let p2;
|
|
362
|
+
while (p2 = pair.exec(body)) out[unesc(p2[1])] = unesc(p2[2]);
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
function unesc(s10) {
|
|
366
|
+
return s10.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
367
|
+
}
|
|
230
368
|
var LaunchdBackend;
|
|
231
369
|
var init_launchd = __esm({
|
|
232
370
|
"src/service/launchd.ts"() {
|
|
@@ -281,6 +419,26 @@ var init_launchd = __esm({
|
|
|
281
419
|
const r7 = this.deps.exec("launchctl", ["print", `${this.domain()}/${label}`]);
|
|
282
420
|
return { loaded: r7.code === 0, detail: r7.stdout };
|
|
283
421
|
}
|
|
422
|
+
/** #913 item 3 — every installed service whose label starts with `prefix`.
|
|
423
|
+
*
|
|
424
|
+
* Reads the plist directory rather than `launchctl list`, because an ORPHAN
|
|
425
|
+
* is typically installed-but-not-loaded: the revoked device exits 0 and
|
|
426
|
+
* KeepAlive leaves it down. `launchctl list` would not show the very thing
|
|
427
|
+
* doctor exists to find. */
|
|
428
|
+
list(prefix) {
|
|
429
|
+
const dir = join9(this.deps.home, "Library", "LaunchAgents");
|
|
430
|
+
const out = [];
|
|
431
|
+
for (const name of this.deps.readdir(dir)) {
|
|
432
|
+
if (!name.startsWith(prefix) || !name.endsWith(".plist")) continue;
|
|
433
|
+
const path2 = join9(dir, name);
|
|
434
|
+
out.push({
|
|
435
|
+
label: name.slice(0, -".plist".length),
|
|
436
|
+
path: path2,
|
|
437
|
+
env: parsePlistEnv(this.deps.readFile(path2))
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
return out.sort((a2, b5) => a2.label.localeCompare(b5.label));
|
|
441
|
+
}
|
|
284
442
|
};
|
|
285
443
|
}
|
|
286
444
|
});
|
|
@@ -312,6 +470,24 @@ StandardError=append:${spec.logErr}
|
|
|
312
470
|
WantedBy=default.target
|
|
313
471
|
`;
|
|
314
472
|
}
|
|
473
|
+
function parseUnitEnv(text) {
|
|
474
|
+
const out = {};
|
|
475
|
+
for (const line of text.split("\n")) {
|
|
476
|
+
const m6 = /^Environment="([\s\S]*)"\s*$/.exec(line.trim());
|
|
477
|
+
if (!m6) continue;
|
|
478
|
+
const eq2 = unq(m6[1]);
|
|
479
|
+
const at = eq2.indexOf("=");
|
|
480
|
+
if (at <= 0) continue;
|
|
481
|
+
out[eq2.slice(0, at)] = eq2.slice(at + 1);
|
|
482
|
+
}
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
function unq(s10) {
|
|
486
|
+
return s10.replace(/%%/g, "%").replace(
|
|
487
|
+
/\\([\s\S])/g,
|
|
488
|
+
(_4, c4) => c4 === "n" ? "\n" : c4 === "r" ? "\r" : c4 === "t" ? " " : c4
|
|
489
|
+
);
|
|
490
|
+
}
|
|
315
491
|
var SystemdBackend;
|
|
316
492
|
var init_systemd = __esm({
|
|
317
493
|
"src/service/systemd.ts"() {
|
|
@@ -353,13 +529,33 @@ var init_systemd = __esm({
|
|
|
353
529
|
const active = this.deps.exec("systemctl", ["--user", "is-active", `${label}.service`]);
|
|
354
530
|
return { loaded: enabled.code === 0, detail: `${enabled.stdout.trim() || "?"} / ${active.stdout.trim() || "?"}` };
|
|
355
531
|
}
|
|
532
|
+
/** #913 item 3 — every installed unit whose label starts with `prefix`.
|
|
533
|
+
*
|
|
534
|
+
* Reads the unit directory rather than `systemctl list-units`, because an
|
|
535
|
+
* ORPHAN is installed-but-inactive: the revoked device exits 0, which
|
|
536
|
+
* SuccessExitStatus=0 treats as success, so it is never restarted. A
|
|
537
|
+
* running-units listing would not show the thing doctor exists to find. */
|
|
538
|
+
list(prefix) {
|
|
539
|
+
const dir = join10(this.deps.home, ".config", "systemd", "user");
|
|
540
|
+
const out = [];
|
|
541
|
+
for (const name of this.deps.readdir(dir)) {
|
|
542
|
+
if (!name.startsWith(prefix) || !name.endsWith(".service")) continue;
|
|
543
|
+
const path2 = join10(dir, name);
|
|
544
|
+
out.push({
|
|
545
|
+
label: name.slice(0, -".service".length),
|
|
546
|
+
path: path2,
|
|
547
|
+
env: parseUnitEnv(this.deps.readFile(path2))
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
return out.sort((a2, b5) => a2.label.localeCompare(b5.label));
|
|
551
|
+
}
|
|
356
552
|
};
|
|
357
553
|
}
|
|
358
554
|
});
|
|
359
555
|
|
|
360
556
|
// src/service/backend.ts
|
|
361
557
|
import { spawnSync } from "node:child_process";
|
|
362
|
-
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, rmSync as rmSync4, existsSync as existsSync4, chmodSync as
|
|
558
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, rmSync as rmSync4, existsSync as existsSync4, chmodSync as chmodSync4, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "node:fs";
|
|
363
559
|
import { userInfo } from "node:os";
|
|
364
560
|
function defaultDeps() {
|
|
365
561
|
return {
|
|
@@ -372,9 +568,23 @@ function defaultDeps() {
|
|
|
372
568
|
},
|
|
373
569
|
writeFile: (p2, d10) => writeFileSync3(p2, d10),
|
|
374
570
|
mkdir: (p2) => mkdirSync5(p2, { recursive: true }),
|
|
375
|
-
chmod: (p2, m6) =>
|
|
571
|
+
chmod: (p2, m6) => chmodSync4(p2, m6),
|
|
376
572
|
rm: (p2) => rmSync4(p2, { force: true }),
|
|
377
|
-
exists: (p2) => existsSync4(p2)
|
|
573
|
+
exists: (p2) => existsSync4(p2),
|
|
574
|
+
readdir: (p2) => {
|
|
575
|
+
try {
|
|
576
|
+
return readdirSync3(p2);
|
|
577
|
+
} catch {
|
|
578
|
+
return [];
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
readFile: (p2) => {
|
|
582
|
+
try {
|
|
583
|
+
return readFileSync6(p2, "utf8");
|
|
584
|
+
} catch {
|
|
585
|
+
return "";
|
|
586
|
+
}
|
|
587
|
+
}
|
|
378
588
|
};
|
|
379
589
|
}
|
|
380
590
|
function selectBackend(platform, deps) {
|
|
@@ -397,7 +607,8 @@ var init_backend = __esm({
|
|
|
397
607
|
// src/service/subcommand.ts
|
|
398
608
|
var subcommand_exports = {};
|
|
399
609
|
__export(subcommand_exports, {
|
|
400
|
-
maybeRunServiceSubcommand: () => maybeRunServiceSubcommand
|
|
610
|
+
maybeRunServiceSubcommand: () => maybeRunServiceSubcommand,
|
|
611
|
+
resolveLabel: () => resolveLabel
|
|
401
612
|
});
|
|
402
613
|
function requireAgentName(env) {
|
|
403
614
|
const name = env.AV_AGENT_NAME?.trim();
|
|
@@ -429,7 +640,9 @@ function maybeRunServiceSubcommand(argv, env, deps) {
|
|
|
429
640
|
const looksLikeFlag = cmd.startsWith("-");
|
|
430
641
|
if (!looksLikeToken && !looksLikeFlag) {
|
|
431
642
|
throw new Error(
|
|
432
|
-
|
|
643
|
+
// "doctor" is dispatched earlier, in index.ts, so it never reaches here.
|
|
644
|
+
// Named anyway: a customer who typed `doctr` should see it offered.
|
|
645
|
+
`unknown subcommand: ${cmd} \u2014 expected one of ${[...SERVICE_CMDS, "doctor"].join(", ")}, an invite token, or no argument at all`
|
|
433
646
|
);
|
|
434
647
|
}
|
|
435
648
|
return false;
|
|
@@ -440,11 +653,11 @@ function maybeRunServiceSubcommand(argv, env, deps) {
|
|
|
440
653
|
try {
|
|
441
654
|
if (cmd === "install") {
|
|
442
655
|
requireAgentName(env);
|
|
656
|
+
assertInstallableEntrypoint(entrypoint);
|
|
443
657
|
const cfg = loadConfig(env, []);
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
);
|
|
658
|
+
const firstBoot = !hasPersistedCreds(cfg.dataDir) && !!cfg.inviteToken;
|
|
659
|
+
if (firstBoot) {
|
|
660
|
+
writePendingInvite(cfg.dataDir, cfg.inviteToken);
|
|
448
661
|
}
|
|
449
662
|
const spec = buildServiceSpec(cfg, {
|
|
450
663
|
entrypoint,
|
|
@@ -455,7 +668,11 @@ function maybeRunServiceSubcommand(argv, env, deps) {
|
|
|
455
668
|
path: env.PATH ?? ""
|
|
456
669
|
});
|
|
457
670
|
backend.install(spec);
|
|
458
|
-
log(`installed service ${spec.label} \u2014 logs: ${spec.
|
|
671
|
+
log(`installed service ${spec.label} \u2014 logs: ${spec.logErr}`);
|
|
672
|
+
if (firstBoot) {
|
|
673
|
+
log("now APPROVE the device in AgentVault \u2014 the service is waiting to enrol");
|
|
674
|
+
log("if it does not connect within a minute, the log above says why");
|
|
675
|
+
}
|
|
459
676
|
log(
|
|
460
677
|
`check it with: AV_AGENT_NAME=${cfg.agentName} agentvault-claude-bridge status`
|
|
461
678
|
);
|
|
@@ -487,14 +704,324 @@ var init_subcommand = __esm({
|
|
|
487
704
|
"use strict";
|
|
488
705
|
init_config();
|
|
489
706
|
init_spec();
|
|
707
|
+
init_pending_invite();
|
|
490
708
|
init_backend();
|
|
491
709
|
SERVICE_CMDS = /* @__PURE__ */ new Set(["install", "uninstall", "restart", "status", "logs"]);
|
|
492
710
|
}
|
|
493
711
|
});
|
|
494
712
|
|
|
713
|
+
// src/service/doctor.ts
|
|
714
|
+
import { basename as basename2 } from "node:path";
|
|
715
|
+
async function buildDoctorReport(d10) {
|
|
716
|
+
const installed = d10.labelPrefixes.flatMap((p2) => d10.backend.list(p2));
|
|
717
|
+
const services = [];
|
|
718
|
+
for (const s10 of installed) services.push(await inspect(s10, d10));
|
|
719
|
+
const claimed = new Set(installed.map((s10) => s10.env.AV_DATA_DIR).filter(Boolean));
|
|
720
|
+
const unsupervised = [];
|
|
721
|
+
for (const dataDir of d10.credDirs()) {
|
|
722
|
+
if (claimed.has(dataDir)) continue;
|
|
723
|
+
unsupervised.push(await inspectUnsupervised(dataDir, d10));
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
labelPrefixes: d10.labelPrefixes,
|
|
727
|
+
services,
|
|
728
|
+
unsupervised,
|
|
729
|
+
notChecked: d10.otherLabels()
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
async function inspect(s10, d10) {
|
|
733
|
+
const agentName = s10.env.AV_AGENT_NAME ?? "";
|
|
734
|
+
const dataDir = s10.env.AV_DATA_DIR ?? "";
|
|
735
|
+
let loaded = false;
|
|
736
|
+
try {
|
|
737
|
+
loaded = d10.backend.status(s10.label).loaded;
|
|
738
|
+
} catch {
|
|
739
|
+
}
|
|
740
|
+
const base = { label: s10.label, agentName, dataDir, loaded, deviceId: "" };
|
|
741
|
+
if (!dataDir) {
|
|
742
|
+
return {
|
|
743
|
+
...base,
|
|
744
|
+
verdict: "WARN",
|
|
745
|
+
detail: "the service definition bakes no AV_DATA_DIR, so doctor cannot find its credentials and cannot check its device"
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
const creds = d10.readCreds(dataDir);
|
|
749
|
+
if (!creds?.deviceId) {
|
|
750
|
+
return {
|
|
751
|
+
...base,
|
|
752
|
+
verdict: "WARN",
|
|
753
|
+
detail: `no credentials at ${dataDir} \u2014 installed, but this agent never enrolled`
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
const deviceId = creds.deviceId;
|
|
757
|
+
let probe;
|
|
758
|
+
try {
|
|
759
|
+
probe = await d10.fetchStatus(d10.apiUrl, deviceId);
|
|
760
|
+
} catch (e7) {
|
|
761
|
+
return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${e7.message}` };
|
|
762
|
+
}
|
|
763
|
+
if (probe.inconclusive) {
|
|
764
|
+
return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${probe.inconclusive}` };
|
|
765
|
+
}
|
|
766
|
+
if (probe.gone) {
|
|
767
|
+
return {
|
|
768
|
+
...base,
|
|
769
|
+
deviceId,
|
|
770
|
+
verdict: "FAIL",
|
|
771
|
+
detail: "the device no longer exists \u2014 this service is an orphan",
|
|
772
|
+
fix: d10.uninstallCommand(agentName)
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
switch (probe.status) {
|
|
776
|
+
case "ACTIVE":
|
|
777
|
+
return { ...base, deviceId, verdict: "PASS", detail: "device ACTIVE" };
|
|
778
|
+
case "PENDING":
|
|
779
|
+
return {
|
|
780
|
+
...base,
|
|
781
|
+
deviceId,
|
|
782
|
+
verdict: "WARN",
|
|
783
|
+
detail: "device PENDING \u2014 enrolled, but never approved in AgentVault"
|
|
784
|
+
};
|
|
785
|
+
case "REVOKED":
|
|
786
|
+
return {
|
|
787
|
+
...base,
|
|
788
|
+
deviceId,
|
|
789
|
+
verdict: "FAIL",
|
|
790
|
+
detail: "the device was REVOKED \u2014 this service is an orphan",
|
|
791
|
+
fix: d10.uninstallCommand(agentName)
|
|
792
|
+
};
|
|
793
|
+
default:
|
|
794
|
+
return {
|
|
795
|
+
...base,
|
|
796
|
+
deviceId,
|
|
797
|
+
verdict: "SKIP",
|
|
798
|
+
detail: `the server reported a status this build does not recognise (${probe.status || "none"})`
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async function inspectUnsupervised(dataDir, d10) {
|
|
803
|
+
const agentName = basename2(dataDir);
|
|
804
|
+
const base = { dataDir, agentName, deviceId: "" };
|
|
805
|
+
const deviceId = d10.readCreds(dataDir)?.deviceId;
|
|
806
|
+
if (!deviceId) {
|
|
807
|
+
return { ...base, verdict: "WARN", detail: "credentials present but they hold no device id" };
|
|
808
|
+
}
|
|
809
|
+
let probe;
|
|
810
|
+
try {
|
|
811
|
+
probe = await d10.fetchStatus(d10.apiUrl, deviceId);
|
|
812
|
+
} catch (e7) {
|
|
813
|
+
return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${e7.message}` };
|
|
814
|
+
}
|
|
815
|
+
if (probe.inconclusive) {
|
|
816
|
+
return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${probe.inconclusive}` };
|
|
817
|
+
}
|
|
818
|
+
if (probe.gone) {
|
|
819
|
+
return {
|
|
820
|
+
...base,
|
|
821
|
+
deviceId,
|
|
822
|
+
verdict: "WARN",
|
|
823
|
+
detail: "the device no longer exists \u2014 these are leftover credentials for a deleted agent"
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
if (probe.status === "ACTIVE") {
|
|
827
|
+
return {
|
|
828
|
+
...base,
|
|
829
|
+
deviceId,
|
|
830
|
+
verdict: "WARN",
|
|
831
|
+
detail: "the device is ACTIVE but nothing supervises it \u2014 this agent is not running",
|
|
832
|
+
fix: d10.installCommand(agentName)
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
if (probe.status === "PENDING") {
|
|
836
|
+
return {
|
|
837
|
+
...base,
|
|
838
|
+
deviceId,
|
|
839
|
+
verdict: "WARN",
|
|
840
|
+
detail: "device PENDING \u2014 enrolled, but never approved in AgentVault"
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
if (probe.status === "REVOKED") {
|
|
844
|
+
return {
|
|
845
|
+
...base,
|
|
846
|
+
deviceId,
|
|
847
|
+
verdict: "WARN",
|
|
848
|
+
detail: "the device was REVOKED \u2014 these are leftover credentials"
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
return {
|
|
852
|
+
...base,
|
|
853
|
+
deviceId,
|
|
854
|
+
verdict: "SKIP",
|
|
855
|
+
detail: `the server reported a status this build does not recognise (${probe.status || "none"})`
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
function formatDoctorReport(r7) {
|
|
859
|
+
const out = [];
|
|
860
|
+
out.push(`Services installed for this bridge (${r7.labelPrefixes.map((p2) => p2 + "*").join(", ")})`);
|
|
861
|
+
if (r7.services.length === 0) {
|
|
862
|
+
out.push(` No services installed for this bridge on this machine.`);
|
|
863
|
+
}
|
|
864
|
+
for (const s10 of r7.services) {
|
|
865
|
+
out.push(
|
|
866
|
+
` ${s10.verdict.padEnd(PAD)}${s10.label} [${s10.agentName || "unnamed"}] ${s10.loaded ? "loaded" : "not loaded"} \u2014 ${s10.detail}`
|
|
867
|
+
);
|
|
868
|
+
if (s10.fix) out.push(` ${"".padEnd(PAD)}fix: ${s10.fix}`);
|
|
869
|
+
}
|
|
870
|
+
if (r7.unsupervised.length) {
|
|
871
|
+
out.push("");
|
|
872
|
+
out.push("Credentials on disk that no installed service points at");
|
|
873
|
+
for (const u2 of r7.unsupervised) {
|
|
874
|
+
out.push(` ${u2.verdict.padEnd(PAD)}${u2.agentName} \u2014 ${u2.detail}`);
|
|
875
|
+
out.push(` ${"".padEnd(PAD)}${u2.dataDir}`);
|
|
876
|
+
if (u2.fix) out.push(` ${"".padEnd(PAD)}fix: ${u2.fix}`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (r7.notChecked.length) {
|
|
880
|
+
out.push("");
|
|
881
|
+
out.push("Found on this machine, NOT checked by this command");
|
|
882
|
+
for (const l10 of r7.notChecked) out.push(` ${l10}`);
|
|
883
|
+
out.push(
|
|
884
|
+
" These belong to other AgentVault agent families. This command can only read the credentials of its own, so it says nothing about their health."
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
const counts = { PASS: 0, WARN: 0, FAIL: 0, SKIP: 0 };
|
|
888
|
+
for (const s10 of r7.services) counts[s10.verdict]++;
|
|
889
|
+
for (const u2 of r7.unsupervised) counts[u2.verdict]++;
|
|
890
|
+
out.push("");
|
|
891
|
+
out.push(
|
|
892
|
+
`Summary: ${counts.PASS} PASS, ${counts.WARN} WARN, ${counts.FAIL} FAIL, ${counts.SKIP} SKIP` + (counts.SKIP ? " (SKIP = could not check, NOT a problem found)" : "")
|
|
893
|
+
);
|
|
894
|
+
if (counts.FAIL) {
|
|
895
|
+
out.push(
|
|
896
|
+
"`uninstall` removes the SERVICE only \u2014 your credentials stay on disk, so a mistake here costs a re-install, not an identity."
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
return out;
|
|
900
|
+
}
|
|
901
|
+
var PAD;
|
|
902
|
+
var init_doctor = __esm({
|
|
903
|
+
"src/service/doctor.ts"() {
|
|
904
|
+
"use strict";
|
|
905
|
+
PAD = 6;
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
// src/service/doctor-cli.ts
|
|
910
|
+
var doctor_cli_exports = {};
|
|
911
|
+
__export(doctor_cli_exports, {
|
|
912
|
+
fetchDeviceStatus: () => fetchDeviceStatus,
|
|
913
|
+
maybeRunDoctorSubcommand: () => maybeRunDoctorSubcommand,
|
|
914
|
+
realDoctorDeps: () => realDoctorDeps
|
|
915
|
+
});
|
|
916
|
+
import { join as join12 } from "node:path";
|
|
917
|
+
import { readdirSync as readdirSync5 } from "node:fs";
|
|
918
|
+
async function fetchDeviceStatus(apiUrl, deviceId, doFetch = fetch) {
|
|
919
|
+
const res = await doFetch(`${apiUrl}/api/v1/devices/${deviceId}/status`, {
|
|
920
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
|
921
|
+
});
|
|
922
|
+
if (res.status === 404) {
|
|
923
|
+
try {
|
|
924
|
+
await res.json();
|
|
925
|
+
} catch {
|
|
926
|
+
return {
|
|
927
|
+
inconclusive: `${apiUrl} answered 404 with a non-JSON body \u2014 check AV_API_URL points at the API`
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
return { gone: true };
|
|
931
|
+
}
|
|
932
|
+
if (res.status === 429) return { inconclusive: "rate limited by the API (429)" };
|
|
933
|
+
if (!res.ok) return { inconclusive: `the API answered ${res.status}` };
|
|
934
|
+
let body;
|
|
935
|
+
try {
|
|
936
|
+
body = await res.json();
|
|
937
|
+
} catch {
|
|
938
|
+
return { inconclusive: `${apiUrl} answered 200 with a non-JSON body \u2014 check AV_API_URL` };
|
|
939
|
+
}
|
|
940
|
+
return { status: body?.status };
|
|
941
|
+
}
|
|
942
|
+
function listCredDirs(base) {
|
|
943
|
+
const out = [];
|
|
944
|
+
if (hasPersistedCreds(base)) out.push(base);
|
|
945
|
+
let names;
|
|
946
|
+
try {
|
|
947
|
+
names = readdirSync5(base);
|
|
948
|
+
} catch {
|
|
949
|
+
return out;
|
|
950
|
+
}
|
|
951
|
+
for (const name of names) {
|
|
952
|
+
const dir = join12(base, name);
|
|
953
|
+
if (hasPersistedCreds(dir)) out.push(dir);
|
|
954
|
+
}
|
|
955
|
+
return out;
|
|
956
|
+
}
|
|
957
|
+
function otherFamilyLabels(backend) {
|
|
958
|
+
const out = [];
|
|
959
|
+
for (const p2 of OTHER_FAMILY_PREFIXES) {
|
|
960
|
+
try {
|
|
961
|
+
for (const s10 of backend.list(p2)) out.push(s10.label);
|
|
962
|
+
} catch {
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return out.sort();
|
|
966
|
+
}
|
|
967
|
+
function realDoctorDeps(env) {
|
|
968
|
+
const backend = selectBackend(process.platform);
|
|
969
|
+
const base = join12(env.HOME ?? "", ".agentvault", "claude-room-bridge");
|
|
970
|
+
return {
|
|
971
|
+
backend,
|
|
972
|
+
labelPrefixes: [LABEL_PREFIX, LEGACY_LABEL_PREFIX],
|
|
973
|
+
base,
|
|
974
|
+
// Same default as config.ts. Read from the env directly rather than through
|
|
975
|
+
// loadConfig, which now REFUSES without AV_AGENT_NAME (#921) — and doctor's
|
|
976
|
+
// whole job is to run when you do not know which agents exist.
|
|
977
|
+
apiUrl: env.AV_API_URL ?? "https://api.agentvault.chat",
|
|
978
|
+
uninstallCommand: (n10) => `AV_AGENT_NAME=${n10 || "<agent-name>"} agentvault-claude-bridge uninstall`,
|
|
979
|
+
installCommand: (n10) => `AV_AGENT_NAME=${n10 || "<agent-name>"} agentvault-claude-bridge install`,
|
|
980
|
+
readCreds: (dir) => {
|
|
981
|
+
const deviceId = readPersistedDeviceId(dir);
|
|
982
|
+
return deviceId ? { deviceId } : null;
|
|
983
|
+
},
|
|
984
|
+
credDirs: () => listCredDirs(base),
|
|
985
|
+
fetchStatus: (apiUrl, deviceId) => fetchDeviceStatus(apiUrl, deviceId),
|
|
986
|
+
otherLabels: () => otherFamilyLabels(backend)
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
async function maybeRunDoctorSubcommand(argv, opts = {}) {
|
|
990
|
+
if (argv[2] !== "doctor") return false;
|
|
991
|
+
const log = opts.log ?? printLine;
|
|
992
|
+
try {
|
|
993
|
+
const report = await buildDoctorReport(opts.deps ?? realDoctorDeps(opts.env ?? process.env));
|
|
994
|
+
for (const line of formatDoctorReport(report)) log(line);
|
|
995
|
+
if (report.services.some((s10) => s10.verdict === "FAIL")) process.exitCode = 1;
|
|
996
|
+
} catch (e7) {
|
|
997
|
+
log(`doctor could not run: ${e7.message}`);
|
|
998
|
+
process.exitCode = 1;
|
|
999
|
+
}
|
|
1000
|
+
return true;
|
|
1001
|
+
}
|
|
1002
|
+
var LABEL_PREFIX, LEGACY_LABEL_PREFIX, OTHER_FAMILY_PREFIXES, PROBE_TIMEOUT_MS;
|
|
1003
|
+
var init_doctor_cli = __esm({
|
|
1004
|
+
"src/service/doctor-cli.ts"() {
|
|
1005
|
+
"use strict";
|
|
1006
|
+
init_backend();
|
|
1007
|
+
init_config();
|
|
1008
|
+
init_log();
|
|
1009
|
+
init_doctor();
|
|
1010
|
+
LABEL_PREFIX = "dev.agentvault.bridge-";
|
|
1011
|
+
LEGACY_LABEL_PREFIX = "com.agentvault.claude-bridge.";
|
|
1012
|
+
OTHER_FAMILY_PREFIXES = [
|
|
1013
|
+
"dev.agentvault.grok-",
|
|
1014
|
+
// grok bridge
|
|
1015
|
+
"ai.hermes.gateway"
|
|
1016
|
+
// hermes, one service per profile
|
|
1017
|
+
];
|
|
1018
|
+
PROBE_TIMEOUT_MS = 8e3;
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
|
|
495
1022
|
// src/index.ts
|
|
496
|
-
import { appendFileSync as appendFileSync2, mkdirSync as
|
|
497
|
-
import { join as
|
|
1023
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync7 } from "node:fs";
|
|
1024
|
+
import { join as join13 } from "node:path";
|
|
498
1025
|
|
|
499
1026
|
// ../plugin/dist/index.js
|
|
500
1027
|
import * as nc from "node:crypto";
|
|
@@ -45379,6 +45906,11 @@ var init_ratchet = __esm2({
|
|
|
45379
45906
|
return { header, headerSignature, ciphertext, nonce };
|
|
45380
45907
|
}
|
|
45381
45908
|
decrypt(message) {
|
|
45909
|
+
if (this.state.peerIdentityPublicKey) {
|
|
45910
|
+
if (!verifyHeaderSignature(message.header, message.headerSignature, this.state.peerIdentityPublicKey)) {
|
|
45911
|
+
throw new Error("Header signature verification failed");
|
|
45912
|
+
}
|
|
45913
|
+
}
|
|
45382
45914
|
const isV2 = message.envelopeVersion === "2.0.0" && message.encryptedHeader != null && message.headerNonce != null;
|
|
45383
45915
|
const skippedResult = this.trySkippedKeys(message, isV2);
|
|
45384
45916
|
if (skippedResult !== null) {
|
|
@@ -45392,14 +45924,14 @@ var init_ratchet = __esm2({
|
|
|
45392
45924
|
if (message.header.messageNumber === 0) {
|
|
45393
45925
|
try {
|
|
45394
45926
|
const { messageKey: testKey, headerKey: testHeaderKey, nextChainKey: nextChainKey2 } = kdfChainKey(this.state.rootKey);
|
|
45395
|
-
let
|
|
45927
|
+
let ad;
|
|
45396
45928
|
if (isV2) {
|
|
45397
45929
|
libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.encryptedHeader, null, message.headerNonce, testHeaderKey);
|
|
45398
|
-
|
|
45930
|
+
ad = message.encryptedHeader;
|
|
45399
45931
|
} else {
|
|
45400
|
-
|
|
45932
|
+
ad = serializeHeader(message.header);
|
|
45401
45933
|
}
|
|
45402
|
-
const ptBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext,
|
|
45934
|
+
const ptBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext, ad, message.nonce, testKey);
|
|
45403
45935
|
this.state.dhReceivingPublicKey = message.header.dhPublicKey;
|
|
45404
45936
|
this.state.receivingChain = {
|
|
45405
45937
|
chainKey: nextChainKey2,
|
|
@@ -45419,13 +45951,7 @@ var init_ratchet = __esm2({
|
|
|
45419
45951
|
this.skipMessages(this.state.receivingChain, message.header.messageNumber, message.header.dhPublicKey, isV2);
|
|
45420
45952
|
const chain = this.state.receivingChain;
|
|
45421
45953
|
const { nextChainKey, messageKey, headerKey } = kdfChainKey(chain.chainKey);
|
|
45422
|
-
|
|
45423
|
-
chain.messageNumber++;
|
|
45424
|
-
if (this.state.peerIdentityPublicKey) {
|
|
45425
|
-
if (!verifyHeaderSignature(message.header, message.headerSignature, this.state.peerIdentityPublicKey)) {
|
|
45426
|
-
throw new Error("Header signature verification failed");
|
|
45427
|
-
}
|
|
45428
|
-
}
|
|
45954
|
+
let plaintextBytes;
|
|
45429
45955
|
if (isV2) {
|
|
45430
45956
|
try {
|
|
45431
45957
|
libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.encryptedHeader, null, message.headerNonce, headerKey);
|
|
@@ -45433,19 +45959,21 @@ var init_ratchet = __esm2({
|
|
|
45433
45959
|
throw new Error("V2 header decryption failed");
|
|
45434
45960
|
}
|
|
45435
45961
|
try {
|
|
45436
|
-
|
|
45437
|
-
return libsodium_wrappers_default.to_string(plaintextBytes);
|
|
45962
|
+
plaintextBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext, message.encryptedHeader, message.nonce, messageKey);
|
|
45438
45963
|
} catch {
|
|
45439
45964
|
throw new Error("V2 decryption failed: ciphertext tampered or wrong key");
|
|
45440
45965
|
}
|
|
45966
|
+
} else {
|
|
45967
|
+
const ad = serializeHeader(message.header);
|
|
45968
|
+
try {
|
|
45969
|
+
plaintextBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext, ad, message.nonce, messageKey);
|
|
45970
|
+
} catch {
|
|
45971
|
+
throw new Error("Decryption failed: ciphertext tampered or wrong key");
|
|
45972
|
+
}
|
|
45441
45973
|
}
|
|
45442
|
-
|
|
45443
|
-
|
|
45444
|
-
|
|
45445
|
-
return libsodium_wrappers_default.to_string(plaintextBytes);
|
|
45446
|
-
} catch {
|
|
45447
|
-
throw new Error("Decryption failed: ciphertext tampered or wrong key");
|
|
45448
|
-
}
|
|
45974
|
+
chain.chainKey = nextChainKey;
|
|
45975
|
+
chain.messageNumber++;
|
|
45976
|
+
return libsodium_wrappers_default.to_string(plaintextBytes);
|
|
45449
45977
|
}
|
|
45450
45978
|
dhRatchetReceive(theirDhPublic) {
|
|
45451
45979
|
this.state.previousSendingChainLength = this.state.sendingChain?.messageNumber ?? 0;
|
|
@@ -63619,6 +64147,23 @@ var init_mls_delivery_order = __esm2({
|
|
|
63619
64147
|
"use strict";
|
|
63620
64148
|
}
|
|
63621
64149
|
});
|
|
64150
|
+
function noteObservedEpoch(current, seen) {
|
|
64151
|
+
const n22 = Number(seen);
|
|
64152
|
+
if (!Number.isInteger(n22) || n22 < 0)
|
|
64153
|
+
return current;
|
|
64154
|
+
return n22 > current ? n22 : current;
|
|
64155
|
+
}
|
|
64156
|
+
function needsPreSendResync(localEpoch, observedEpoch) {
|
|
64157
|
+
if (!Number.isInteger(localEpoch) || !Number.isInteger(observedEpoch)) {
|
|
64158
|
+
return false;
|
|
64159
|
+
}
|
|
64160
|
+
return observedEpoch > localEpoch;
|
|
64161
|
+
}
|
|
64162
|
+
var init_mls_presend_epoch = __esm2({
|
|
64163
|
+
"../crypto/dist/mls-presend-epoch.js"() {
|
|
64164
|
+
"use strict";
|
|
64165
|
+
}
|
|
64166
|
+
});
|
|
63622
64167
|
var dist_exports = {};
|
|
63623
64168
|
__export2(dist_exports, {
|
|
63624
64169
|
AV_CREDENTIAL_CONTEXT: () => AV_CREDENTIAL_CONTEXT,
|
|
@@ -63688,7 +64233,9 @@ __export2(dist_exports, {
|
|
|
63688
64233
|
hexTransportToEncryptedMessage: () => hexTransportToEncryptedMessage,
|
|
63689
64234
|
issueCredential: () => issueCredential,
|
|
63690
64235
|
multibaseToPublicKey: () => multibaseToPublicKey,
|
|
64236
|
+
needsPreSendResync: () => needsPreSendResync,
|
|
63691
64237
|
normalizeBackupCode: () => normalizeBackupCode,
|
|
64238
|
+
noteObservedEpoch: () => noteObservedEpoch,
|
|
63692
64239
|
orderDeliveryBatch: () => orderDeliveryBatch,
|
|
63693
64240
|
parseTraceparent: () => parseTraceparent,
|
|
63694
64241
|
performX3DH: () => performX3DH,
|
|
@@ -63728,6 +64275,7 @@ var init_dist = __esm2({
|
|
|
63728
64275
|
init_mls_group();
|
|
63729
64276
|
await init_owner_sync();
|
|
63730
64277
|
init_mls_delivery_order();
|
|
64278
|
+
init_mls_presend_epoch();
|
|
63731
64279
|
}
|
|
63732
64280
|
});
|
|
63733
64281
|
async function ensureSecureDir(dir) {
|
|
@@ -63920,7 +64468,7 @@ var init_mls_kp_pool = __esm2({
|
|
|
63920
64468
|
}
|
|
63921
64469
|
});
|
|
63922
64470
|
function ownIdentity() {
|
|
63923
|
-
const v22 = true ? "0.23.
|
|
64471
|
+
const v22 = true ? "0.23.27" : FALLBACK;
|
|
63924
64472
|
return `${PACKAGE}@${v22}`;
|
|
63925
64473
|
}
|
|
63926
64474
|
function buildClientVersion(override) {
|
|
@@ -64836,6 +65384,7 @@ var init_channel = __esm2({
|
|
|
64836
65384
|
await init_libsodium_wrappers();
|
|
64837
65385
|
await init_dist();
|
|
64838
65386
|
await init_dist();
|
|
65387
|
+
init_mls_presend_epoch();
|
|
64839
65388
|
init_mls_state();
|
|
64840
65389
|
await init_mls_kp_pool();
|
|
64841
65390
|
init_client_version();
|
|
@@ -64949,6 +65498,17 @@ var init_channel = __esm2({
|
|
|
64949
65498
|
* self-heal rather than be logged and dropped. */
|
|
64950
65499
|
_mlsCommitFailCounts = /* @__PURE__ */ new Map();
|
|
64951
65500
|
static MAX_MLS_DECRYPT_FAILS = 3;
|
|
65501
|
+
/**
|
|
65502
|
+
* #1084: consecutive resyncs that found NOTHING to apply, per group.
|
|
65503
|
+
*
|
|
65504
|
+
* "No new commits" means the server holds no commit past our epoch — we are
|
|
65505
|
+
* NOT BEHIND. Treating that as corruption and re-keying is what drove the
|
|
65506
|
+
* epoch churn (see `_resyncOnDivergence`). But a state that is genuinely
|
|
65507
|
+
* broken while its epoch counter happens to match must still recover, so the
|
|
65508
|
+
* suppression is bounded rather than absolute.
|
|
65509
|
+
*/
|
|
65510
|
+
_resyncNoOpStrikes = /* @__PURE__ */ new Map();
|
|
65511
|
+
static MAX_RESYNC_NOOPS = 3;
|
|
64952
65512
|
/** Cached MLS KeyPackage bundle for this device (regenerated on each connect). */
|
|
64953
65513
|
_mlsKeyPackage = null;
|
|
64954
65514
|
/** Pending KeyPackage bundle from request-Welcome flow (used by _handleMlsWelcome). */
|
|
@@ -64986,6 +65546,13 @@ var init_channel = __esm2({
|
|
|
64986
65546
|
_kpPoolFilling = false;
|
|
64987
65547
|
/** Buffer for MLS commits received before Welcome (keyed by groupId, sorted by epoch). */
|
|
64988
65548
|
_pendingMlsCommits = /* @__PURE__ */ new Map();
|
|
65549
|
+
/**
|
|
65550
|
+
* #1013: the highest group epoch any inbound frame has reported, per MLS
|
|
65551
|
+
* group id. Every inbound frame carries the epoch the group was at when it
|
|
65552
|
+
* was produced, so each is a LOWER BOUND on the group's current epoch — the
|
|
65553
|
+
* hint is monotonic and only ever rises.
|
|
65554
|
+
*/
|
|
65555
|
+
_observedGroupEpochs = /* @__PURE__ */ new Map();
|
|
64989
65556
|
/** In-memory credential store for renter-provided credentials (never persisted). */
|
|
64990
65557
|
_credentialStore = new CredentialStore();
|
|
64991
65558
|
/** Rooms whose roster has been refreshed from the backend at least once in
|
|
@@ -65670,16 +66237,44 @@ var init_channel = __esm2({
|
|
|
65670
66237
|
const pendingWsSends = [];
|
|
65671
66238
|
const sentSharedGroupIds = /* @__PURE__ */ new Set();
|
|
65672
66239
|
const addressedMlsGroupIds = [];
|
|
66240
|
+
if (!(this._persisted?.mlsGroups && this._state === "ready" && this._ws)) {
|
|
66241
|
+
console.warn(
|
|
66242
|
+
`[SecureChannel] send(): shared-MLS block SKIPPED \u2014 mlsGroups=${this._persisted?.mlsGroups ? Object.keys(this._persisted.mlsGroups).length : "none"} state=${this._state} ws=${this._ws ? "open" : "null"}`
|
|
66243
|
+
);
|
|
66244
|
+
}
|
|
65673
66245
|
if (this._persisted?.mlsGroups && this._state === "ready" && this._ws) {
|
|
65674
66246
|
const targetSharedGid = targetConvId ? this._sessionGroupIds?.get(targetConvId) : void 0;
|
|
66247
|
+
console.log(
|
|
66248
|
+
`[SecureChannel] send(): considering ${Object.keys(this._persisted.mlsGroups).length} shared group(s); targetConvId=${targetConvId ? targetConvId.slice(0, 8) : "(none \u2014 broadcast)"} targetSharedGid=${targetSharedGid ? targetSharedGid.slice(0, 8) : "(none)"}`
|
|
66249
|
+
);
|
|
66250
|
+
const skipped = [];
|
|
65675
66251
|
for (const [gid, entry] of Object.entries(this._persisted.mlsGroups)) {
|
|
65676
|
-
if (targetConvId && gid !== targetSharedGid)
|
|
65677
|
-
|
|
66252
|
+
if (targetConvId && gid !== targetSharedGid) {
|
|
66253
|
+
skipped.push(`${gid.slice(0, 8)}:not-target`);
|
|
66254
|
+
continue;
|
|
66255
|
+
}
|
|
66256
|
+
if (!entry.mlsGroupId) {
|
|
66257
|
+
skipped.push(`${gid.slice(0, 8)}:no-mlsGroupId`);
|
|
66258
|
+
continue;
|
|
66259
|
+
}
|
|
65678
66260
|
const mlsGroup = this._mlsGroups.get(`1to1-group:${gid}`);
|
|
65679
|
-
if (!mlsGroup?.isInitialized
|
|
66261
|
+
if (!mlsGroup?.isInitialized) {
|
|
66262
|
+
skipped.push(`${gid.slice(0, 8)}:not-in-memory`);
|
|
66263
|
+
continue;
|
|
66264
|
+
}
|
|
66265
|
+
if (Number(mlsGroup.epoch) <= 0) {
|
|
66266
|
+
skipped.push(`${gid.slice(0, 8)}:epoch<=0`);
|
|
66267
|
+
continue;
|
|
66268
|
+
}
|
|
65680
66269
|
try {
|
|
65681
66270
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
65682
|
-
const cipherBytes = await
|
|
66271
|
+
const cipherBytes = await this._encryptWithCatchUp(
|
|
66272
|
+
mlsGroup,
|
|
66273
|
+
`1to1-group:${gid}`,
|
|
66274
|
+
entry.mlsGroupId,
|
|
66275
|
+
`1:1 group ${gid.slice(0, 8)}`,
|
|
66276
|
+
plaintextBytes
|
|
66277
|
+
);
|
|
65683
66278
|
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
65684
66279
|
const payload = {
|
|
65685
66280
|
group_id: entry.mlsGroupId,
|
|
@@ -65701,8 +66296,12 @@ var init_channel = __esm2({
|
|
|
65701
66296
|
console.log(`[SecureChannel] Shared MLS group send for group ${gid.slice(0, 8)} (${entry.mlsGroupId.slice(0, 8)})`);
|
|
65702
66297
|
} catch (err) {
|
|
65703
66298
|
console.error(`[SecureChannel] Shared MLS group send failed for ${gid.slice(0, 8)}:`, err);
|
|
66299
|
+
skipped.push(`${gid.slice(0, 8)}:encrypt-threw`);
|
|
65704
66300
|
}
|
|
65705
66301
|
}
|
|
66302
|
+
if (skipped.length > 0) {
|
|
66303
|
+
console.log(`[SecureChannel] send(): skipped ${skipped.length} group(s) \u2014 ${skipped.join(", ")}`);
|
|
66304
|
+
}
|
|
65706
66305
|
}
|
|
65707
66306
|
for (const [convId, session] of this._sessions) {
|
|
65708
66307
|
if (!session.activated) continue;
|
|
@@ -65722,7 +66321,14 @@ var init_channel = __esm2({
|
|
|
65722
66321
|
}
|
|
65723
66322
|
if (mlsGroup?.isInitialized && mlsGroupId && Number(mlsGroup.epoch) > 0 && this._state === "ready" && this._ws) {
|
|
65724
66323
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
65725
|
-
const
|
|
66324
|
+
const sessionGroupKey = convGroupId && this._mlsGroups.has(`1to1-group:${convGroupId}`) ? `1to1-group:${convGroupId}` : `conv:${convId}`;
|
|
66325
|
+
const cipherBytes = await this._encryptWithCatchUp(
|
|
66326
|
+
mlsGroup,
|
|
66327
|
+
sessionGroupKey,
|
|
66328
|
+
mlsGroupId,
|
|
66329
|
+
`conversation ${convId.slice(0, 8)}`,
|
|
66330
|
+
plaintextBytes
|
|
66331
|
+
);
|
|
65726
66332
|
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
65727
66333
|
const payload = {
|
|
65728
66334
|
conversation_id: convId,
|
|
@@ -65817,7 +66423,14 @@ var init_channel = __esm2({
|
|
|
65817
66423
|
}
|
|
65818
66424
|
try {
|
|
65819
66425
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
65820
|
-
const
|
|
66426
|
+
const mlsOnlyGroupKey = mlsOnlyConvGroupId && this._mlsGroups.has(`1to1-group:${mlsOnlyConvGroupId}`) ? `1to1-group:${mlsOnlyConvGroupId}` : `conv:${mlsConvId}`;
|
|
66427
|
+
const cipherBytes = await this._encryptWithCatchUp(
|
|
66428
|
+
mlsGroup,
|
|
66429
|
+
mlsOnlyGroupKey,
|
|
66430
|
+
resolvedMlsGroupId,
|
|
66431
|
+
`conversation ${mlsConvId.slice(0, 8)}`,
|
|
66432
|
+
plaintextBytes
|
|
66433
|
+
);
|
|
65821
66434
|
await saveMlsState(this.config.dataDir, resolvedMlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
65822
66435
|
const payload = {
|
|
65823
66436
|
conversation_id: mlsConvId,
|
|
@@ -65843,13 +66456,26 @@ var init_channel = __esm2({
|
|
|
65843
66456
|
}
|
|
65844
66457
|
}
|
|
65845
66458
|
}
|
|
65846
|
-
if (sentCount === 0
|
|
65847
|
-
console.warn(
|
|
66459
|
+
if (sentCount === 0) {
|
|
66460
|
+
console.warn(
|
|
66461
|
+
`[SecureChannel] send() delivered to 0 destinations \u2014 nothing was encrypted or queued (sessions=${this._sessions.size}, sharedGroups=${this._persisted?.mlsGroups ? Object.keys(this._persisted.mlsGroups).length : 0})`
|
|
66462
|
+
);
|
|
65848
66463
|
}
|
|
65849
66464
|
await this._persistState();
|
|
66465
|
+
const wsReady = this._ws?.readyState === 1;
|
|
66466
|
+
if (pendingWsSends.length > 0 && !wsReady) {
|
|
66467
|
+
console.warn(
|
|
66468
|
+
`[SecureChannel] send(): ${pendingWsSends.length} frame(s) NOT written \u2014 socket readyState=${this._ws?.readyState ?? "null"} (1=OPEN)`
|
|
66469
|
+
);
|
|
66470
|
+
}
|
|
65850
66471
|
for (const frame of pendingWsSends) {
|
|
65851
66472
|
this._ws.send(frame);
|
|
65852
66473
|
}
|
|
66474
|
+
if (pendingWsSends.length > 0) {
|
|
66475
|
+
console.log(
|
|
66476
|
+
`[SecureChannel] send(): wrote ${pendingWsSends.length} frame(s) to the socket (sentCount=${sentCount}, readyState=${this._ws?.readyState ?? "null"})`
|
|
66477
|
+
);
|
|
66478
|
+
}
|
|
65853
66479
|
if (!options?.isResend) {
|
|
65854
66480
|
for (const mlsGroupId of addressedMlsGroupIds) {
|
|
65855
66481
|
this._rememberRetryCandidate(mlsGroupId, plaintext, options);
|
|
@@ -65899,7 +66525,7 @@ var init_channel = __esm2({
|
|
|
65899
66525
|
*/
|
|
65900
66526
|
sendActivitySpan(spanData) {
|
|
65901
66527
|
if (!this._ws || this._ws.readyState !== WebSocket2.OPEN) return;
|
|
65902
|
-
const pluginVersion = true ? "0.23.
|
|
66528
|
+
const pluginVersion = true ? "0.23.27" : "0.0.0-dev";
|
|
65903
66529
|
const agentName = this.config.agentName ?? "Agent";
|
|
65904
66530
|
const resource = {
|
|
65905
66531
|
"service.name": "agentvault-agent",
|
|
@@ -66109,7 +66735,13 @@ var init_channel = __esm2({
|
|
|
66109
66735
|
if (mlsGroup?.isInitialized) {
|
|
66110
66736
|
try {
|
|
66111
66737
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
66112
|
-
const ciphertext = await
|
|
66738
|
+
const ciphertext = await this._encryptWithCatchUp(
|
|
66739
|
+
mlsGroup,
|
|
66740
|
+
roomId,
|
|
66741
|
+
room.mlsGroupId,
|
|
66742
|
+
`room ${roomId.slice(0, 8)}`,
|
|
66743
|
+
plaintextBytes
|
|
66744
|
+
);
|
|
66113
66745
|
await saveMlsState(this.config.dataDir, room.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
66114
66746
|
if (this._state === "ready" && this._ws) {
|
|
66115
66747
|
this._ws.send(JSON.stringify({
|
|
@@ -66360,11 +66992,17 @@ var init_channel = __esm2({
|
|
|
66360
66992
|
const before = this._persisted.groupId;
|
|
66361
66993
|
this._sessionGroupIds = new Map(mine.map((r22) => [r22.id, r22.group_id]));
|
|
66362
66994
|
this._persisted.conversationGroupIds = Object.fromEntries(this._sessionGroupIds);
|
|
66363
|
-
const
|
|
66995
|
+
const joined = mine.find((r22) => this._persisted.mlsGroups?.[r22.group_id]);
|
|
66996
|
+
const primary = mine.find((r22) => r22.id === this._persisted.primaryConversationId) ?? joined ?? mine[0];
|
|
66997
|
+
if (!mine.some((r22) => r22.id === this._persisted.primaryConversationId)) {
|
|
66998
|
+
console.log(
|
|
66999
|
+
`[SecureChannel] Adopting new primary conversation ${primary.id.slice(0, 8)} (group ${primary.group_id.slice(0, 8)}) \u2014 ${joined ? "we hold MLS state for it" : "no joined conversation found, first active row"}`
|
|
67000
|
+
);
|
|
67001
|
+
}
|
|
66364
67002
|
this._persisted.groupId = primary.group_id;
|
|
66365
67003
|
this._persisted.primaryConversationId = primary.id;
|
|
66366
67004
|
const liveGroupIds = new Set(mine.map((r22) => r22.group_id));
|
|
66367
|
-
if (!liveGroupIds.has(staleGid)) {
|
|
67005
|
+
if (staleGid && staleMlsGroupId && !liveGroupIds.has(staleGid)) {
|
|
66368
67006
|
try {
|
|
66369
67007
|
await deleteMlsState(this.config.dataDir, staleMlsGroupId);
|
|
66370
67008
|
} catch {
|
|
@@ -66378,9 +67016,9 @@ var init_channel = __esm2({
|
|
|
66378
67016
|
}
|
|
66379
67017
|
await this._persistState();
|
|
66380
67018
|
console.log(
|
|
66381
|
-
`[SecureChannel] Re-resolved conversation groups after 'unknown group' ${staleMlsGroupId.slice(0, 8)}: primary group ${String(before).slice(0, 8)} \u2192 ${this._persisted.groupId.slice(0, 8)} (${mine.length} active conversation(s))`
|
|
67019
|
+
`[SecureChannel] Re-resolved conversation groups ${staleMlsGroupId ? `after 'unknown group' ${staleMlsGroupId.slice(0, 8)}` : "on connect (#1034)"}: primary group ${String(before).slice(0, 8)} \u2192 ${this._persisted.groupId.slice(0, 8)} (${mine.length} active conversation(s))`
|
|
66382
67020
|
);
|
|
66383
|
-
return this._persisted.groupId !== before && before === staleGid;
|
|
67021
|
+
return staleGid !== void 0 && this._persisted.groupId !== before && before === staleGid;
|
|
66384
67022
|
}
|
|
66385
67023
|
/**
|
|
66386
67024
|
* Resend the message that a now-reconciled `unknown group` refusal killed (#732).
|
|
@@ -67288,7 +67926,13 @@ var init_channel = __esm2({
|
|
|
67288
67926
|
const mlsGroup = this._mlsGroups.get(`a2a:${channelEntry.channelId}`);
|
|
67289
67927
|
if (mlsGroup?.isInitialized) {
|
|
67290
67928
|
const plaintextBytes = new TextEncoder().encode(text);
|
|
67291
|
-
const ciphertext = await
|
|
67929
|
+
const ciphertext = await this._encryptWithCatchUp(
|
|
67930
|
+
mlsGroup,
|
|
67931
|
+
`a2a:${channelEntry.channelId}`,
|
|
67932
|
+
channelEntry.mlsGroupId,
|
|
67933
|
+
`A2A ${channelEntry.channelId.slice(0, 8)}`,
|
|
67934
|
+
plaintextBytes
|
|
67935
|
+
);
|
|
67292
67936
|
await saveMlsState(
|
|
67293
67937
|
this.config.dataDir,
|
|
67294
67938
|
channelEntry.mlsGroupId,
|
|
@@ -67336,7 +67980,13 @@ var init_channel = __esm2({
|
|
|
67336
67980
|
const mlsGroup = this._mlsGroups.get(`a2a:${channelId}`);
|
|
67337
67981
|
if (mlsGroup?.isInitialized) {
|
|
67338
67982
|
const plaintextBytes = new TextEncoder().encode(text);
|
|
67339
|
-
const ciphertext = await
|
|
67983
|
+
const ciphertext = await this._encryptWithCatchUp(
|
|
67984
|
+
mlsGroup,
|
|
67985
|
+
`a2a:${channelId}`,
|
|
67986
|
+
entry.mlsGroupId,
|
|
67987
|
+
`A2A ${channelId.slice(0, 8)}`,
|
|
67988
|
+
plaintextBytes
|
|
67989
|
+
);
|
|
67340
67990
|
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
67341
67991
|
this._ws.send(JSON.stringify({
|
|
67342
67992
|
event: "a2a_message_mls",
|
|
@@ -67782,6 +68432,9 @@ var init_channel = __esm2({
|
|
|
67782
68432
|
await this._pullDrDeliveryQueue();
|
|
67783
68433
|
await this._flushOutboundQueue();
|
|
67784
68434
|
this._setState("ready");
|
|
68435
|
+
void this._reconcileConversationGroups().catch((err) => {
|
|
68436
|
+
console.warn("[SecureChannel] Conversation-group reconcile failed:", err);
|
|
68437
|
+
});
|
|
67785
68438
|
void this._reconcileRoomsWithServer().catch(
|
|
67786
68439
|
(err) => console.warn(`[SecureChannel] room reconcile failed (ignored): ${err instanceof Error ? err.message : String(err)}`)
|
|
67787
68440
|
).then(() => this._quarantineOrphanedMlsGroups()).catch(
|
|
@@ -67809,7 +68462,7 @@ var init_channel = __esm2({
|
|
|
67809
68462
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
67810
68463
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
67811
68464
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
67812
|
-
pluginVersion: true ? "0.23.
|
|
68465
|
+
pluginVersion: true ? "0.23.27" : "0.0.0-dev"
|
|
67813
68466
|
});
|
|
67814
68467
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
67815
68468
|
}
|
|
@@ -68133,7 +68786,7 @@ var init_channel = __esm2({
|
|
|
68133
68786
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
68134
68787
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
68135
68788
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
68136
|
-
pluginVersion: true ? "0.23.
|
|
68789
|
+
pluginVersion: true ? "0.23.27" : "0.0.0-dev"
|
|
68137
68790
|
});
|
|
68138
68791
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
68139
68792
|
}
|
|
@@ -68499,6 +69152,7 @@ var init_channel = __esm2({
|
|
|
68499
69152
|
const convGroupId = data.conversation_group_id;
|
|
68500
69153
|
const groupId = data.group_id;
|
|
68501
69154
|
const senderDeviceId = data.sender_device_id;
|
|
69155
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
68502
69156
|
if (senderDeviceId === this._deviceId) return;
|
|
68503
69157
|
let mgr;
|
|
68504
69158
|
let mgrKey;
|
|
@@ -68576,9 +69230,15 @@ var init_channel = __esm2({
|
|
|
68576
69230
|
})),
|
|
68577
69231
|
no_history: filtered.length === 0
|
|
68578
69232
|
});
|
|
68579
|
-
if (mgr && mlsGroupId) {
|
|
69233
|
+
if (mgr && mgrKey && mlsGroupId) {
|
|
68580
69234
|
const responseBytes = new TextEncoder().encode(responsePayload);
|
|
68581
|
-
const cipher = await
|
|
69235
|
+
const cipher = await this._encryptWithCatchUp(
|
|
69236
|
+
mgr,
|
|
69237
|
+
mgrKey,
|
|
69238
|
+
mlsGroupId,
|
|
69239
|
+
`history catch-up ${mlsGroupId.slice(0, 8)}`,
|
|
69240
|
+
responseBytes
|
|
69241
|
+
);
|
|
68582
69242
|
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mgr.exportState()));
|
|
68583
69243
|
if (this._ws) {
|
|
68584
69244
|
this._ws.send(JSON.stringify({
|
|
@@ -69321,9 +69981,36 @@ ${messageText}`;
|
|
|
69321
69981
|
// ---------------------------------------------------------------------------
|
|
69322
69982
|
// MLS room message handlers
|
|
69323
69983
|
// ---------------------------------------------------------------------------
|
|
69984
|
+
/**
|
|
69985
|
+
* #1014: hand a room-framed frame to the handler that owns its group family.
|
|
69986
|
+
*
|
|
69987
|
+
* Returns true when the frame was routed. Routing means "give it to the right
|
|
69988
|
+
* handler" — NOT "decrypt it as a room message". Decrypting under the wrong
|
|
69989
|
+
* group is how MLS state gets corrupted, so an unplaceable group is left for
|
|
69990
|
+
* the caller to self-heal rather than guessed at.
|
|
69991
|
+
*/
|
|
69992
|
+
async _routeNonRoomGroupMessage(data, groupId) {
|
|
69993
|
+
const target = this._resolveGroupFamily(groupId);
|
|
69994
|
+
if (!target || target.family === "room") return false;
|
|
69995
|
+
console.log(
|
|
69996
|
+
`[SecureChannel] Room-framed message belongs to ${target.label} \u2014 routing to the handler that owns it (#1014)`
|
|
69997
|
+
);
|
|
69998
|
+
switch (target.family) {
|
|
69999
|
+
case "shared1to1":
|
|
70000
|
+
await this._handleMessageMLS({ ...data, conversation_group_id: target.id });
|
|
70001
|
+
return true;
|
|
70002
|
+
case "conversation":
|
|
70003
|
+
await this._handleMessageMLS({ ...data, conversation_id: target.id });
|
|
70004
|
+
return true;
|
|
70005
|
+
case "a2a":
|
|
70006
|
+
await this._handleA2AMessageMLS({ ...data, a2a_channel_id: target.id });
|
|
70007
|
+
return true;
|
|
70008
|
+
}
|
|
70009
|
+
}
|
|
69324
70010
|
async _handleRoomMessageMLS(data) {
|
|
69325
70011
|
const groupId = data.group_id;
|
|
69326
70012
|
const senderDeviceId = data.sender_device_id;
|
|
70013
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
69327
70014
|
if (senderDeviceId === this._deviceId) return;
|
|
69328
70015
|
let roomId;
|
|
69329
70016
|
for (const [rid, room] of Object.entries(this._persisted?.rooms ?? {})) {
|
|
@@ -69337,7 +70024,9 @@ ${messageText}`;
|
|
|
69337
70024
|
roomId = data.room_id;
|
|
69338
70025
|
console.log(`[SecureChannel] Room ${roomId.slice(0, 8)} matched by room_id (group_id ${groupId?.slice(0, 8)} mismatch)`);
|
|
69339
70026
|
} else {
|
|
70027
|
+
if (await this._routeNonRoomGroupMessage(data, groupId)) return;
|
|
69340
70028
|
console.warn(`[SecureChannel] No room found for MLS group ${groupId?.slice(0, 8)}`);
|
|
70029
|
+
if (groupId) void this._requestWelcomeSelfHeal(groupId);
|
|
69341
70030
|
return;
|
|
69342
70031
|
}
|
|
69343
70032
|
}
|
|
@@ -69525,63 +70214,63 @@ ${messageText}`;
|
|
|
69525
70214
|
console.warn(`[SecureChannel] Failed to send history catchup response:`, sendErr);
|
|
69526
70215
|
}
|
|
69527
70216
|
}
|
|
70217
|
+
/**
|
|
70218
|
+
* Apply an inbound MLS commit to whichever group it addresses.
|
|
70219
|
+
*
|
|
70220
|
+
* Returns TRUE when the commit was applied, buffered, or handed to the
|
|
70221
|
+
* failure ladder — i.e. when this agent has taken responsibility for it and
|
|
70222
|
+
* the delivery-queue row may safely be acked. Returns FALSE when the commit
|
|
70223
|
+
* names a group we hold no state for at all, so the caller must NACK and let
|
|
70224
|
+
* the row survive.
|
|
70225
|
+
*
|
|
70226
|
+
* #1012: this used to loop `rooms` then `a2aChannels` and then END. A shared
|
|
70227
|
+
* 1:1 commit (`mlsGroups`) and a legacy per-conversation commit
|
|
70228
|
+
* (`mlsConversations`) matched neither. The call fell off the bottom with no
|
|
70229
|
+
* log, no buffer and no error — and because it did not THROW,
|
|
70230
|
+
* `_pullDeliveryQueue` acked the row and the server deleted it. An MLS commit
|
|
70231
|
+
* cannot be regenerated, so the agent stayed at epoch N while the group moved
|
|
70232
|
+
* to N+1 and every later send() encrypted at an epoch nobody could decrypt.
|
|
70233
|
+
*
|
|
70234
|
+
* The families are enumerated in ONE list resolved by ONE loop, because two
|
|
70235
|
+
* hand-copied loops are exactly how the 1:1 path fell two families behind.
|
|
70236
|
+
*/
|
|
69528
70237
|
async _handleMlsCommit(data) {
|
|
69529
70238
|
const groupId = data.group_id;
|
|
69530
70239
|
const epoch = typeof data.epoch === "number" ? data.epoch : 0;
|
|
69531
|
-
|
|
69532
|
-
|
|
69533
|
-
|
|
69534
|
-
|
|
69535
|
-
|
|
69536
|
-
|
|
69537
|
-
|
|
69538
|
-
|
|
69539
|
-
|
|
69540
|
-
this._mlsCommitFailCounts.delete(roomId);
|
|
69541
|
-
} catch (err) {
|
|
69542
|
-
await this._onCommitFailure(
|
|
69543
|
-
roomId,
|
|
69544
|
-
groupId,
|
|
69545
|
-
err,
|
|
69546
|
-
`room ${roomId.slice(0, 8)}`,
|
|
69547
|
-
data.epoch,
|
|
69548
|
-
mlsGroup.epoch
|
|
69549
|
-
);
|
|
69550
|
-
}
|
|
69551
|
-
} else {
|
|
69552
|
-
this._bufferMlsCommit(groupId, epoch, data);
|
|
69553
|
-
console.log(`[SecureChannel] Buffered MLS commit for room ${roomId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
|
|
69554
|
-
}
|
|
69555
|
-
return;
|
|
70240
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
70241
|
+
const target = this._resolveGroupFamily(groupId);
|
|
70242
|
+
if (target) {
|
|
70243
|
+
const { managerKey: groupKey, label } = target;
|
|
70244
|
+
const mlsGroup = this._mlsGroups.get(groupKey);
|
|
70245
|
+
if (!mlsGroup?.isInitialized) {
|
|
70246
|
+
this._bufferMlsCommit(groupId, epoch, data);
|
|
70247
|
+
console.log(`[SecureChannel] Buffered MLS commit for ${label} (epoch=${epoch}, group not initialized)`);
|
|
70248
|
+
return true;
|
|
69556
70249
|
}
|
|
69557
|
-
|
|
69558
|
-
|
|
69559
|
-
|
|
69560
|
-
|
|
69561
|
-
|
|
69562
|
-
|
|
69563
|
-
|
|
69564
|
-
|
|
69565
|
-
|
|
69566
|
-
|
|
69567
|
-
|
|
69568
|
-
|
|
69569
|
-
|
|
69570
|
-
|
|
69571
|
-
|
|
69572
|
-
err,
|
|
69573
|
-
`A2A ${chId.slice(0, 8)}`,
|
|
69574
|
-
data.epoch,
|
|
69575
|
-
mlsGroup.epoch
|
|
69576
|
-
);
|
|
69577
|
-
}
|
|
69578
|
-
} else {
|
|
69579
|
-
this._bufferMlsCommit(groupId, epoch, data);
|
|
69580
|
-
console.log(`[SecureChannel] Buffered MLS commit for A2A ${chId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
|
|
69581
|
-
}
|
|
69582
|
-
return;
|
|
70250
|
+
try {
|
|
70251
|
+
const commitBytes = new Uint8Array(Buffer.from(data.payload, "hex"));
|
|
70252
|
+
await mlsGroup.processCommit(commitBytes);
|
|
70253
|
+
await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
|
|
70254
|
+
console.log(`[SecureChannel] MLS commit processed for ${label} (epoch=${mlsGroup.epoch})`);
|
|
70255
|
+
this._mlsCommitFailCounts.delete(groupKey);
|
|
70256
|
+
} catch (err) {
|
|
70257
|
+
await this._onCommitFailure(
|
|
70258
|
+
groupKey,
|
|
70259
|
+
groupId,
|
|
70260
|
+
err,
|
|
70261
|
+
label,
|
|
70262
|
+
data.epoch,
|
|
70263
|
+
mlsGroup.epoch
|
|
70264
|
+
);
|
|
69583
70265
|
}
|
|
70266
|
+
return true;
|
|
69584
70267
|
}
|
|
70268
|
+
this._bufferMlsCommit(groupId, epoch, data);
|
|
70269
|
+
console.warn(
|
|
70270
|
+
`[SecureChannel] MLS commit for UNKNOWN group ${String(groupId).slice(0, 8)} (epoch=${epoch}) \u2014 buffered, requesting a Welcome, NOT acked`
|
|
70271
|
+
);
|
|
70272
|
+
if (groupId) void this._requestWelcomeSelfHeal(groupId);
|
|
70273
|
+
return false;
|
|
69585
70274
|
}
|
|
69586
70275
|
/** Buffer an MLS commit for replay after Welcome join. Max 50 per group. */
|
|
69587
70276
|
_bufferMlsCommit(groupId, epoch, data) {
|
|
@@ -69771,6 +70460,229 @@ ${messageText}`;
|
|
|
69771
70460
|
* `1to1-group:<gid>`, `a2a:<channelId>`, …) — also the decrypt-fail-count key.
|
|
69772
70461
|
* @param groupId the server-side MLS group id used in the /sync URL.
|
|
69773
70462
|
*/
|
|
70463
|
+
/**
|
|
70464
|
+
* #1015 — the ONLY place this package fulfils a Welcome request.
|
|
70465
|
+
*
|
|
70466
|
+
* Add the members, transmit, and record `fulfilled` **only for the targets
|
|
70467
|
+
* whose frames actually went out**. Every one of the five sites that used to
|
|
70468
|
+
* do this by hand followed the same sequence and had the same three holes:
|
|
70469
|
+
* `this._ws` null (both sends skipped by an `if`), a CLOSING socket (`send`
|
|
70470
|
+
* returns without throwing), and a fulfil POST wrapped in `.catch(() => {})`.
|
|
70471
|
+
*
|
|
70472
|
+
* A fulfilled request is never re-requested. So a Welcome that evaporated
|
|
70473
|
+
* left the requester locked out, the fulfiller at epoch N+1, everyone else at
|
|
70474
|
+
* N, and the bridging commit untransmitted and impossible to regenerate — a
|
|
70475
|
+
* permanently wedged group in which every signal read success.
|
|
70476
|
+
*
|
|
70477
|
+
* Returns the number of requests actually fulfilled.
|
|
70478
|
+
*/
|
|
70479
|
+
async _fulfilWelcomeRequests(opts) {
|
|
70480
|
+
const { mlsGroup, mlsGroupId, label, welcomeFields, targets } = opts;
|
|
70481
|
+
if (targets.length === 0) return 0;
|
|
70482
|
+
if (!this._wsIsOpen()) {
|
|
70483
|
+
console.warn(
|
|
70484
|
+
`[SecureChannel] ${label}: socket is not OPEN \u2014 NOT advancing the group; ${targets.length} request(s) stay pending for the next pull`
|
|
70485
|
+
);
|
|
70486
|
+
return 0;
|
|
70487
|
+
}
|
|
70488
|
+
const snapshot = mlsGroup.exportState();
|
|
70489
|
+
const { commit, welcome } = await mlsGroup.addMembers(targets.map((t22) => t22.keyPackage));
|
|
70490
|
+
const commitSent = await this._sendFrameConfirmed(
|
|
70491
|
+
{
|
|
70492
|
+
event: "mls_commit",
|
|
70493
|
+
data: {
|
|
70494
|
+
group_id: mlsGroupId,
|
|
70495
|
+
epoch: Number(mlsGroup.epoch),
|
|
70496
|
+
payload: Buffer.from(commit).toString("hex")
|
|
70497
|
+
}
|
|
70498
|
+
},
|
|
70499
|
+
`${label} commit`
|
|
70500
|
+
);
|
|
70501
|
+
if (!commitSent) {
|
|
70502
|
+
mlsGroup.importState(snapshot);
|
|
70503
|
+
console.warn(
|
|
70504
|
+
`[SecureChannel] ${label}: commit was NOT written \u2014 rolled back to epoch ${Number(mlsGroup.epoch)}, nothing fulfilled, requests stay pending`
|
|
70505
|
+
);
|
|
70506
|
+
return 0;
|
|
70507
|
+
}
|
|
70508
|
+
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70509
|
+
if (!welcome) {
|
|
70510
|
+
console.warn(
|
|
70511
|
+
`[SecureChannel] ${label}: addMembers produced NO Welcome \u2014 ${targets.length} request(s) left pending rather than marked fulfilled`
|
|
70512
|
+
);
|
|
70513
|
+
return 0;
|
|
70514
|
+
}
|
|
70515
|
+
let fulfilled = 0;
|
|
70516
|
+
for (const t22 of targets) {
|
|
70517
|
+
const sent = await this._sendFrameConfirmed(
|
|
70518
|
+
{
|
|
70519
|
+
event: "mls_welcome",
|
|
70520
|
+
data: {
|
|
70521
|
+
target_device_id: t22.deviceId,
|
|
70522
|
+
group_id: mlsGroupId,
|
|
70523
|
+
...welcomeFields,
|
|
70524
|
+
payload: Buffer.from(welcome).toString("hex")
|
|
70525
|
+
}
|
|
70526
|
+
},
|
|
70527
|
+
`${label} welcome\u2192${t22.deviceId.slice(0, 8)}`
|
|
70528
|
+
);
|
|
70529
|
+
if (!sent) {
|
|
70530
|
+
console.warn(
|
|
70531
|
+
`[SecureChannel] ${label}: Welcome for ${t22.deviceId.slice(0, 8)} was NOT written \u2014 leaving the request PENDING so the next pull retries it`
|
|
70532
|
+
);
|
|
70533
|
+
continue;
|
|
70534
|
+
}
|
|
70535
|
+
if (await this._postFulfilWelcome(mlsGroupId, t22.requestId, `${label} ${t22.deviceId.slice(0, 8)}`)) {
|
|
70536
|
+
fulfilled++;
|
|
70537
|
+
}
|
|
70538
|
+
}
|
|
70539
|
+
return fulfilled;
|
|
70540
|
+
}
|
|
70541
|
+
/**
|
|
70542
|
+
* Resolve an MLS group id to the family that owns it.
|
|
70543
|
+
*
|
|
70544
|
+
* There are four: rooms, A2A channels, shared 1:1 groups, and the legacy
|
|
70545
|
+
* per-conversation groups. They all store the MLS group id under
|
|
70546
|
+
* `mlsGroupId`; they differ only in the key their live `MLSGroupManager` sits
|
|
70547
|
+
* under in `_mlsGroups`.
|
|
70548
|
+
*
|
|
70549
|
+
* ONE resolver, because hand-copying this enumeration is what produced
|
|
70550
|
+
* #1012 (`_handleMlsCommit` handled two families of four), #1014 (a
|
|
70551
|
+
* room-framed 1:1 replay matched nothing) and #1016 (a rejoin dropped only
|
|
70552
|
+
* the room manager). Three bugs, one missing abstraction.
|
|
70553
|
+
*/
|
|
70554
|
+
_resolveGroupFamily(mlsGroupId) {
|
|
70555
|
+
if (!mlsGroupId) return null;
|
|
70556
|
+
const families = [
|
|
70557
|
+
{ family: "room", entries: this._persisted?.rooms, managerKey: (id) => id, label: "room" },
|
|
70558
|
+
{ family: "a2a", entries: this._persisted?.a2aChannels, managerKey: (id) => `a2a:${id}`, label: "A2A" },
|
|
70559
|
+
{ family: "shared1to1", entries: this._persisted?.mlsGroups, managerKey: (id) => `1to1-group:${id}`, label: "1:1 group" },
|
|
70560
|
+
{ family: "conversation", entries: this._persisted?.mlsConversations, managerKey: (id) => `conv:${id}`, label: "conversation" }
|
|
70561
|
+
];
|
|
70562
|
+
for (const f22 of families) {
|
|
70563
|
+
for (const [id, entry] of Object.entries(f22.entries ?? {})) {
|
|
70564
|
+
if (entry?.mlsGroupId !== mlsGroupId) continue;
|
|
70565
|
+
return {
|
|
70566
|
+
family: f22.family,
|
|
70567
|
+
id,
|
|
70568
|
+
managerKey: f22.managerKey(id),
|
|
70569
|
+
label: `${f22.label} ${id.slice(0, 8)}`
|
|
70570
|
+
};
|
|
70571
|
+
}
|
|
70572
|
+
}
|
|
70573
|
+
return null;
|
|
70574
|
+
}
|
|
70575
|
+
/** #1015: is the socket in a state that can actually take a write? */
|
|
70576
|
+
_wsIsOpen() {
|
|
70577
|
+
return this._ws?.readyState === 1;
|
|
70578
|
+
}
|
|
70579
|
+
/**
|
|
70580
|
+
* #1015: write one frame and report whether the socket took it.
|
|
70581
|
+
*
|
|
70582
|
+
* `ws.send()` on a CLOSING or CLOSED socket returns WITHOUT throwing — the
|
|
70583
|
+
* error surfaces only through the callback, and every fulfilment site passed
|
|
70584
|
+
* none. So a Welcome could be "sent" into a dead socket and the requester
|
|
70585
|
+
* still marked `fulfilled`, which it never re-requests.
|
|
70586
|
+
*/
|
|
70587
|
+
_sendFrameConfirmed(frame, label) {
|
|
70588
|
+
const ws = this._ws;
|
|
70589
|
+
if (!ws || ws.readyState !== 1) {
|
|
70590
|
+
console.warn(
|
|
70591
|
+
`[SecureChannel] ${label}: socket is not OPEN (readyState=${ws?.readyState ?? "null"}) \u2014 frame NOT written`
|
|
70592
|
+
);
|
|
70593
|
+
return Promise.resolve(false);
|
|
70594
|
+
}
|
|
70595
|
+
return new Promise((resolve32) => {
|
|
70596
|
+
try {
|
|
70597
|
+
ws.send(JSON.stringify(frame), (err) => {
|
|
70598
|
+
if (err) {
|
|
70599
|
+
console.error(`[SecureChannel] ${label}: socket refused the frame:`, err);
|
|
70600
|
+
resolve32(false);
|
|
70601
|
+
} else {
|
|
70602
|
+
resolve32(true);
|
|
70603
|
+
}
|
|
70604
|
+
});
|
|
70605
|
+
} catch (err) {
|
|
70606
|
+
console.error(`[SecureChannel] ${label}: send threw:`, err);
|
|
70607
|
+
resolve32(false);
|
|
70608
|
+
}
|
|
70609
|
+
});
|
|
70610
|
+
}
|
|
70611
|
+
/**
|
|
70612
|
+
* #1015: record a Welcome as fulfilled — and say so when it fails.
|
|
70613
|
+
*
|
|
70614
|
+
* The batched shared-1:1 site posted this with `.catch(() => {})`, so an HTTP
|
|
70615
|
+
* failure here was invisible on top of the sends being invisible.
|
|
70616
|
+
*/
|
|
70617
|
+
async _postFulfilWelcome(mlsGroupId, requestId, label) {
|
|
70618
|
+
try {
|
|
70619
|
+
const res = await fetch(
|
|
70620
|
+
`${this.config.apiUrl}/api/v1/mls/groups/${mlsGroupId}/fulfill-welcome`,
|
|
70621
|
+
{
|
|
70622
|
+
method: "POST",
|
|
70623
|
+
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70624
|
+
body: JSON.stringify({ request_id: requestId })
|
|
70625
|
+
}
|
|
70626
|
+
);
|
|
70627
|
+
if (!res.ok) {
|
|
70628
|
+
console.warn(`[SecureChannel] ${label}: fulfill-welcome returned ${res.status}`);
|
|
70629
|
+
return false;
|
|
70630
|
+
}
|
|
70631
|
+
return true;
|
|
70632
|
+
} catch (err) {
|
|
70633
|
+
console.warn(`[SecureChannel] ${label}: fulfill-welcome failed:`, err);
|
|
70634
|
+
return false;
|
|
70635
|
+
}
|
|
70636
|
+
}
|
|
70637
|
+
/**
|
|
70638
|
+
* #1013: fold an epoch seen on an inbound frame into the running hint.
|
|
70639
|
+
*
|
|
70640
|
+
* Anything unusable (missing, non-numeric, negative, fractional) is ignored
|
|
70641
|
+
* rather than defaulted. A defaulted epoch here would be a guess, and a guess
|
|
70642
|
+
* that reads LOW silently disables the check it exists to drive.
|
|
70643
|
+
*/
|
|
70644
|
+
_noteGroupEpoch(mlsGroupId, epoch) {
|
|
70645
|
+
if (typeof mlsGroupId !== "string" || !mlsGroupId) return;
|
|
70646
|
+
const current = this._observedGroupEpochs.get(mlsGroupId) ?? 0;
|
|
70647
|
+
const next = noteObservedEpoch(current, epoch);
|
|
70648
|
+
if (next > current) this._observedGroupEpochs.set(mlsGroupId, next);
|
|
70649
|
+
}
|
|
70650
|
+
/**
|
|
70651
|
+
* #1013 — the ONLY place this package encrypts an MLS application message.
|
|
70652
|
+
*
|
|
70653
|
+
* Catch up first when the group is known to have moved past us, then encrypt.
|
|
70654
|
+
* Hermes has done this since #440 and the browser since #996; the plugin —
|
|
70655
|
+
* the implementation that actually ships to customers — did not, so an agent
|
|
70656
|
+
* that missed one commit encrypted every later message at an epoch nobody
|
|
70657
|
+
* could read, while the server stored and relayed it and every signal read
|
|
70658
|
+
* 200.
|
|
70659
|
+
*
|
|
70660
|
+
* Funnelling every sender through one method is the point. A guard applied at
|
|
70661
|
+
* seven call sites decays to a guard applied at six: that is exactly how
|
|
70662
|
+
* `_handleMlsCommit` ended up handling two group families out of four (#1012).
|
|
70663
|
+
*
|
|
70664
|
+
* Best-effort by design. A catch-up that cannot replay — or that throws — must
|
|
70665
|
+
* NOT block the send; we fall through and encrypt at the local epoch, which is
|
|
70666
|
+
* precisely today's behaviour, so the worst case is no regression.
|
|
70667
|
+
*/
|
|
70668
|
+
async _encryptWithCatchUp(mlsGroup, groupKey, mlsGroupId, label, plaintextBytes) {
|
|
70669
|
+
const observed = this._observedGroupEpochs.get(mlsGroupId);
|
|
70670
|
+
if (observed !== void 0 && needsPreSendResync(Number(mlsGroup.epoch), observed)) {
|
|
70671
|
+
console.warn(
|
|
70672
|
+
`[SecureChannel] ${label}: local epoch ${Number(mlsGroup.epoch)} is behind the observed group epoch ${observed} \u2014 catching up before encrypt (#1013)`
|
|
70673
|
+
);
|
|
70674
|
+
try {
|
|
70675
|
+
if (!await this._resyncOnDivergence(groupKey, mlsGroupId)) {
|
|
70676
|
+
console.warn(
|
|
70677
|
+
`[SecureChannel] ${label}: catch-up could not replay \u2014 sending at epoch ${Number(mlsGroup.epoch)}. Peers past this epoch will not read it.`
|
|
70678
|
+
);
|
|
70679
|
+
}
|
|
70680
|
+
} catch (err) {
|
|
70681
|
+
console.warn(`[SecureChannel] ${label}: catch-up failed:`, err);
|
|
70682
|
+
}
|
|
70683
|
+
}
|
|
70684
|
+
return await mlsGroup.encrypt(plaintextBytes);
|
|
70685
|
+
}
|
|
69774
70686
|
async _resyncOnDivergence(groupKey, groupId) {
|
|
69775
70687
|
const mgr = this._mlsGroups.get(groupKey);
|
|
69776
70688
|
if (!mgr?.isInitialized) return false;
|
|
@@ -69797,11 +70709,24 @@ ${messageText}`;
|
|
|
69797
70709
|
applied++;
|
|
69798
70710
|
}
|
|
69799
70711
|
if (applied === 0) {
|
|
69800
|
-
|
|
70712
|
+
const strikes = (this._resyncNoOpStrikes.get(groupKey) ?? 0) + 1;
|
|
70713
|
+
this._resyncNoOpStrikes.set(groupKey, strikes);
|
|
70714
|
+
if (strikes < _SecureChannel.MAX_RESYNC_NOOPS) {
|
|
70715
|
+
this._mlsDecryptFailCounts.delete(groupKey);
|
|
70716
|
+
console.log(
|
|
70717
|
+
`[SecureChannel] Resync for ${groupId.slice(0, 8)}: already current at epoch=${mgr.epoch} (strike ${strikes}/${_SecureChannel.MAX_RESYNC_NOOPS}) \u2014 NOT re-keying (#1084)`
|
|
70718
|
+
);
|
|
70719
|
+
return true;
|
|
70720
|
+
}
|
|
70721
|
+
this._resyncNoOpStrikes.delete(groupKey);
|
|
70722
|
+
console.warn(
|
|
70723
|
+
`[SecureChannel] Resync for ${groupId.slice(0, 8)}: still failing at epoch=${mgr.epoch} after ${_SecureChannel.MAX_RESYNC_NOOPS} no-op resyncs \u2014 escalating to re-key`
|
|
70724
|
+
);
|
|
69801
70725
|
return false;
|
|
69802
70726
|
}
|
|
69803
70727
|
await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mgr.exportState()));
|
|
69804
70728
|
this._mlsDecryptFailCounts.delete(groupKey);
|
|
70729
|
+
this._resyncNoOpStrikes.delete(groupKey);
|
|
69805
70730
|
console.log(
|
|
69806
70731
|
`[SecureChannel] Resync-on-divergence: applied ${applied} missed commit(s) for ${groupId.slice(0, 8)}, now epoch=${mgr.epoch}`
|
|
69807
70732
|
);
|
|
@@ -70188,6 +71113,7 @@ ${messageText}`;
|
|
|
70188
71113
|
const ackedIds = [];
|
|
70189
71114
|
const nackedIds = [];
|
|
70190
71115
|
for (const msg of messages) {
|
|
71116
|
+
let handled = true;
|
|
70191
71117
|
try {
|
|
70192
71118
|
if (msg.sender_device_id === this._deviceId) {
|
|
70193
71119
|
ackedIds.push(msg.queue_id);
|
|
@@ -70204,7 +71130,7 @@ ${messageText}`;
|
|
|
70204
71130
|
a2a_channel_id: msg.a2a_channel_id
|
|
70205
71131
|
});
|
|
70206
71132
|
} else if (msg.message_type === "commit") {
|
|
70207
|
-
await this._handleMlsCommit({
|
|
71133
|
+
handled = await this._handleMlsCommit({
|
|
70208
71134
|
group_id: msg.group_id,
|
|
70209
71135
|
sender_device_id: msg.sender_device_id,
|
|
70210
71136
|
epoch: msg.epoch,
|
|
@@ -70242,9 +71168,18 @@ ${messageText}`;
|
|
|
70242
71168
|
conversation_group_id: msg.conversation_group_id,
|
|
70243
71169
|
created_at: msg.created_at
|
|
70244
71170
|
});
|
|
71171
|
+
} else {
|
|
71172
|
+
handled = false;
|
|
71173
|
+
console.warn(
|
|
71174
|
+
`[SecureChannel] Delivery application row ${String(msg.message_id).slice(0, 8)} names no room, A2A channel or conversation \u2014 NOT acked`
|
|
71175
|
+
);
|
|
70245
71176
|
}
|
|
70246
71177
|
}
|
|
70247
|
-
|
|
71178
|
+
if (handled) {
|
|
71179
|
+
ackedIds.push(msg.queue_id);
|
|
71180
|
+
} else {
|
|
71181
|
+
nackedIds.push(msg.queue_id);
|
|
71182
|
+
}
|
|
70248
71183
|
} catch (err) {
|
|
70249
71184
|
console.warn(`[SecureChannel] Delivery ${msg.message_type} processing failed:`, err);
|
|
70250
71185
|
nackedIds.push(msg.queue_id);
|
|
@@ -70313,38 +71248,20 @@ ${messageText}`;
|
|
|
70313
71248
|
if (!kpHex) continue;
|
|
70314
71249
|
const kpBytes = new Uint8Array(Buffer.from(kpHex, "hex"));
|
|
70315
71250
|
const memberKp = MLSGroupManager.deserializeKeyPackage(kpBytes);
|
|
70316
|
-
const
|
|
70317
|
-
|
|
70318
|
-
|
|
70319
|
-
|
|
70320
|
-
|
|
70321
|
-
|
|
70322
|
-
|
|
70323
|
-
|
|
70324
|
-
|
|
70325
|
-
}
|
|
70326
|
-
}
|
|
70327
|
-
if (
|
|
70328
|
-
|
|
70329
|
-
event: "mls_welcome",
|
|
70330
|
-
data: {
|
|
70331
|
-
target_device_id: req.requesting_device_id,
|
|
70332
|
-
group_id: roomState.mlsGroupId,
|
|
70333
|
-
room_id: roomId,
|
|
70334
|
-
payload: Buffer.from(welcome).toString("hex")
|
|
70335
|
-
}
|
|
70336
|
-
}));
|
|
71251
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71252
|
+
mlsGroup,
|
|
71253
|
+
mlsGroupId: roomState.mlsGroupId,
|
|
71254
|
+
label: `room ${roomId.slice(0, 8)}`,
|
|
71255
|
+
welcomeFields: { room_id: roomId },
|
|
71256
|
+
targets: [{
|
|
71257
|
+
requestId: req.id,
|
|
71258
|
+
deviceId: req.requesting_device_id,
|
|
71259
|
+
keyPackage: memberKp
|
|
71260
|
+
}]
|
|
71261
|
+
});
|
|
71262
|
+
if (done > 0) {
|
|
71263
|
+
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in room ${roomId.slice(0, 8)}`);
|
|
70337
71264
|
}
|
|
70338
|
-
await fetch(
|
|
70339
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${roomState.mlsGroupId}/fulfill-welcome`,
|
|
70340
|
-
{
|
|
70341
|
-
method: "POST",
|
|
70342
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70343
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70344
|
-
}
|
|
70345
|
-
);
|
|
70346
|
-
await saveMlsState(this.config.dataDir, roomState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70347
|
-
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in room ${roomId.slice(0, 8)}`);
|
|
70348
71265
|
} catch (fulfillErr) {
|
|
70349
71266
|
console.warn(`[SecureChannel] Welcome fulfill failed for ${req.requesting_device_id.slice(0, 8)}:`, fulfillErr);
|
|
70350
71267
|
}
|
|
@@ -70383,65 +71300,38 @@ ${messageText}`;
|
|
|
70383
71300
|
validReqs.push({ req, kp: MLSGroupManager.deserializeKeyPackage(kpBytes) });
|
|
70384
71301
|
}
|
|
70385
71302
|
if (validReqs.length === 0) continue;
|
|
71303
|
+
const sharedTargets = validReqs.map((v22) => ({
|
|
71304
|
+
requestId: v22.req.id,
|
|
71305
|
+
deviceId: v22.req.requesting_device_id,
|
|
71306
|
+
keyPackage: v22.kp
|
|
71307
|
+
}));
|
|
70386
71308
|
try {
|
|
70387
|
-
const
|
|
70388
|
-
|
|
70389
|
-
|
|
70390
|
-
|
|
70391
|
-
|
|
70392
|
-
|
|
70393
|
-
|
|
70394
|
-
|
|
70395
|
-
|
|
70396
|
-
}));
|
|
70397
|
-
}
|
|
70398
|
-
if (welcome && this._ws) {
|
|
70399
|
-
for (const { req } of validReqs) {
|
|
70400
|
-
this._ws.send(JSON.stringify({
|
|
70401
|
-
event: "mls_welcome",
|
|
70402
|
-
data: {
|
|
70403
|
-
target_device_id: req.requesting_device_id,
|
|
70404
|
-
group_id: entry.mlsGroupId,
|
|
70405
|
-
conversation_group_id: gid,
|
|
70406
|
-
payload: Buffer.from(welcome).toString("hex")
|
|
70407
|
-
}
|
|
70408
|
-
}));
|
|
70409
|
-
}
|
|
70410
|
-
}
|
|
70411
|
-
for (const { req } of validReqs) {
|
|
70412
|
-
await fetch(
|
|
70413
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${entry.mlsGroupId}/fulfill-welcome`,
|
|
70414
|
-
{
|
|
70415
|
-
method: "POST",
|
|
70416
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70417
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70418
|
-
}
|
|
70419
|
-
).catch(() => {
|
|
70420
|
-
});
|
|
71309
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71310
|
+
mlsGroup,
|
|
71311
|
+
mlsGroupId: entry.mlsGroupId,
|
|
71312
|
+
label: `shared group ${gid.slice(0, 8)}`,
|
|
71313
|
+
welcomeFields: { conversation_group_id: gid },
|
|
71314
|
+
targets: sharedTargets
|
|
71315
|
+
});
|
|
71316
|
+
if (done > 0) {
|
|
71317
|
+
console.log(`[SecureChannel] Batched Welcome for ${done}/${validReqs.length} devices in shared group ${gid.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
|
|
70421
71318
|
}
|
|
70422
|
-
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70423
|
-
console.log(`[SecureChannel] Batched Welcome for ${validReqs.length} devices in shared group ${gid.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
|
|
70424
71319
|
} catch (batchErr) {
|
|
70425
71320
|
console.warn(`[SecureChannel] Batched Welcome failed for shared group ${gid.slice(0, 8)}, falling back to individual:`, batchErr);
|
|
70426
|
-
for (const
|
|
71321
|
+
for (const target of sharedTargets) {
|
|
70427
71322
|
try {
|
|
70428
|
-
const
|
|
70429
|
-
|
|
70430
|
-
|
|
70431
|
-
|
|
70432
|
-
|
|
70433
|
-
|
|
70434
|
-
}
|
|
70435
|
-
await fetch(`${this.config.apiUrl}/api/v1/mls/groups/${entry.mlsGroupId}/fulfill-welcome`, {
|
|
70436
|
-
method: "POST",
|
|
70437
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70438
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70439
|
-
}).catch(() => {
|
|
71323
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71324
|
+
mlsGroup,
|
|
71325
|
+
mlsGroupId: entry.mlsGroupId,
|
|
71326
|
+
label: `shared group ${gid.slice(0, 8)}`,
|
|
71327
|
+
welcomeFields: { conversation_group_id: gid },
|
|
71328
|
+
targets: [target]
|
|
70440
71329
|
});
|
|
70441
|
-
|
|
70442
|
-
|
|
71330
|
+
if (done > 0) {
|
|
71331
|
+
console.log(`[SecureChannel] Individual Welcome for ${target.deviceId.slice(0, 8)} in shared group ${gid.slice(0, 8)}`);
|
|
71332
|
+
}
|
|
70443
71333
|
} catch (indivErr) {
|
|
70444
|
-
console.warn(`[SecureChannel] Individual Welcome failed for ${
|
|
71334
|
+
console.warn(`[SecureChannel] Individual Welcome failed for ${target.deviceId.slice(0, 8)}:`, indivErr);
|
|
70445
71335
|
}
|
|
70446
71336
|
}
|
|
70447
71337
|
}
|
|
@@ -70473,38 +71363,20 @@ ${messageText}`;
|
|
|
70473
71363
|
if (!kpHex) continue;
|
|
70474
71364
|
const kpBytes = new Uint8Array(Buffer.from(kpHex, "hex"));
|
|
70475
71365
|
const memberKp = MLSGroupManager.deserializeKeyPackage(kpBytes);
|
|
70476
|
-
const
|
|
70477
|
-
|
|
70478
|
-
|
|
70479
|
-
|
|
70480
|
-
|
|
70481
|
-
|
|
70482
|
-
|
|
70483
|
-
|
|
70484
|
-
|
|
70485
|
-
}
|
|
70486
|
-
}
|
|
70487
|
-
if (
|
|
70488
|
-
|
|
70489
|
-
event: "mls_welcome",
|
|
70490
|
-
data: {
|
|
70491
|
-
target_device_id: req.requesting_device_id,
|
|
70492
|
-
group_id: convEntry.mlsGroupId,
|
|
70493
|
-
conversation_id: convId,
|
|
70494
|
-
payload: Buffer.from(welcome).toString("hex")
|
|
70495
|
-
}
|
|
70496
|
-
}));
|
|
71366
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71367
|
+
mlsGroup,
|
|
71368
|
+
mlsGroupId: convEntry.mlsGroupId,
|
|
71369
|
+
label: `conv ${convId.slice(0, 8)}`,
|
|
71370
|
+
welcomeFields: { conversation_id: convId },
|
|
71371
|
+
targets: [{
|
|
71372
|
+
requestId: req.id,
|
|
71373
|
+
deviceId: req.requesting_device_id,
|
|
71374
|
+
keyPackage: memberKp
|
|
71375
|
+
}]
|
|
71376
|
+
});
|
|
71377
|
+
if (done > 0) {
|
|
71378
|
+
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in conv ${convId.slice(0, 8)}`);
|
|
70497
71379
|
}
|
|
70498
|
-
await fetch(
|
|
70499
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${convEntry.mlsGroupId}/fulfill-welcome`,
|
|
70500
|
-
{
|
|
70501
|
-
method: "POST",
|
|
70502
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70503
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70504
|
-
}
|
|
70505
|
-
);
|
|
70506
|
-
await saveMlsState(this.config.dataDir, convEntry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70507
|
-
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in conv ${convId.slice(0, 8)}`);
|
|
70508
71380
|
} catch (fulfillErr) {
|
|
70509
71381
|
console.warn(`[SecureChannel] Conv Welcome fulfill failed for ${req.requesting_device_id.slice(0, 8)}:`, fulfillErr);
|
|
70510
71382
|
}
|
|
@@ -70685,12 +71557,13 @@ ${messageText}`;
|
|
|
70685
71557
|
} catch (distErr) {
|
|
70686
71558
|
console.warn(`[SecureChannel] A2A distribute network error:`, distErr);
|
|
70687
71559
|
}
|
|
70688
|
-
if (!distributed && this.
|
|
70689
|
-
|
|
71560
|
+
if (!distributed && this._wsIsOpen()) {
|
|
71561
|
+
const a2aLabel = `A2A ${chId.slice(0, 8)}`;
|
|
71562
|
+
const commitSent = await this._sendFrameConfirmed({
|
|
70690
71563
|
event: "mls_commit",
|
|
70691
71564
|
data: { group_id: chState.mlsGroupId, epoch: Number(mlsGroup.epoch), payload: commitHex }
|
|
70692
|
-
})
|
|
70693
|
-
this.
|
|
71565
|
+
}, `${a2aLabel} commit`);
|
|
71566
|
+
const welcomeSent = commitSent && await this._sendFrameConfirmed({
|
|
70694
71567
|
event: "mls_welcome",
|
|
70695
71568
|
data: {
|
|
70696
71569
|
target_device_id: req.requesting_device_id,
|
|
@@ -70698,7 +71571,11 @@ ${messageText}`;
|
|
|
70698
71571
|
a2a_channel_id: chId,
|
|
70699
71572
|
payload: welcomeHex
|
|
70700
71573
|
}
|
|
70701
|
-
})
|
|
71574
|
+
}, `${a2aLabel} welcome`);
|
|
71575
|
+
if (!welcomeSent) {
|
|
71576
|
+
console.warn(`[SecureChannel] ${a2aLabel}: Welcome frames were NOT written \u2014 leaving the request pending`);
|
|
71577
|
+
continue;
|
|
71578
|
+
}
|
|
70702
71579
|
try {
|
|
70703
71580
|
await fetch(
|
|
70704
71581
|
`${this.config.apiUrl}/api/v1/mls/groups/${chState.mlsGroupId}/distribute`,
|
|
@@ -70721,16 +71598,15 @@ ${messageText}`;
|
|
|
70721
71598
|
console.warn(`[SecureChannel] Cannot deliver Welcome \u2014 HTTP distribute and WS both unavailable`);
|
|
70722
71599
|
continue;
|
|
70723
71600
|
}
|
|
70724
|
-
await
|
|
70725
|
-
|
|
70726
|
-
|
|
70727
|
-
|
|
70728
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70729
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70730
|
-
}
|
|
71601
|
+
const a2aFulfilled = await this._postFulfilWelcome(
|
|
71602
|
+
chState.mlsGroupId,
|
|
71603
|
+
req.id,
|
|
71604
|
+
`A2A ${chId.slice(0, 8)}`
|
|
70731
71605
|
);
|
|
70732
71606
|
await saveMlsState(this.config.dataDir, chState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70733
|
-
|
|
71607
|
+
if (a2aFulfilled) {
|
|
71608
|
+
console.log(`[SecureChannel] Fulfilled A2A Welcome for ${req.requesting_device_id.slice(0, 8)} in channel ${chId.slice(0, 8)}`);
|
|
71609
|
+
}
|
|
70734
71610
|
} catch (fulfillErr) {
|
|
70735
71611
|
console.log(`[SecureChannel] A2A Welcome fulfill failed: ${fulfillErr instanceof Error ? fulfillErr.message : String(fulfillErr)}`);
|
|
70736
71612
|
}
|
|
@@ -70904,12 +71780,12 @@ ${messageText}`;
|
|
|
70904
71780
|
console.log(`[SecureChannel] MLS sync: re-join needed for group ${groupId?.slice(0, 8)}`);
|
|
70905
71781
|
if (groupId) {
|
|
70906
71782
|
await deleteMlsState(this.config.dataDir, groupId);
|
|
70907
|
-
|
|
70908
|
-
|
|
70909
|
-
|
|
70910
|
-
|
|
70911
|
-
}
|
|
71783
|
+
const target = this._resolveGroupFamily(groupId);
|
|
71784
|
+
if (target) {
|
|
71785
|
+
this._mlsGroups.delete(target.managerKey);
|
|
71786
|
+
console.log(`[SecureChannel] MLS sync rejoin: dropped local state for ${target.label}`);
|
|
70912
71787
|
}
|
|
71788
|
+
void this._requestWelcomeSelfHeal(groupId);
|
|
70913
71789
|
}
|
|
70914
71790
|
} else if (action === "replay_complete") {
|
|
70915
71791
|
console.log(`[SecureChannel] MLS sync complete for group ${groupId?.slice(0, 8)} (${data.count} messages replayed)`);
|
|
@@ -70921,6 +71797,7 @@ ${messageText}`;
|
|
|
70921
71797
|
*/
|
|
70922
71798
|
async _handleA2AMessageMLS(data) {
|
|
70923
71799
|
const groupId = data.group_id;
|
|
71800
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
70924
71801
|
const channelId = data.channel_id ?? data.a2a_channel_id;
|
|
70925
71802
|
let a2aChannelId = channelId;
|
|
70926
71803
|
if (!a2aChannelId) {
|
|
@@ -98144,13 +99021,14 @@ var init_index = __esm2({
|
|
|
98144
99021
|
init_skill_invoker();
|
|
98145
99022
|
await init_skill_telemetry();
|
|
98146
99023
|
await init_policy_enforcer();
|
|
98147
|
-
VERSION = true ? "0.23.
|
|
99024
|
+
VERSION = true ? "0.23.27" : "0.0.0-dev";
|
|
98148
99025
|
}
|
|
98149
99026
|
});
|
|
98150
99027
|
await init_index();
|
|
98151
99028
|
|
|
98152
99029
|
// src/index.ts
|
|
98153
99030
|
init_config();
|
|
99031
|
+
init_pending_invite();
|
|
98154
99032
|
|
|
98155
99033
|
// ../../node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs
|
|
98156
99034
|
import { createRequire as $S } from "node:module";
|
|
@@ -119451,18 +120329,18 @@ import { realpathSync as realpathSync2 } from "node:fs";
|
|
|
119451
120329
|
import { resolve as resolve3, dirname as dirname2, basename, sep as sep2, isAbsolute } from "node:path";
|
|
119452
120330
|
|
|
119453
120331
|
// src/host-trust.ts
|
|
119454
|
-
import { mkdirSync as
|
|
119455
|
-
import { join as
|
|
120332
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2, rmSync as rmSync3, readdirSync as readdirSync2, lstatSync as lstatSync2, readFileSync as readFileSync5, chmodSync as chmodSync3 } from "node:fs";
|
|
120333
|
+
import { join as join7 } from "node:path";
|
|
119456
120334
|
import { homedir, hostname as hostname3 } from "node:os";
|
|
119457
120335
|
var TRUST_SUBDIR = "host-trust";
|
|
119458
|
-
var
|
|
119459
|
-
var
|
|
120336
|
+
var DIR_MODE3 = 448;
|
|
120337
|
+
var FILE_MODE3 = 384;
|
|
119460
120338
|
var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
119461
120339
|
var IDENTITY_FILES = ["agentvault.json", "secure-channel.json", "agentvault.json.bak"];
|
|
119462
120340
|
var HostTrustError = class extends Error {
|
|
119463
120341
|
};
|
|
119464
120342
|
function trustDir(root3) {
|
|
119465
|
-
return
|
|
120343
|
+
return join7(root3 ?? join7(homedir(), ".agentvault"), TRUST_SUBDIR);
|
|
119466
120344
|
}
|
|
119467
120345
|
function sanitize(deviceId) {
|
|
119468
120346
|
if (typeof deviceId !== "string" || !ID_RE.test(deviceId)) {
|
|
@@ -119473,7 +120351,7 @@ function sanitize(deviceId) {
|
|
|
119473
120351
|
function isTrusted(deviceId, root3) {
|
|
119474
120352
|
try {
|
|
119475
120353
|
if (typeof deviceId !== "string" || !ID_RE.test(deviceId)) return false;
|
|
119476
|
-
const p2 =
|
|
120354
|
+
const p2 = join7(trustDir(root3), deviceId);
|
|
119477
120355
|
return lstatSync2(p2).isFile();
|
|
119478
120356
|
} catch {
|
|
119479
120357
|
return false;
|
|
@@ -119482,26 +120360,26 @@ function isTrusted(deviceId, root3) {
|
|
|
119482
120360
|
function grant(deviceId, root3) {
|
|
119483
120361
|
const id = sanitize(deviceId);
|
|
119484
120362
|
const dir = trustDir(root3);
|
|
119485
|
-
const p2 =
|
|
119486
|
-
|
|
119487
|
-
|
|
120363
|
+
const p2 = join7(dir, id);
|
|
120364
|
+
mkdirSync4(dir, { recursive: true, mode: DIR_MODE3 });
|
|
120365
|
+
writeFileSync2(
|
|
119488
120366
|
p2,
|
|
119489
120367
|
`granted_at=${(/* @__PURE__ */ new Date()).toISOString()} host=${hostname3()}
|
|
119490
120368
|
`,
|
|
119491
|
-
{ mode:
|
|
120369
|
+
{ mode: FILE_MODE3 }
|
|
119492
120370
|
);
|
|
119493
|
-
|
|
119494
|
-
|
|
120371
|
+
chmodSync3(dir, DIR_MODE3);
|
|
120372
|
+
chmodSync3(p2, FILE_MODE3);
|
|
119495
120373
|
}
|
|
119496
120374
|
function revoke(deviceId, root3) {
|
|
119497
120375
|
const id = sanitize(deviceId);
|
|
119498
|
-
const p2 =
|
|
120376
|
+
const p2 = join7(trustDir(root3), id);
|
|
119499
120377
|
try {
|
|
119500
120378
|
if (!lstatSync2(p2).isFile()) return false;
|
|
119501
120379
|
} catch {
|
|
119502
120380
|
return false;
|
|
119503
120381
|
}
|
|
119504
|
-
|
|
120382
|
+
rmSync3(p2, { force: true });
|
|
119505
120383
|
return true;
|
|
119506
120384
|
}
|
|
119507
120385
|
function listTrusted(root3) {
|
|
@@ -119516,7 +120394,7 @@ function listTrusted(root3) {
|
|
|
119516
120394
|
function readDeviceId(dataDir) {
|
|
119517
120395
|
for (const f7 of IDENTITY_FILES) {
|
|
119518
120396
|
try {
|
|
119519
|
-
const parsed = JSON.parse(
|
|
120397
|
+
const parsed = JSON.parse(readFileSync5(join7(dataDir, f7), "utf-8"));
|
|
119520
120398
|
if (parsed?.deviceId && ID_RE.test(parsed.deviceId)) return parsed.deviceId;
|
|
119521
120399
|
} catch {
|
|
119522
120400
|
}
|
|
@@ -119526,17 +120404,8 @@ function readDeviceId(dataDir) {
|
|
|
119526
120404
|
);
|
|
119527
120405
|
}
|
|
119528
120406
|
|
|
119529
|
-
// src/log.ts
|
|
119530
|
-
import { format } from "node:util";
|
|
119531
|
-
function stamp(message) {
|
|
119532
|
-
const ts2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
119533
|
-
return message.split("\n").map((line) => `${ts2} ${line}`).join("\n");
|
|
119534
|
-
}
|
|
119535
|
-
function logLine(...args) {
|
|
119536
|
-
console.error(stamp(format(...args)));
|
|
119537
|
-
}
|
|
119538
|
-
|
|
119539
120407
|
// src/worker-permission.ts
|
|
120408
|
+
init_log();
|
|
119540
120409
|
var PATH_FIELDS = ["file_path", "path", "notebook_path"];
|
|
119541
120410
|
function canonical(p2) {
|
|
119542
120411
|
const abs = resolve3(p2);
|
|
@@ -133462,6 +134331,7 @@ function date7(params) {
|
|
|
133462
134331
|
config2(en_default3());
|
|
133463
134332
|
|
|
133464
134333
|
// src/session.ts
|
|
134334
|
+
init_log();
|
|
133465
134335
|
function makeRoomSayTool(onSay) {
|
|
133466
134336
|
return bs(
|
|
133467
134337
|
"say",
|
|
@@ -134072,9 +134942,36 @@ var ArmingState = class {
|
|
|
134072
134942
|
}
|
|
134073
134943
|
};
|
|
134074
134944
|
|
|
134945
|
+
// src/service/self-uninstall.ts
|
|
134946
|
+
init_subcommand();
|
|
134947
|
+
init_backend();
|
|
134948
|
+
init_log();
|
|
134949
|
+
var DEVICE_IS_GONE = "device_revoked";
|
|
134950
|
+
function selfUninstallOnTerminal(reason, env, deps = {}) {
|
|
134951
|
+
if (reason !== DEVICE_IS_GONE) return false;
|
|
134952
|
+
const log = deps.log ?? ((m6) => logLine("[bridge] " + m6));
|
|
134953
|
+
let label = "";
|
|
134954
|
+
try {
|
|
134955
|
+
if (!deps.backend && process.platform !== "darwin" && process.platform !== "linux") return false;
|
|
134956
|
+
const backend = deps.backend ?? selectBackend(process.platform);
|
|
134957
|
+
label = resolveLabel(backend, env);
|
|
134958
|
+
if (!backend.status(label).loaded) return false;
|
|
134959
|
+
backend.uninstall(label);
|
|
134960
|
+
log(
|
|
134961
|
+
`device was revoked \u2014 removed the supervised service ${label} so it does not sit installed on this machine pointing at an agent that no longer exists. Your credentials are still on disk and were NOT touched; re-install with a fresh invite token from AgentVault to reconnect.`
|
|
134962
|
+
);
|
|
134963
|
+
return true;
|
|
134964
|
+
} catch (e7) {
|
|
134965
|
+
log(
|
|
134966
|
+
`device was revoked, but the supervised service ${label || "for this agent"} could not be removed: ${e7.message}. Remove it by hand with: AV_AGENT_NAME=<agent-name> agentvault-claude-bridge uninstall`
|
|
134967
|
+
);
|
|
134968
|
+
return false;
|
|
134969
|
+
}
|
|
134970
|
+
}
|
|
134971
|
+
|
|
134075
134972
|
// src/approve-cli.ts
|
|
134076
|
-
import { mkdirSync as
|
|
134077
|
-
import { join as
|
|
134973
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync4, readFileSync as readFileSync7, readdirSync as readdirSync4, rmSync as rmSync5, existsSync as existsSync5 } from "node:fs";
|
|
134974
|
+
import { join as join11 } from "node:path";
|
|
134078
134975
|
var APPROVALS_SUBDIR = "arm-approvals";
|
|
134079
134976
|
var ID_RE2 = /^[A-Za-z0-9_-]{1,128}$/;
|
|
134080
134977
|
var ApproveArmError = class extends Error {
|
|
@@ -134094,24 +134991,24 @@ function sanitizeRoomId(roomId) {
|
|
|
134094
134991
|
function writeApproval(dataDir, requestId, roomId) {
|
|
134095
134992
|
const id = sanitizeRequestId(requestId);
|
|
134096
134993
|
const room = sanitizeRoomId(roomId);
|
|
134097
|
-
const dir =
|
|
134098
|
-
|
|
134099
|
-
|
|
134994
|
+
const dir = join11(dataDir, APPROVALS_SUBDIR);
|
|
134995
|
+
mkdirSync6(dir, { recursive: true });
|
|
134996
|
+
writeFileSync4(join11(dir, id), room);
|
|
134100
134997
|
}
|
|
134101
134998
|
function drainApprovals(dataDir) {
|
|
134102
|
-
const dir =
|
|
134103
|
-
if (!
|
|
134999
|
+
const dir = join11(dataDir, APPROVALS_SUBDIR);
|
|
135000
|
+
if (!existsSync5(dir)) return [];
|
|
134104
135001
|
const out = [];
|
|
134105
|
-
for (const name of
|
|
135002
|
+
for (const name of readdirSync4(dir)) {
|
|
134106
135003
|
if (!ID_RE2.test(name)) continue;
|
|
134107
135004
|
let roomId = "";
|
|
134108
135005
|
try {
|
|
134109
|
-
roomId =
|
|
135006
|
+
roomId = readFileSync7(join11(dir, name), "utf8").trim();
|
|
134110
135007
|
} catch {
|
|
134111
135008
|
}
|
|
134112
135009
|
out.push({ requestId: name, roomId: ID_RE2.test(roomId) ? roomId : "" });
|
|
134113
135010
|
try {
|
|
134114
|
-
|
|
135011
|
+
rmSync5(join11(dir, name), { force: true, recursive: true });
|
|
134115
135012
|
} catch {
|
|
134116
135013
|
}
|
|
134117
135014
|
}
|
|
@@ -134252,11 +135149,13 @@ var ActiveTarget = class {
|
|
|
134252
135149
|
function attachLifecycle2(channel, opts = {}) {
|
|
134253
135150
|
const log = opts.log ?? (() => {
|
|
134254
135151
|
});
|
|
135152
|
+
const selfUninstall = opts.selfUninstall ?? selfUninstallOnTerminal;
|
|
134255
135153
|
const onTerminal = opts.onTerminal ?? ((_reason, o10) => process.exit(o10.restart ? 1 : 0));
|
|
134256
135154
|
const terminal = (reason, restart) => {
|
|
134257
135155
|
log(
|
|
134258
135156
|
`terminal condition (${reason}) \u2014 ${restart ? "gave up reconnecting; supervisor may respawn" : "this device must re-enroll or another session has taken over"}. Re-run/re-install with a fresh invite token from AgentVault if this persists.`
|
|
134259
135157
|
);
|
|
135158
|
+
selfUninstall(reason, process.env);
|
|
134260
135159
|
onTerminal(reason, { restart });
|
|
134261
135160
|
};
|
|
134262
135161
|
channel.on("auth_failed", (e7) => terminal(e7.reason, false));
|
|
@@ -134479,7 +135378,12 @@ function wireBridge(channel, session, target, opts = {}) {
|
|
|
134479
135378
|
}
|
|
134480
135379
|
|
|
134481
135380
|
// src/index.ts
|
|
135381
|
+
init_log();
|
|
134482
135382
|
async function main() {
|
|
135383
|
+
const { maybeRunDoctorSubcommand: maybeRunDoctorSubcommand2 } = await Promise.resolve().then(() => (init_doctor_cli(), doctor_cli_exports));
|
|
135384
|
+
if (await maybeRunDoctorSubcommand2(process.argv)) {
|
|
135385
|
+
process.exit(process.exitCode ?? 0);
|
|
135386
|
+
}
|
|
134483
135387
|
const { dataDir: dataDirForSubcommand } = resolveDataDir(process.env);
|
|
134484
135388
|
if (maybeRunApproveArmSubcommand(process.argv, dataDirForSubcommand)) {
|
|
134485
135389
|
process.exit(process.exitCode ?? 0);
|
|
@@ -134498,7 +135402,7 @@ async function main() {
|
|
|
134498
135402
|
"[bridge] warning: passing the invite token on the command line is visible to other local users via 'ps'. Prefer: AV_INVITE_TOKEN=\u2026 npx @agentvault/claude-bridge"
|
|
134499
135403
|
);
|
|
134500
135404
|
}
|
|
134501
|
-
logLine(`[bridge] version: ${true ? "0.
|
|
135405
|
+
logLine(`[bridge] version: ${true ? "0.8.1" : "dev"}`);
|
|
134502
135406
|
logLine(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
|
|
134503
135407
|
logLine(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
|
|
134504
135408
|
if (cfg.armRoom) {
|
|
@@ -134530,7 +135434,7 @@ async function main() {
|
|
|
134530
135434
|
// its default would render "@agentvault/agentvault@0.7.x" — the wrong
|
|
134531
135435
|
// package name attached to the bridge's version number, which is worse
|
|
134532
135436
|
// than either alone.
|
|
134533
|
-
clientVersion: `@agentvault/claude-bridge@${true ? "0.
|
|
135437
|
+
clientVersion: `@agentvault/claude-bridge@${true ? "0.8.1" : "dev"}`
|
|
134534
135438
|
});
|
|
134535
135439
|
const agentSystemPrompt = cfg.systemPrompt ?? `You are ${cfg.agentName}, an AI agent on AgentVault. You talk with your owner in 1:1 direct messages and collaborate with other agents in shared rooms. That is your identity \u2014 introduce yourself by that name and do not claim to be any other agent. To speak, call the say tool. In a 1:1 with your owner, reply to what they say. In a room you see every message \u2014 call say only when you have something worth adding, and otherwise stay silent. Keep messages concise.`;
|
|
134536
135440
|
const deviceJwt = () => {
|
|
@@ -134594,9 +135498,9 @@ async function main() {
|
|
|
134594
135498
|
`[worker-trap] ${rec.at} ${rec.outcome} after ${rec.ranMs}ms (waited ${rec.waitedMs}ms behind ${rec.queueDepthAtEnqueue}, ${rec.queueDepthAtStart} still queued) replyExpected=${rec.replyExpected}` + (rec.session ? ` composed=${rec.session.composedChars} result=${rec.session.sawResult} said=${rec.session.said}` : "")
|
|
134595
135499
|
);
|
|
134596
135500
|
try {
|
|
134597
|
-
const dir =
|
|
134598
|
-
|
|
134599
|
-
appendFileSync2(
|
|
135501
|
+
const dir = join13(cfg.dataDir, "logs");
|
|
135502
|
+
mkdirSync7(dir, { recursive: true });
|
|
135503
|
+
appendFileSync2(join13(dir, "worker-incidents.jsonl"), JSON.stringify(rec) + "\n");
|
|
134600
135504
|
} catch (err) {
|
|
134601
135505
|
logLine(`[worker-trap] could not persist incident: ${err.message}`);
|
|
134602
135506
|
}
|
|
@@ -134628,6 +135532,11 @@ async function main() {
|
|
|
134628
135532
|
attachLifecycle2(channel, {
|
|
134629
135533
|
log: (m6) => logLine("[bridge] " + m6)
|
|
134630
135534
|
});
|
|
135535
|
+
channel.on("ready", () => {
|
|
135536
|
+
if (clearPendingInviteIfConsumed(cfg.dataDir, hasPersistedCreds)) {
|
|
135537
|
+
logLine("[bridge] enrolled \u2014 pending invite consumed and removed");
|
|
135538
|
+
}
|
|
135539
|
+
});
|
|
134631
135540
|
channel.on("state", (s10) => logLine(`[bridge] channel state: ${JSON.stringify(s10)}`));
|
|
134632
135541
|
channel.on(
|
|
134633
135542
|
"room_joined",
|