@agentvault/grok-bridge 0.3.2 → 0.4.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 +28 -14
- package/dist/index.js +1095 -387
- 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 +1 -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
|
@@ -54,6 +54,24 @@ var init_pending_invite = __esm({
|
|
|
54
54
|
}
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
// src/log.ts
|
|
58
|
+
import { format } from "node:util";
|
|
59
|
+
function stamp(message) {
|
|
60
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
61
|
+
return message.split("\n").map((line) => `${ts} ${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
|
+
|
|
57
75
|
// src/config.ts
|
|
58
76
|
import { existsSync as existsSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync2, chmodSync as chmodSync2 } from "node:fs";
|
|
59
77
|
import { join as join6, resolve as resolve2, sep, dirname } from "node:path";
|
|
@@ -71,7 +89,7 @@ function assertInstallableEntrypoint(entrypoint) {
|
|
|
71
89
|
if (isEphemeralEntrypoint(entrypoint)) {
|
|
72
90
|
throw new Error(
|
|
73
91
|
`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:
|
|
74
|
-
npm i -g @agentvault/grok-bridge && agentvault-grok-bridge `
|
|
92
|
+
npm i -g @agentvault/grok-bridge && agentvault-grok-bridge install`
|
|
75
93
|
);
|
|
76
94
|
}
|
|
77
95
|
}
|
|
@@ -83,6 +101,17 @@ function hasRecoverableBackup(dataDir) {
|
|
|
83
101
|
return false;
|
|
84
102
|
}
|
|
85
103
|
}
|
|
104
|
+
function readPersistedDeviceId(dataDir) {
|
|
105
|
+
for (const f2 of [...CRED_FILES, BACKUP_FILE]) {
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(readFileSync3(join6(dataDir, f2), "utf-8"));
|
|
108
|
+
const id = parsed?.deviceId;
|
|
109
|
+
if (typeof id === "string" && id) return id;
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
86
115
|
function hasPersistedCreds(dataDir) {
|
|
87
116
|
if (CRED_FILES.some((f2) => existsSync2(join6(dataDir, f2)))) return true;
|
|
88
117
|
return hasRecoverableBackup(dataDir);
|
|
@@ -91,20 +120,41 @@ function slugify2(name) {
|
|
|
91
120
|
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
92
121
|
}
|
|
93
122
|
function resolveDataDir(env) {
|
|
94
|
-
|
|
123
|
+
const supplied = env.AV_AGENT_NAME?.trim();
|
|
124
|
+
const agentName = supplied || LEGACY_DEFAULT_AGENT_NAME;
|
|
125
|
+
const agentNameSource = supplied ? "supplied" : "defaulted";
|
|
126
|
+
if (env.AV_DATA_DIR)
|
|
127
|
+
return { dataDir: env.AV_DATA_DIR, source: "explicit", agentName, agentNameSource };
|
|
95
128
|
const base = join6(env.HOME ?? "", ".agentvault", "grok-room-bridge");
|
|
96
|
-
|
|
129
|
+
const perAgent = join6(base, slugify2(agentName));
|
|
130
|
+
if (!supplied && !hasPersistedCreds(perAgent)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`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}, so there is no existing install to infer a name from. Re-run with AV_AGENT_NAME=<agent-name> set.`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return { dataDir: perAgent, source: "per-agent", agentName, agentNameSource };
|
|
97
136
|
}
|
|
98
137
|
function loadConfig(env, argv = []) {
|
|
99
|
-
const { dataDir, source: dataDirSource } = resolveDataDir(env);
|
|
138
|
+
const { dataDir, source: dataDirSource, agentName, agentNameSource } = resolveDataDir(env);
|
|
139
|
+
if (agentNameSource === "defaulted") {
|
|
140
|
+
logLine(
|
|
141
|
+
`[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.`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
100
144
|
const inviteToken = (argv[0] && !argv[0].startsWith("-") ? argv[0] : "") || env.AV_INVITE_TOKEN || readPendingInvite(dataDir) || "";
|
|
145
|
+
if (argv.includes(REMOVED_INSTALL_FLAG)) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`${REMOVED_INSTALL_FLAG} was removed \u2014 it handed the agent over from a foreground process that could be reaped before the encrypted group finished forming, leaving an agent that was connected and mute (#909). Use the subcommand instead, which lets the service enrol itself:
|
|
148
|
+
npm i -g @agentvault/grok-bridge && AV_AGENT_NAME=<agent-name> AV_INVITE_TOKEN=<token> agentvault-grok-bridge install`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
101
151
|
const unknownFlags = argv.filter((a2) => a2.startsWith("-") && !KNOWN_FLAGS.has(a2));
|
|
102
152
|
if (unknownFlags.length) {
|
|
103
153
|
throw new Error(
|
|
104
154
|
`unknown option(s): ${unknownFlags.join(", ")} \u2014 supported flags are ${[...KNOWN_FLAGS].join(", ")}`
|
|
105
155
|
);
|
|
106
156
|
}
|
|
107
|
-
const workspaceDir = env.AV_WORKSPACE_DIR || join6(dirname(dataDir), "workspaces", slugify2(
|
|
157
|
+
const workspaceDir = env.AV_WORKSPACE_DIR || join6(dirname(dataDir), "workspaces", slugify2(agentName));
|
|
108
158
|
const wsReal = resolve2(workspaceDir);
|
|
109
159
|
const ddReal = resolve2(dataDir);
|
|
110
160
|
if (wsReal === ddReal || wsReal.startsWith(ddReal + sep) || ddReal.startsWith(wsReal + sep)) {
|
|
@@ -129,17 +179,14 @@ function loadConfig(env, argv = []) {
|
|
|
129
179
|
inviteToken,
|
|
130
180
|
dataDir,
|
|
131
181
|
dataDirSource,
|
|
132
|
-
// One-paste install (--install): enrol, hand off to the supervisor, exit.
|
|
133
|
-
// Default false keeps a bare `npx @agentvault/grok-bridge` run identical.
|
|
134
|
-
installAfterEnroll: argv.includes(INSTALL_FLAG),
|
|
135
182
|
...parseInfoFlags(argv),
|
|
136
183
|
apiUrl: env.AV_API_URL ?? "https://api.agentvault.chat",
|
|
137
184
|
// Identity shown to the agent's own session. Set this to the agent's
|
|
138
185
|
// AgentVault name via AV_AGENT_NAME (the native connect command passes it).
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
186
|
+
// Resolved in resolveDataDir so the name, the data dir and the workspace dir
|
|
187
|
+
// can never disagree about who this agent is (#921/#927).
|
|
188
|
+
agentName,
|
|
189
|
+
agentNameSource,
|
|
143
190
|
roomFilter: env.AV_ROOM_ID || void 0,
|
|
144
191
|
workspaceDir,
|
|
145
192
|
grokPath: env.AV_GROK_PATH || void 0,
|
|
@@ -149,35 +196,19 @@ function loadConfig(env, argv = []) {
|
|
|
149
196
|
systemPrompt: env.AV_SYSTEM_PROMPT || void 0
|
|
150
197
|
};
|
|
151
198
|
}
|
|
152
|
-
var
|
|
199
|
+
var REMOVED_INSTALL_FLAG, VERSION_FLAGS, HELP_FLAGS, KNOWN_FLAGS, CRED_FILES, BACKUP_FILE, LEGACY_DEFAULT_AGENT_NAME;
|
|
153
200
|
var init_config = __esm({
|
|
154
201
|
"src/config.ts"() {
|
|
155
202
|
"use strict";
|
|
156
203
|
init_pending_invite();
|
|
157
|
-
|
|
204
|
+
init_log();
|
|
205
|
+
REMOVED_INSTALL_FLAG = "--install";
|
|
158
206
|
VERSION_FLAGS = ["--version", "-v"];
|
|
159
207
|
HELP_FLAGS = ["--help", "-h"];
|
|
160
|
-
KNOWN_FLAGS = /* @__PURE__ */ new Set([
|
|
208
|
+
KNOWN_FLAGS = /* @__PURE__ */ new Set([...VERSION_FLAGS, ...HELP_FLAGS]);
|
|
161
209
|
CRED_FILES = ["agentvault.json", "secure-channel.json"];
|
|
162
210
|
BACKUP_FILE = "agentvault.json.bak";
|
|
163
|
-
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// src/log.ts
|
|
167
|
-
import { format } from "node:util";
|
|
168
|
-
function stamp(message) {
|
|
169
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
170
|
-
return message.split("\n").map((line) => `${ts} ${line}`).join("\n");
|
|
171
|
-
}
|
|
172
|
-
function logLine(...args) {
|
|
173
|
-
console.error(stamp(format(...args)));
|
|
174
|
-
}
|
|
175
|
-
function printLine(text) {
|
|
176
|
-
console.log(text);
|
|
177
|
-
}
|
|
178
|
-
var init_log = __esm({
|
|
179
|
-
"src/log.ts"() {
|
|
180
|
-
"use strict";
|
|
211
|
+
LEGACY_DEFAULT_AGENT_NAME = "Grok";
|
|
181
212
|
}
|
|
182
213
|
});
|
|
183
214
|
|
|
@@ -211,16 +242,6 @@ function writeLauncher(deps, spec) {
|
|
|
211
242
|
deps.writeFile(spec.launcherPath, spec.launcherScript);
|
|
212
243
|
deps.chmod(spec.launcherPath, 493);
|
|
213
244
|
}
|
|
214
|
-
function parseLauncherEntrypoint(script) {
|
|
215
|
-
if (!script) return void 0;
|
|
216
|
-
const m2 = /^if n=\$\(command -v .+\); then exec "\$n" (.+) "\$@"; fi$/m.exec(script);
|
|
217
|
-
if (!m2) return void 0;
|
|
218
|
-
return unSq(m2[1]);
|
|
219
|
-
}
|
|
220
|
-
function unSq(quoted) {
|
|
221
|
-
if (quoted.length < 2 || !quoted.startsWith("'") || !quoted.endsWith("'")) return void 0;
|
|
222
|
-
return quoted.slice(1, -1).replace(/'\\''/g, "'");
|
|
223
|
-
}
|
|
224
245
|
var FALLBACK_NODES;
|
|
225
246
|
var init_launcher = __esm({
|
|
226
247
|
"src/service/launcher.ts"() {
|
|
@@ -314,6 +335,37 @@ ${envEntries}
|
|
|
314
335
|
</plist>
|
|
315
336
|
`;
|
|
316
337
|
}
|
|
338
|
+
function parsePlistEnv(xml) {
|
|
339
|
+
const out = {};
|
|
340
|
+
const keyIdx = xml.indexOf("<key>EnvironmentVariables</key>");
|
|
341
|
+
if (keyIdx < 0) return out;
|
|
342
|
+
const open = xml.indexOf("<dict>", keyIdx);
|
|
343
|
+
if (open < 0) return out;
|
|
344
|
+
let depth = 0;
|
|
345
|
+
let i2 = open;
|
|
346
|
+
let close = -1;
|
|
347
|
+
const tag = /<(\/?)dict>/g;
|
|
348
|
+
tag.lastIndex = open;
|
|
349
|
+
let m2;
|
|
350
|
+
while (m2 = tag.exec(xml)) {
|
|
351
|
+
depth += m2[1] ? -1 : 1;
|
|
352
|
+
if (depth === 0) {
|
|
353
|
+
close = m2.index;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
i2 = m2.index;
|
|
357
|
+
}
|
|
358
|
+
void i2;
|
|
359
|
+
if (close < 0) return out;
|
|
360
|
+
const body = xml.slice(open, close);
|
|
361
|
+
const pair = /<key>([\s\S]*?)<\/key>\s*<string>([\s\S]*?)<\/string>/g;
|
|
362
|
+
let p2;
|
|
363
|
+
while (p2 = pair.exec(body)) out[unesc(p2[1])] = unesc(p2[2]);
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
function unesc(s2) {
|
|
367
|
+
return s2.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
368
|
+
}
|
|
317
369
|
var LaunchdBackend;
|
|
318
370
|
var init_launchd = __esm({
|
|
319
371
|
"src/service/launchd.ts"() {
|
|
@@ -368,6 +420,26 @@ var init_launchd = __esm({
|
|
|
368
420
|
const r2 = this.deps.exec("launchctl", ["print", `${this.domain()}/${label}`]);
|
|
369
421
|
return { loaded: r2.code === 0, detail: r2.stdout };
|
|
370
422
|
}
|
|
423
|
+
/** #913 item 3 — every installed service whose label starts with `prefix`.
|
|
424
|
+
*
|
|
425
|
+
* Reads the plist directory rather than `launchctl list`, because an ORPHAN
|
|
426
|
+
* is typically installed-but-not-loaded: the revoked device exits 0 and
|
|
427
|
+
* KeepAlive leaves it down. `launchctl list` would not show the very thing
|
|
428
|
+
* doctor exists to find. */
|
|
429
|
+
list(prefix) {
|
|
430
|
+
const dir = join11(this.deps.home, "Library", "LaunchAgents");
|
|
431
|
+
const out = [];
|
|
432
|
+
for (const name of this.deps.readdir(dir)) {
|
|
433
|
+
if (!name.startsWith(prefix) || !name.endsWith(".plist")) continue;
|
|
434
|
+
const path2 = join11(dir, name);
|
|
435
|
+
out.push({
|
|
436
|
+
label: name.slice(0, -".plist".length),
|
|
437
|
+
path: path2,
|
|
438
|
+
env: parsePlistEnv(this.deps.readFile(path2))
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
return out.sort((a2, b2) => a2.label.localeCompare(b2.label));
|
|
442
|
+
}
|
|
371
443
|
};
|
|
372
444
|
}
|
|
373
445
|
});
|
|
@@ -399,6 +471,24 @@ StandardError=append:${spec.logErr}
|
|
|
399
471
|
WantedBy=default.target
|
|
400
472
|
`;
|
|
401
473
|
}
|
|
474
|
+
function parseUnitEnv(text) {
|
|
475
|
+
const out = {};
|
|
476
|
+
for (const line of text.split("\n")) {
|
|
477
|
+
const m2 = /^Environment="([\s\S]*)"\s*$/.exec(line.trim());
|
|
478
|
+
if (!m2) continue;
|
|
479
|
+
const eq = unq(m2[1]);
|
|
480
|
+
const at = eq.indexOf("=");
|
|
481
|
+
if (at <= 0) continue;
|
|
482
|
+
out[eq.slice(0, at)] = eq.slice(at + 1);
|
|
483
|
+
}
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
function unq(s2) {
|
|
487
|
+
return s2.replace(/%%/g, "%").replace(
|
|
488
|
+
/\\([\s\S])/g,
|
|
489
|
+
(_2, c2) => c2 === "n" ? "\n" : c2 === "r" ? "\r" : c2 === "t" ? " " : c2
|
|
490
|
+
);
|
|
491
|
+
}
|
|
402
492
|
var SystemdBackend;
|
|
403
493
|
var init_systemd = __esm({
|
|
404
494
|
"src/service/systemd.ts"() {
|
|
@@ -439,18 +529,33 @@ var init_systemd = __esm({
|
|
|
439
529
|
const active = this.deps.exec("systemctl", ["--user", "is-active", `${label}.service`]);
|
|
440
530
|
return { loaded: enabled.code === 0, detail: `${enabled.stdout.trim() || "?"} / ${active.stdout.trim() || "?"}` };
|
|
441
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 = join12(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 = join12(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, b2) => a2.label.localeCompare(b2.label));
|
|
551
|
+
}
|
|
442
552
|
};
|
|
443
553
|
}
|
|
444
554
|
});
|
|
445
555
|
|
|
446
556
|
// src/service/backend.ts
|
|
447
|
-
var backend_exports = {};
|
|
448
|
-
__export(backend_exports, {
|
|
449
|
-
defaultDeps: () => defaultDeps,
|
|
450
|
-
selectBackend: () => selectBackend
|
|
451
|
-
});
|
|
452
557
|
import { spawnSync } from "node:child_process";
|
|
453
|
-
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, rmSync as rmSync2, existsSync as existsSync4, chmodSync as chmodSync5 } from "node:fs";
|
|
558
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, rmSync as rmSync2, existsSync as existsSync4, chmodSync as chmodSync5, readdirSync, readFileSync as readFileSync4 } from "node:fs";
|
|
454
559
|
import { userInfo } from "node:os";
|
|
455
560
|
function defaultDeps() {
|
|
456
561
|
return {
|
|
@@ -465,7 +570,21 @@ function defaultDeps() {
|
|
|
465
570
|
mkdir: (p2) => mkdirSync5(p2, { recursive: true }),
|
|
466
571
|
chmod: (p2, m2) => chmodSync5(p2, m2),
|
|
467
572
|
rm: (p2) => rmSync2(p2, { force: true }),
|
|
468
|
-
exists: (p2) => existsSync4(p2)
|
|
573
|
+
exists: (p2) => existsSync4(p2),
|
|
574
|
+
readdir: (p2) => {
|
|
575
|
+
try {
|
|
576
|
+
return readdirSync(p2);
|
|
577
|
+
} catch {
|
|
578
|
+
return [];
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
readFile: (p2) => {
|
|
582
|
+
try {
|
|
583
|
+
return readFileSync4(p2, "utf8");
|
|
584
|
+
} catch {
|
|
585
|
+
return "";
|
|
586
|
+
}
|
|
587
|
+
}
|
|
469
588
|
};
|
|
470
589
|
}
|
|
471
590
|
function selectBackend(platform, deps) {
|
|
@@ -488,6 +607,7 @@ var init_backend = __esm({
|
|
|
488
607
|
// src/service/subcommand.ts
|
|
489
608
|
var subcommand_exports = {};
|
|
490
609
|
__export(subcommand_exports, {
|
|
610
|
+
labelFor: () => labelFor,
|
|
491
611
|
maybeRunServiceSubcommand: () => maybeRunServiceSubcommand
|
|
492
612
|
});
|
|
493
613
|
function requireAgentName(env) {
|
|
@@ -510,7 +630,9 @@ function maybeRunServiceSubcommand(argv, env, deps) {
|
|
|
510
630
|
const looksLikeFlag = cmd.startsWith("-");
|
|
511
631
|
if (!looksLikeToken && !looksLikeFlag) {
|
|
512
632
|
throw new Error(
|
|
513
|
-
|
|
633
|
+
// "doctor" is dispatched earlier, in index.ts, so it never reaches here.
|
|
634
|
+
// Named anyway: a customer who typed `doctr` should see it offered.
|
|
635
|
+
`unknown subcommand: ${cmd} \u2014 expected one of ${[...SERVICE_CMDS, "doctor"].join(", ")}, an invite token, or no argument at all`
|
|
514
636
|
);
|
|
515
637
|
}
|
|
516
638
|
return false;
|
|
@@ -521,6 +643,7 @@ function maybeRunServiceSubcommand(argv, env, deps) {
|
|
|
521
643
|
try {
|
|
522
644
|
if (cmd === "install") {
|
|
523
645
|
requireAgentName(env);
|
|
646
|
+
assertInstallableEntrypoint(entrypoint);
|
|
524
647
|
const cfg = loadConfig(env, []);
|
|
525
648
|
const firstBoot = !hasPersistedCreds(cfg.dataDir) && !!cfg.inviteToken;
|
|
526
649
|
if (firstBoot) {
|
|
@@ -578,96 +701,313 @@ var init_subcommand = __esm({
|
|
|
578
701
|
}
|
|
579
702
|
});
|
|
580
703
|
|
|
581
|
-
// src/service/
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
704
|
+
// src/service/doctor.ts
|
|
705
|
+
import { basename } from "node:path";
|
|
706
|
+
async function buildDoctorReport(d2) {
|
|
707
|
+
const installed = d2.labelPrefixes.flatMap((p2) => d2.backend.list(p2));
|
|
708
|
+
const services = [];
|
|
709
|
+
for (const s2 of installed) services.push(await inspect(s2, d2));
|
|
710
|
+
const claimed = new Set(installed.map((s2) => s2.env.AV_DATA_DIR).filter(Boolean));
|
|
711
|
+
const unsupervised = [];
|
|
712
|
+
for (const dataDir of d2.credDirs()) {
|
|
713
|
+
if (claimed.has(dataDir)) continue;
|
|
714
|
+
unsupervised.push(await inspectUnsupervised(dataDir, d2));
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
labelPrefixes: d2.labelPrefixes,
|
|
718
|
+
services,
|
|
719
|
+
unsupervised,
|
|
720
|
+
notChecked: d2.otherLabels()
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
async function inspect(s2, d2) {
|
|
724
|
+
const agentName = s2.env.AV_AGENT_NAME ?? "";
|
|
725
|
+
const dataDir = s2.env.AV_DATA_DIR ?? "";
|
|
726
|
+
let loaded = false;
|
|
727
|
+
try {
|
|
728
|
+
loaded = d2.backend.status(s2.label).loaded;
|
|
729
|
+
} catch {
|
|
730
|
+
}
|
|
731
|
+
const base = { label: s2.label, agentName, dataDir, loaded, deviceId: "" };
|
|
732
|
+
if (!dataDir) {
|
|
733
|
+
return {
|
|
734
|
+
...base,
|
|
735
|
+
verdict: "WARN",
|
|
736
|
+
detail: "the service definition bakes no AV_DATA_DIR, so doctor cannot find its credentials and cannot check its device"
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
const creds = d2.readCreds(dataDir);
|
|
740
|
+
if (!creds?.deviceId) {
|
|
741
|
+
return {
|
|
742
|
+
...base,
|
|
743
|
+
verdict: "WARN",
|
|
744
|
+
detail: `no credentials at ${dataDir} \u2014 installed, but this agent never enrolled`
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
const deviceId = creds.deviceId;
|
|
748
|
+
let probe;
|
|
749
|
+
try {
|
|
750
|
+
probe = await d2.fetchStatus(d2.apiUrl, deviceId);
|
|
751
|
+
} catch (e) {
|
|
752
|
+
return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${e.message}` };
|
|
753
|
+
}
|
|
754
|
+
if (probe.inconclusive) {
|
|
755
|
+
return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${probe.inconclusive}` };
|
|
756
|
+
}
|
|
757
|
+
if (probe.gone) {
|
|
758
|
+
return {
|
|
759
|
+
...base,
|
|
760
|
+
deviceId,
|
|
761
|
+
verdict: "FAIL",
|
|
762
|
+
detail: "the device no longer exists \u2014 this service is an orphan",
|
|
763
|
+
fix: d2.uninstallCommand(agentName)
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
switch (probe.status) {
|
|
767
|
+
case "ACTIVE":
|
|
768
|
+
return { ...base, deviceId, verdict: "PASS", detail: "device ACTIVE" };
|
|
769
|
+
case "PENDING":
|
|
770
|
+
return {
|
|
771
|
+
...base,
|
|
772
|
+
deviceId,
|
|
773
|
+
verdict: "WARN",
|
|
774
|
+
detail: "device PENDING \u2014 enrolled, but never approved in AgentVault"
|
|
775
|
+
};
|
|
776
|
+
case "REVOKED":
|
|
777
|
+
return {
|
|
778
|
+
...base,
|
|
779
|
+
deviceId,
|
|
780
|
+
verdict: "FAIL",
|
|
781
|
+
detail: "the device was REVOKED \u2014 this service is an orphan",
|
|
782
|
+
fix: d2.uninstallCommand(agentName)
|
|
783
|
+
};
|
|
784
|
+
default:
|
|
785
|
+
return {
|
|
786
|
+
...base,
|
|
787
|
+
deviceId,
|
|
788
|
+
verdict: "SKIP",
|
|
789
|
+
detail: `the server reported a status this build does not recognise (${probe.status || "none"})`
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
async function inspectUnsupervised(dataDir, d2) {
|
|
794
|
+
const agentName = basename(dataDir);
|
|
795
|
+
const base = { dataDir, agentName, deviceId: "" };
|
|
796
|
+
const deviceId = d2.readCreds(dataDir)?.deviceId;
|
|
797
|
+
if (!deviceId) {
|
|
798
|
+
return { ...base, verdict: "WARN", detail: "credentials present but they hold no device id" };
|
|
799
|
+
}
|
|
800
|
+
let probe;
|
|
801
|
+
try {
|
|
802
|
+
probe = await d2.fetchStatus(d2.apiUrl, deviceId);
|
|
803
|
+
} catch (e) {
|
|
804
|
+
return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${e.message}` };
|
|
805
|
+
}
|
|
806
|
+
if (probe.inconclusive) {
|
|
807
|
+
return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${probe.inconclusive}` };
|
|
808
|
+
}
|
|
809
|
+
if (probe.gone) {
|
|
810
|
+
return {
|
|
811
|
+
...base,
|
|
812
|
+
deviceId,
|
|
813
|
+
verdict: "WARN",
|
|
814
|
+
detail: "the device no longer exists \u2014 these are leftover credentials for a deleted agent"
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
if (probe.status === "ACTIVE") {
|
|
818
|
+
return {
|
|
819
|
+
...base,
|
|
820
|
+
deviceId,
|
|
821
|
+
verdict: "WARN",
|
|
822
|
+
detail: "the device is ACTIVE but nothing supervises it \u2014 this agent is not running",
|
|
823
|
+
fix: d2.installCommand(agentName)
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
if (probe.status === "PENDING") {
|
|
827
|
+
return {
|
|
828
|
+
...base,
|
|
829
|
+
deviceId,
|
|
830
|
+
verdict: "WARN",
|
|
831
|
+
detail: "device PENDING \u2014 enrolled, but never approved in AgentVault"
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
if (probe.status === "REVOKED") {
|
|
835
|
+
return {
|
|
836
|
+
...base,
|
|
837
|
+
deviceId,
|
|
838
|
+
verdict: "WARN",
|
|
839
|
+
detail: "the device was REVOKED \u2014 these are leftover credentials"
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
...base,
|
|
844
|
+
deviceId,
|
|
845
|
+
verdict: "SKIP",
|
|
846
|
+
detail: `the server reported a status this build does not recognise (${probe.status || "none"})`
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
function formatDoctorReport(r2) {
|
|
850
|
+
const out = [];
|
|
851
|
+
out.push(`Services installed for this bridge (${r2.labelPrefixes.map((p2) => p2 + "*").join(", ")})`);
|
|
852
|
+
if (r2.services.length === 0) {
|
|
853
|
+
out.push(` No services installed for this bridge on this machine.`);
|
|
854
|
+
}
|
|
855
|
+
for (const s2 of r2.services) {
|
|
856
|
+
out.push(
|
|
857
|
+
` ${s2.verdict.padEnd(PAD)}${s2.label} [${s2.agentName || "unnamed"}] ${s2.loaded ? "loaded" : "not loaded"} \u2014 ${s2.detail}`
|
|
858
|
+
);
|
|
859
|
+
if (s2.fix) out.push(` ${"".padEnd(PAD)}fix: ${s2.fix}`);
|
|
860
|
+
}
|
|
861
|
+
if (r2.unsupervised.length) {
|
|
862
|
+
out.push("");
|
|
863
|
+
out.push("Credentials on disk that no installed service points at");
|
|
864
|
+
for (const u2 of r2.unsupervised) {
|
|
865
|
+
out.push(` ${u2.verdict.padEnd(PAD)}${u2.agentName} \u2014 ${u2.detail}`);
|
|
866
|
+
out.push(` ${"".padEnd(PAD)}${u2.dataDir}`);
|
|
867
|
+
if (u2.fix) out.push(` ${"".padEnd(PAD)}fix: ${u2.fix}`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (r2.notChecked.length) {
|
|
871
|
+
out.push("");
|
|
872
|
+
out.push("Found on this machine, NOT checked by this command");
|
|
873
|
+
for (const l2 of r2.notChecked) out.push(` ${l2}`);
|
|
874
|
+
out.push(
|
|
875
|
+
" These belong to other AgentVault agent families. This command can only read the credentials of its own, so it says nothing about their health."
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
const counts = { PASS: 0, WARN: 0, FAIL: 0, SKIP: 0 };
|
|
879
|
+
for (const s2 of r2.services) counts[s2.verdict]++;
|
|
880
|
+
for (const u2 of r2.unsupervised) counts[u2.verdict]++;
|
|
881
|
+
out.push("");
|
|
882
|
+
out.push(
|
|
883
|
+
`Summary: ${counts.PASS} PASS, ${counts.WARN} WARN, ${counts.FAIL} FAIL, ${counts.SKIP} SKIP` + (counts.SKIP ? " (SKIP = could not check, NOT a problem found)" : "")
|
|
884
|
+
);
|
|
885
|
+
if (counts.FAIL) {
|
|
886
|
+
out.push(
|
|
887
|
+
"`uninstall` removes the SERVICE only \u2014 your credentials stay on disk, so a mistake here costs a re-install, not an identity."
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
return out;
|
|
891
|
+
}
|
|
892
|
+
var PAD;
|
|
893
|
+
var init_doctor = __esm({
|
|
894
|
+
"src/service/doctor.ts"() {
|
|
895
|
+
"use strict";
|
|
896
|
+
PAD = 6;
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// src/service/doctor-cli.ts
|
|
901
|
+
var doctor_cli_exports = {};
|
|
902
|
+
__export(doctor_cli_exports, {
|
|
903
|
+
fetchDeviceStatus: () => fetchDeviceStatus,
|
|
904
|
+
maybeRunDoctorSubcommand: () => maybeRunDoctorSubcommand,
|
|
905
|
+
realDoctorDeps: () => realDoctorDeps
|
|
585
906
|
});
|
|
586
|
-
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
|
|
587
907
|
import { join as join13 } from "node:path";
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
let fired = false;
|
|
593
|
-
channel.on("ready", () => {
|
|
594
|
-
if (fired) return;
|
|
595
|
-
fired = true;
|
|
596
|
-
void (async () => {
|
|
597
|
-
try {
|
|
598
|
-
if (!hasCreds(cfg.dataDir)) {
|
|
599
|
-
throw new Error(
|
|
600
|
-
`enrolment did not persist credentials to ${cfg.dataDir} \u2014 refusing to install a service that would crash-loop`
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
if (!fenceAuthExists(cfg.fenceHome)) {
|
|
604
|
-
throw new Error(
|
|
605
|
-
"no grok credential in the fence \u2014 run `grok login`, then paste the same command again (your enrolment is already saved, so the token is not needed). Note that XAI_API_KEY from your shell is NOT inherited by the background service."
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
if (deps.waitForIdle) {
|
|
609
|
-
const timeoutMs = deps.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
610
|
-
let timer;
|
|
611
|
-
const timedOut = await Promise.race([
|
|
612
|
-
deps.waitForIdle().then(() => false),
|
|
613
|
-
new Promise((r2) => {
|
|
614
|
-
timer = setTimeout(() => r2(true), timeoutMs);
|
|
615
|
-
})
|
|
616
|
-
]);
|
|
617
|
-
if (timer) clearTimeout(timer);
|
|
618
|
-
if (timedOut) {
|
|
619
|
-
deps.log(
|
|
620
|
-
`agent still busy after ${timeoutMs}ms \u2014 handing off anyway; that reply may be lost`
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
const reaped = await deps.reap();
|
|
625
|
-
if (!reaped.ok) {
|
|
626
|
-
throw new Error(
|
|
627
|
-
`teardown did not complete (${reaped.failures.join("; ")}) \u2014 refusing to install over a session that may still be open`
|
|
628
|
-
);
|
|
629
|
-
}
|
|
630
|
-
const backend = deps.backend ?? selectBackend(process.platform);
|
|
631
|
-
const spec = buildServiceSpec(cfg, {
|
|
632
|
-
entrypoint: deps.entrypoint,
|
|
633
|
-
home: deps.env.HOME ?? "",
|
|
634
|
-
path: deps.env.PATH ?? ""
|
|
635
|
-
});
|
|
636
|
-
const installedEntrypoint = parseLauncherEntrypoint(readLauncher(spec.launcherPath));
|
|
637
|
-
const entrypointIsCurrent = installedEntrypoint !== void 0 && !isEphemeralEntrypoint(installedEntrypoint) && installedEntrypoint === deps.entrypoint;
|
|
638
|
-
if (backend.status(spec.label).loaded && entrypointIsCurrent) {
|
|
639
|
-
backend.restart(spec.label);
|
|
640
|
-
deps.log(`service ${spec.label} already installed and current \u2014 restarted`);
|
|
641
|
-
} else {
|
|
642
|
-
backend.install(spec);
|
|
643
|
-
deps.log(
|
|
644
|
-
installedEntrypoint && !entrypointIsCurrent ? `service ${spec.label} pointed at a stale entrypoint (${installedEntrypoint}) \u2014 re-installed` : `installed service ${spec.label}`
|
|
645
|
-
);
|
|
646
|
-
}
|
|
647
|
-
const after = backend.status(spec.label);
|
|
648
|
-
if (!after.loaded) {
|
|
649
|
-
throw new Error(`service did not load: ${after.detail}`);
|
|
650
|
-
}
|
|
651
|
-
deps.log(`it is running now. logs: ${spec.logErr}`);
|
|
652
|
-
deps.log(`check it: agentvault-grok-bridge status`);
|
|
653
|
-
deps.log(`restart it: agentvault-grok-bridge restart (never 'install')`);
|
|
654
|
-
deps.exit(0);
|
|
655
|
-
} catch (e) {
|
|
656
|
-
deps.log(`--install failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
657
|
-
deps.exit(1);
|
|
658
|
-
}
|
|
659
|
-
})();
|
|
908
|
+
import { readdirSync as readdirSync2 } from "node:fs";
|
|
909
|
+
async function fetchDeviceStatus(apiUrl, deviceId, doFetch = fetch) {
|
|
910
|
+
const res = await doFetch(`${apiUrl}/api/v1/devices/${deviceId}/status`, {
|
|
911
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
|
660
912
|
});
|
|
913
|
+
if (res.status === 404) {
|
|
914
|
+
try {
|
|
915
|
+
await res.json();
|
|
916
|
+
} catch {
|
|
917
|
+
return {
|
|
918
|
+
inconclusive: `${apiUrl} answered 404 with a non-JSON body \u2014 check AV_API_URL points at the API`
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
return { gone: true };
|
|
922
|
+
}
|
|
923
|
+
if (res.status === 429) return { inconclusive: "rate limited by the API (429)" };
|
|
924
|
+
if (!res.ok) return { inconclusive: `the API answered ${res.status}` };
|
|
925
|
+
let body;
|
|
926
|
+
try {
|
|
927
|
+
body = await res.json();
|
|
928
|
+
} catch {
|
|
929
|
+
return { inconclusive: `${apiUrl} answered 200 with a non-JSON body \u2014 check AV_API_URL` };
|
|
930
|
+
}
|
|
931
|
+
return { status: body?.status };
|
|
932
|
+
}
|
|
933
|
+
function listCredDirs(base) {
|
|
934
|
+
const out = [];
|
|
935
|
+
if (hasPersistedCreds(base)) out.push(base);
|
|
936
|
+
let names;
|
|
937
|
+
try {
|
|
938
|
+
names = readdirSync2(base);
|
|
939
|
+
} catch {
|
|
940
|
+
return out;
|
|
941
|
+
}
|
|
942
|
+
for (const name of names) {
|
|
943
|
+
const dir = join13(base, name);
|
|
944
|
+
if (hasPersistedCreds(dir)) out.push(dir);
|
|
945
|
+
}
|
|
946
|
+
return out;
|
|
947
|
+
}
|
|
948
|
+
function otherFamilyLabels(backend) {
|
|
949
|
+
const out = [];
|
|
950
|
+
for (const p2 of OTHER_FAMILY_PREFIXES) {
|
|
951
|
+
try {
|
|
952
|
+
for (const s2 of backend.list(p2)) out.push(s2.label);
|
|
953
|
+
} catch {
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return out.sort();
|
|
957
|
+
}
|
|
958
|
+
function realDoctorDeps(env) {
|
|
959
|
+
const backend = selectBackend(process.platform);
|
|
960
|
+
const base = join13(env.HOME ?? "", ".agentvault", "grok-room-bridge");
|
|
961
|
+
return {
|
|
962
|
+
backend,
|
|
963
|
+
labelPrefixes: [LABEL_PREFIX],
|
|
964
|
+
base,
|
|
965
|
+
// Same default as config.ts. Read from the env directly rather than through
|
|
966
|
+
// loadConfig, which now REFUSES without AV_AGENT_NAME (#921) — and doctor's
|
|
967
|
+
// whole job is to run when you do not know which agents exist.
|
|
968
|
+
apiUrl: env.AV_API_URL ?? "https://api.agentvault.chat",
|
|
969
|
+
uninstallCommand: (n2) => `AV_AGENT_NAME=${n2 || "<agent-name>"} agentvault-grok-bridge uninstall`,
|
|
970
|
+
installCommand: (n2) => `AV_AGENT_NAME=${n2 || "<agent-name>"} agentvault-grok-bridge install`,
|
|
971
|
+
readCreds: (dir) => {
|
|
972
|
+
const deviceId = readPersistedDeviceId(dir);
|
|
973
|
+
return deviceId ? { deviceId } : null;
|
|
974
|
+
},
|
|
975
|
+
credDirs: () => listCredDirs(base),
|
|
976
|
+
fetchStatus: (apiUrl, deviceId) => fetchDeviceStatus(apiUrl, deviceId),
|
|
977
|
+
otherLabels: () => otherFamilyLabels(backend)
|
|
978
|
+
};
|
|
661
979
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
980
|
+
async function maybeRunDoctorSubcommand(argv, opts = {}) {
|
|
981
|
+
if (argv[2] !== "doctor") return false;
|
|
982
|
+
const log = opts.log ?? printLine;
|
|
983
|
+
try {
|
|
984
|
+
const report = await buildDoctorReport(opts.deps ?? realDoctorDeps(opts.env ?? process.env));
|
|
985
|
+
for (const line of formatDoctorReport(report)) log(line);
|
|
986
|
+
if (report.services.some((s2) => s2.verdict === "FAIL")) process.exitCode = 1;
|
|
987
|
+
} catch (e) {
|
|
988
|
+
log(`doctor could not run: ${e.message}`);
|
|
989
|
+
process.exitCode = 1;
|
|
990
|
+
}
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
var LABEL_PREFIX, OTHER_FAMILY_PREFIXES, PROBE_TIMEOUT_MS;
|
|
994
|
+
var init_doctor_cli = __esm({
|
|
995
|
+
"src/service/doctor-cli.ts"() {
|
|
665
996
|
"use strict";
|
|
666
|
-
init_config();
|
|
667
|
-
init_spec();
|
|
668
|
-
init_launcher();
|
|
669
997
|
init_backend();
|
|
670
|
-
|
|
998
|
+
init_config();
|
|
999
|
+
init_log();
|
|
1000
|
+
init_doctor();
|
|
1001
|
+
LABEL_PREFIX = "dev.agentvault.grok-";
|
|
1002
|
+
OTHER_FAMILY_PREFIXES = [
|
|
1003
|
+
"dev.agentvault.bridge-",
|
|
1004
|
+
// claude bridge, current label
|
|
1005
|
+
"com.agentvault.claude-bridge.",
|
|
1006
|
+
// claude bridge, pre-rename label (#837)
|
|
1007
|
+
"ai.hermes.gateway"
|
|
1008
|
+
// hermes, one service per profile
|
|
1009
|
+
];
|
|
1010
|
+
PROBE_TIMEOUT_MS = 8e3;
|
|
671
1011
|
}
|
|
672
1012
|
});
|
|
673
1013
|
|
|
@@ -45559,6 +45899,11 @@ var init_ratchet = __esm2({
|
|
|
45559
45899
|
return { header, headerSignature, ciphertext, nonce };
|
|
45560
45900
|
}
|
|
45561
45901
|
decrypt(message) {
|
|
45902
|
+
if (this.state.peerIdentityPublicKey) {
|
|
45903
|
+
if (!verifyHeaderSignature(message.header, message.headerSignature, this.state.peerIdentityPublicKey)) {
|
|
45904
|
+
throw new Error("Header signature verification failed");
|
|
45905
|
+
}
|
|
45906
|
+
}
|
|
45562
45907
|
const isV2 = message.envelopeVersion === "2.0.0" && message.encryptedHeader != null && message.headerNonce != null;
|
|
45563
45908
|
const skippedResult = this.trySkippedKeys(message, isV2);
|
|
45564
45909
|
if (skippedResult !== null) {
|
|
@@ -45572,14 +45917,14 @@ var init_ratchet = __esm2({
|
|
|
45572
45917
|
if (message.header.messageNumber === 0) {
|
|
45573
45918
|
try {
|
|
45574
45919
|
const { messageKey: testKey, headerKey: testHeaderKey, nextChainKey: nextChainKey2 } = kdfChainKey(this.state.rootKey);
|
|
45575
|
-
let
|
|
45920
|
+
let ad;
|
|
45576
45921
|
if (isV2) {
|
|
45577
45922
|
libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.encryptedHeader, null, message.headerNonce, testHeaderKey);
|
|
45578
|
-
|
|
45923
|
+
ad = message.encryptedHeader;
|
|
45579
45924
|
} else {
|
|
45580
|
-
|
|
45925
|
+
ad = serializeHeader(message.header);
|
|
45581
45926
|
}
|
|
45582
|
-
const ptBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext,
|
|
45927
|
+
const ptBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext, ad, message.nonce, testKey);
|
|
45583
45928
|
this.state.dhReceivingPublicKey = message.header.dhPublicKey;
|
|
45584
45929
|
this.state.receivingChain = {
|
|
45585
45930
|
chainKey: nextChainKey2,
|
|
@@ -45599,13 +45944,7 @@ var init_ratchet = __esm2({
|
|
|
45599
45944
|
this.skipMessages(this.state.receivingChain, message.header.messageNumber, message.header.dhPublicKey, isV2);
|
|
45600
45945
|
const chain = this.state.receivingChain;
|
|
45601
45946
|
const { nextChainKey, messageKey, headerKey } = kdfChainKey(chain.chainKey);
|
|
45602
|
-
|
|
45603
|
-
chain.messageNumber++;
|
|
45604
|
-
if (this.state.peerIdentityPublicKey) {
|
|
45605
|
-
if (!verifyHeaderSignature(message.header, message.headerSignature, this.state.peerIdentityPublicKey)) {
|
|
45606
|
-
throw new Error("Header signature verification failed");
|
|
45607
|
-
}
|
|
45608
|
-
}
|
|
45947
|
+
let plaintextBytes;
|
|
45609
45948
|
if (isV2) {
|
|
45610
45949
|
try {
|
|
45611
45950
|
libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.encryptedHeader, null, message.headerNonce, headerKey);
|
|
@@ -45613,19 +45952,21 @@ var init_ratchet = __esm2({
|
|
|
45613
45952
|
throw new Error("V2 header decryption failed");
|
|
45614
45953
|
}
|
|
45615
45954
|
try {
|
|
45616
|
-
|
|
45617
|
-
return libsodium_wrappers_default.to_string(plaintextBytes);
|
|
45955
|
+
plaintextBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext, message.encryptedHeader, message.nonce, messageKey);
|
|
45618
45956
|
} catch {
|
|
45619
45957
|
throw new Error("V2 decryption failed: ciphertext tampered or wrong key");
|
|
45620
45958
|
}
|
|
45959
|
+
} else {
|
|
45960
|
+
const ad = serializeHeader(message.header);
|
|
45961
|
+
try {
|
|
45962
|
+
plaintextBytes = libsodium_wrappers_default.crypto_aead_xchacha20poly1305_ietf_decrypt(null, message.ciphertext, ad, message.nonce, messageKey);
|
|
45963
|
+
} catch {
|
|
45964
|
+
throw new Error("Decryption failed: ciphertext tampered or wrong key");
|
|
45965
|
+
}
|
|
45621
45966
|
}
|
|
45622
|
-
|
|
45623
|
-
|
|
45624
|
-
|
|
45625
|
-
return libsodium_wrappers_default.to_string(plaintextBytes);
|
|
45626
|
-
} catch {
|
|
45627
|
-
throw new Error("Decryption failed: ciphertext tampered or wrong key");
|
|
45628
|
-
}
|
|
45967
|
+
chain.chainKey = nextChainKey;
|
|
45968
|
+
chain.messageNumber++;
|
|
45969
|
+
return libsodium_wrappers_default.to_string(plaintextBytes);
|
|
45629
45970
|
}
|
|
45630
45971
|
dhRatchetReceive(theirDhPublic) {
|
|
45631
45972
|
this.state.previousSendingChainLength = this.state.sendingChain?.messageNumber ?? 0;
|
|
@@ -63799,6 +64140,23 @@ var init_mls_delivery_order = __esm2({
|
|
|
63799
64140
|
"use strict";
|
|
63800
64141
|
}
|
|
63801
64142
|
});
|
|
64143
|
+
function noteObservedEpoch(current, seen) {
|
|
64144
|
+
const n2 = Number(seen);
|
|
64145
|
+
if (!Number.isInteger(n2) || n2 < 0)
|
|
64146
|
+
return current;
|
|
64147
|
+
return n2 > current ? n2 : current;
|
|
64148
|
+
}
|
|
64149
|
+
function needsPreSendResync(localEpoch, observedEpoch) {
|
|
64150
|
+
if (!Number.isInteger(localEpoch) || !Number.isInteger(observedEpoch)) {
|
|
64151
|
+
return false;
|
|
64152
|
+
}
|
|
64153
|
+
return observedEpoch > localEpoch;
|
|
64154
|
+
}
|
|
64155
|
+
var init_mls_presend_epoch = __esm2({
|
|
64156
|
+
"../crypto/dist/mls-presend-epoch.js"() {
|
|
64157
|
+
"use strict";
|
|
64158
|
+
}
|
|
64159
|
+
});
|
|
63802
64160
|
var dist_exports = {};
|
|
63803
64161
|
__export2(dist_exports, {
|
|
63804
64162
|
AV_CREDENTIAL_CONTEXT: () => AV_CREDENTIAL_CONTEXT,
|
|
@@ -63868,7 +64226,9 @@ __export2(dist_exports, {
|
|
|
63868
64226
|
hexTransportToEncryptedMessage: () => hexTransportToEncryptedMessage,
|
|
63869
64227
|
issueCredential: () => issueCredential,
|
|
63870
64228
|
multibaseToPublicKey: () => multibaseToPublicKey,
|
|
64229
|
+
needsPreSendResync: () => needsPreSendResync,
|
|
63871
64230
|
normalizeBackupCode: () => normalizeBackupCode,
|
|
64231
|
+
noteObservedEpoch: () => noteObservedEpoch,
|
|
63872
64232
|
orderDeliveryBatch: () => orderDeliveryBatch,
|
|
63873
64233
|
parseTraceparent: () => parseTraceparent,
|
|
63874
64234
|
performX3DH: () => performX3DH,
|
|
@@ -63908,6 +64268,7 @@ var init_dist = __esm2({
|
|
|
63908
64268
|
init_mls_group();
|
|
63909
64269
|
await init_owner_sync();
|
|
63910
64270
|
init_mls_delivery_order();
|
|
64271
|
+
init_mls_presend_epoch();
|
|
63911
64272
|
}
|
|
63912
64273
|
});
|
|
63913
64274
|
async function ensureSecureDir(dir) {
|
|
@@ -64100,7 +64461,7 @@ var init_mls_kp_pool = __esm2({
|
|
|
64100
64461
|
}
|
|
64101
64462
|
});
|
|
64102
64463
|
function ownIdentity() {
|
|
64103
|
-
const v2 = true ? "0.23.
|
|
64464
|
+
const v2 = true ? "0.23.27" : FALLBACK;
|
|
64104
64465
|
return `${PACKAGE}@${v2}`;
|
|
64105
64466
|
}
|
|
64106
64467
|
function buildClientVersion(override) {
|
|
@@ -65016,6 +65377,7 @@ var init_channel = __esm2({
|
|
|
65016
65377
|
await init_libsodium_wrappers();
|
|
65017
65378
|
await init_dist();
|
|
65018
65379
|
await init_dist();
|
|
65380
|
+
init_mls_presend_epoch();
|
|
65019
65381
|
init_mls_state();
|
|
65020
65382
|
await init_mls_kp_pool();
|
|
65021
65383
|
init_client_version();
|
|
@@ -65129,6 +65491,17 @@ var init_channel = __esm2({
|
|
|
65129
65491
|
* self-heal rather than be logged and dropped. */
|
|
65130
65492
|
_mlsCommitFailCounts = /* @__PURE__ */ new Map();
|
|
65131
65493
|
static MAX_MLS_DECRYPT_FAILS = 3;
|
|
65494
|
+
/**
|
|
65495
|
+
* #1084: consecutive resyncs that found NOTHING to apply, per group.
|
|
65496
|
+
*
|
|
65497
|
+
* "No new commits" means the server holds no commit past our epoch — we are
|
|
65498
|
+
* NOT BEHIND. Treating that as corruption and re-keying is what drove the
|
|
65499
|
+
* epoch churn (see `_resyncOnDivergence`). But a state that is genuinely
|
|
65500
|
+
* broken while its epoch counter happens to match must still recover, so the
|
|
65501
|
+
* suppression is bounded rather than absolute.
|
|
65502
|
+
*/
|
|
65503
|
+
_resyncNoOpStrikes = /* @__PURE__ */ new Map();
|
|
65504
|
+
static MAX_RESYNC_NOOPS = 3;
|
|
65132
65505
|
/** Cached MLS KeyPackage bundle for this device (regenerated on each connect). */
|
|
65133
65506
|
_mlsKeyPackage = null;
|
|
65134
65507
|
/** Pending KeyPackage bundle from request-Welcome flow (used by _handleMlsWelcome). */
|
|
@@ -65166,6 +65539,13 @@ var init_channel = __esm2({
|
|
|
65166
65539
|
_kpPoolFilling = false;
|
|
65167
65540
|
/** Buffer for MLS commits received before Welcome (keyed by groupId, sorted by epoch). */
|
|
65168
65541
|
_pendingMlsCommits = /* @__PURE__ */ new Map();
|
|
65542
|
+
/**
|
|
65543
|
+
* #1013: the highest group epoch any inbound frame has reported, per MLS
|
|
65544
|
+
* group id. Every inbound frame carries the epoch the group was at when it
|
|
65545
|
+
* was produced, so each is a LOWER BOUND on the group's current epoch — the
|
|
65546
|
+
* hint is monotonic and only ever rises.
|
|
65547
|
+
*/
|
|
65548
|
+
_observedGroupEpochs = /* @__PURE__ */ new Map();
|
|
65169
65549
|
/** In-memory credential store for renter-provided credentials (never persisted). */
|
|
65170
65550
|
_credentialStore = new CredentialStore();
|
|
65171
65551
|
/** Rooms whose roster has been refreshed from the backend at least once in
|
|
@@ -65850,16 +66230,44 @@ var init_channel = __esm2({
|
|
|
65850
66230
|
const pendingWsSends = [];
|
|
65851
66231
|
const sentSharedGroupIds = /* @__PURE__ */ new Set();
|
|
65852
66232
|
const addressedMlsGroupIds = [];
|
|
66233
|
+
if (!(this._persisted?.mlsGroups && this._state === "ready" && this._ws)) {
|
|
66234
|
+
console.warn(
|
|
66235
|
+
`[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"}`
|
|
66236
|
+
);
|
|
66237
|
+
}
|
|
65853
66238
|
if (this._persisted?.mlsGroups && this._state === "ready" && this._ws) {
|
|
65854
66239
|
const targetSharedGid = targetConvId ? this._sessionGroupIds?.get(targetConvId) : void 0;
|
|
66240
|
+
console.log(
|
|
66241
|
+
`[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)"}`
|
|
66242
|
+
);
|
|
66243
|
+
const skipped = [];
|
|
65855
66244
|
for (const [gid, entry] of Object.entries(this._persisted.mlsGroups)) {
|
|
65856
|
-
if (targetConvId && gid !== targetSharedGid)
|
|
65857
|
-
|
|
66245
|
+
if (targetConvId && gid !== targetSharedGid) {
|
|
66246
|
+
skipped.push(`${gid.slice(0, 8)}:not-target`);
|
|
66247
|
+
continue;
|
|
66248
|
+
}
|
|
66249
|
+
if (!entry.mlsGroupId) {
|
|
66250
|
+
skipped.push(`${gid.slice(0, 8)}:no-mlsGroupId`);
|
|
66251
|
+
continue;
|
|
66252
|
+
}
|
|
65858
66253
|
const mlsGroup = this._mlsGroups.get(`1to1-group:${gid}`);
|
|
65859
|
-
if (!mlsGroup?.isInitialized
|
|
66254
|
+
if (!mlsGroup?.isInitialized) {
|
|
66255
|
+
skipped.push(`${gid.slice(0, 8)}:not-in-memory`);
|
|
66256
|
+
continue;
|
|
66257
|
+
}
|
|
66258
|
+
if (Number(mlsGroup.epoch) <= 0) {
|
|
66259
|
+
skipped.push(`${gid.slice(0, 8)}:epoch<=0`);
|
|
66260
|
+
continue;
|
|
66261
|
+
}
|
|
65860
66262
|
try {
|
|
65861
66263
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
65862
|
-
const cipherBytes = await
|
|
66264
|
+
const cipherBytes = await this._encryptWithCatchUp(
|
|
66265
|
+
mlsGroup,
|
|
66266
|
+
`1to1-group:${gid}`,
|
|
66267
|
+
entry.mlsGroupId,
|
|
66268
|
+
`1:1 group ${gid.slice(0, 8)}`,
|
|
66269
|
+
plaintextBytes
|
|
66270
|
+
);
|
|
65863
66271
|
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
65864
66272
|
const payload = {
|
|
65865
66273
|
group_id: entry.mlsGroupId,
|
|
@@ -65881,8 +66289,12 @@ var init_channel = __esm2({
|
|
|
65881
66289
|
console.log(`[SecureChannel] Shared MLS group send for group ${gid.slice(0, 8)} (${entry.mlsGroupId.slice(0, 8)})`);
|
|
65882
66290
|
} catch (err) {
|
|
65883
66291
|
console.error(`[SecureChannel] Shared MLS group send failed for ${gid.slice(0, 8)}:`, err);
|
|
66292
|
+
skipped.push(`${gid.slice(0, 8)}:encrypt-threw`);
|
|
65884
66293
|
}
|
|
65885
66294
|
}
|
|
66295
|
+
if (skipped.length > 0) {
|
|
66296
|
+
console.log(`[SecureChannel] send(): skipped ${skipped.length} group(s) \u2014 ${skipped.join(", ")}`);
|
|
66297
|
+
}
|
|
65886
66298
|
}
|
|
65887
66299
|
for (const [convId, session] of this._sessions) {
|
|
65888
66300
|
if (!session.activated) continue;
|
|
@@ -65902,7 +66314,14 @@ var init_channel = __esm2({
|
|
|
65902
66314
|
}
|
|
65903
66315
|
if (mlsGroup?.isInitialized && mlsGroupId && Number(mlsGroup.epoch) > 0 && this._state === "ready" && this._ws) {
|
|
65904
66316
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
65905
|
-
const
|
|
66317
|
+
const sessionGroupKey = convGroupId && this._mlsGroups.has(`1to1-group:${convGroupId}`) ? `1to1-group:${convGroupId}` : `conv:${convId}`;
|
|
66318
|
+
const cipherBytes = await this._encryptWithCatchUp(
|
|
66319
|
+
mlsGroup,
|
|
66320
|
+
sessionGroupKey,
|
|
66321
|
+
mlsGroupId,
|
|
66322
|
+
`conversation ${convId.slice(0, 8)}`,
|
|
66323
|
+
plaintextBytes
|
|
66324
|
+
);
|
|
65906
66325
|
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
65907
66326
|
const payload = {
|
|
65908
66327
|
conversation_id: convId,
|
|
@@ -65997,7 +66416,14 @@ var init_channel = __esm2({
|
|
|
65997
66416
|
}
|
|
65998
66417
|
try {
|
|
65999
66418
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
66000
|
-
const
|
|
66419
|
+
const mlsOnlyGroupKey = mlsOnlyConvGroupId && this._mlsGroups.has(`1to1-group:${mlsOnlyConvGroupId}`) ? `1to1-group:${mlsOnlyConvGroupId}` : `conv:${mlsConvId}`;
|
|
66420
|
+
const cipherBytes = await this._encryptWithCatchUp(
|
|
66421
|
+
mlsGroup,
|
|
66422
|
+
mlsOnlyGroupKey,
|
|
66423
|
+
resolvedMlsGroupId,
|
|
66424
|
+
`conversation ${mlsConvId.slice(0, 8)}`,
|
|
66425
|
+
plaintextBytes
|
|
66426
|
+
);
|
|
66001
66427
|
await saveMlsState(this.config.dataDir, resolvedMlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
66002
66428
|
const payload = {
|
|
66003
66429
|
conversation_id: mlsConvId,
|
|
@@ -66023,13 +66449,26 @@ var init_channel = __esm2({
|
|
|
66023
66449
|
}
|
|
66024
66450
|
}
|
|
66025
66451
|
}
|
|
66026
|
-
if (sentCount === 0
|
|
66027
|
-
console.warn(
|
|
66452
|
+
if (sentCount === 0) {
|
|
66453
|
+
console.warn(
|
|
66454
|
+
`[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})`
|
|
66455
|
+
);
|
|
66028
66456
|
}
|
|
66029
66457
|
await this._persistState();
|
|
66458
|
+
const wsReady = this._ws?.readyState === 1;
|
|
66459
|
+
if (pendingWsSends.length > 0 && !wsReady) {
|
|
66460
|
+
console.warn(
|
|
66461
|
+
`[SecureChannel] send(): ${pendingWsSends.length} frame(s) NOT written \u2014 socket readyState=${this._ws?.readyState ?? "null"} (1=OPEN)`
|
|
66462
|
+
);
|
|
66463
|
+
}
|
|
66030
66464
|
for (const frame of pendingWsSends) {
|
|
66031
66465
|
this._ws.send(frame);
|
|
66032
66466
|
}
|
|
66467
|
+
if (pendingWsSends.length > 0) {
|
|
66468
|
+
console.log(
|
|
66469
|
+
`[SecureChannel] send(): wrote ${pendingWsSends.length} frame(s) to the socket (sentCount=${sentCount}, readyState=${this._ws?.readyState ?? "null"})`
|
|
66470
|
+
);
|
|
66471
|
+
}
|
|
66033
66472
|
if (!options?.isResend) {
|
|
66034
66473
|
for (const mlsGroupId of addressedMlsGroupIds) {
|
|
66035
66474
|
this._rememberRetryCandidate(mlsGroupId, plaintext, options);
|
|
@@ -66079,7 +66518,7 @@ var init_channel = __esm2({
|
|
|
66079
66518
|
*/
|
|
66080
66519
|
sendActivitySpan(spanData) {
|
|
66081
66520
|
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
|
|
66082
|
-
const pluginVersion = true ? "0.23.
|
|
66521
|
+
const pluginVersion = true ? "0.23.27" : "0.0.0-dev";
|
|
66083
66522
|
const agentName = this.config.agentName ?? "Agent";
|
|
66084
66523
|
const resource = {
|
|
66085
66524
|
"service.name": "agentvault-agent",
|
|
@@ -66289,7 +66728,13 @@ var init_channel = __esm2({
|
|
|
66289
66728
|
if (mlsGroup?.isInitialized) {
|
|
66290
66729
|
try {
|
|
66291
66730
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
66292
|
-
const ciphertext = await
|
|
66731
|
+
const ciphertext = await this._encryptWithCatchUp(
|
|
66732
|
+
mlsGroup,
|
|
66733
|
+
roomId,
|
|
66734
|
+
room.mlsGroupId,
|
|
66735
|
+
`room ${roomId.slice(0, 8)}`,
|
|
66736
|
+
plaintextBytes
|
|
66737
|
+
);
|
|
66293
66738
|
await saveMlsState(this.config.dataDir, room.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
66294
66739
|
if (this._state === "ready" && this._ws) {
|
|
66295
66740
|
this._ws.send(JSON.stringify({
|
|
@@ -66540,11 +66985,17 @@ var init_channel = __esm2({
|
|
|
66540
66985
|
const before = this._persisted.groupId;
|
|
66541
66986
|
this._sessionGroupIds = new Map(mine.map((r2) => [r2.id, r2.group_id]));
|
|
66542
66987
|
this._persisted.conversationGroupIds = Object.fromEntries(this._sessionGroupIds);
|
|
66543
|
-
const
|
|
66988
|
+
const joined = mine.find((r2) => this._persisted.mlsGroups?.[r2.group_id]);
|
|
66989
|
+
const primary = mine.find((r2) => r2.id === this._persisted.primaryConversationId) ?? joined ?? mine[0];
|
|
66990
|
+
if (!mine.some((r2) => r2.id === this._persisted.primaryConversationId)) {
|
|
66991
|
+
console.log(
|
|
66992
|
+
`[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"}`
|
|
66993
|
+
);
|
|
66994
|
+
}
|
|
66544
66995
|
this._persisted.groupId = primary.group_id;
|
|
66545
66996
|
this._persisted.primaryConversationId = primary.id;
|
|
66546
66997
|
const liveGroupIds = new Set(mine.map((r2) => r2.group_id));
|
|
66547
|
-
if (!liveGroupIds.has(staleGid)) {
|
|
66998
|
+
if (staleGid && staleMlsGroupId && !liveGroupIds.has(staleGid)) {
|
|
66548
66999
|
try {
|
|
66549
67000
|
await deleteMlsState(this.config.dataDir, staleMlsGroupId);
|
|
66550
67001
|
} catch {
|
|
@@ -66558,9 +67009,9 @@ var init_channel = __esm2({
|
|
|
66558
67009
|
}
|
|
66559
67010
|
await this._persistState();
|
|
66560
67011
|
console.log(
|
|
66561
|
-
`[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))`
|
|
67012
|
+
`[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))`
|
|
66562
67013
|
);
|
|
66563
|
-
return this._persisted.groupId !== before && before === staleGid;
|
|
67014
|
+
return staleGid !== void 0 && this._persisted.groupId !== before && before === staleGid;
|
|
66564
67015
|
}
|
|
66565
67016
|
/**
|
|
66566
67017
|
* Resend the message that a now-reconciled `unknown group` refusal killed (#732).
|
|
@@ -67468,7 +67919,13 @@ var init_channel = __esm2({
|
|
|
67468
67919
|
const mlsGroup = this._mlsGroups.get(`a2a:${channelEntry.channelId}`);
|
|
67469
67920
|
if (mlsGroup?.isInitialized) {
|
|
67470
67921
|
const plaintextBytes = new TextEncoder().encode(text);
|
|
67471
|
-
const ciphertext = await
|
|
67922
|
+
const ciphertext = await this._encryptWithCatchUp(
|
|
67923
|
+
mlsGroup,
|
|
67924
|
+
`a2a:${channelEntry.channelId}`,
|
|
67925
|
+
channelEntry.mlsGroupId,
|
|
67926
|
+
`A2A ${channelEntry.channelId.slice(0, 8)}`,
|
|
67927
|
+
plaintextBytes
|
|
67928
|
+
);
|
|
67472
67929
|
await saveMlsState(
|
|
67473
67930
|
this.config.dataDir,
|
|
67474
67931
|
channelEntry.mlsGroupId,
|
|
@@ -67516,7 +67973,13 @@ var init_channel = __esm2({
|
|
|
67516
67973
|
const mlsGroup = this._mlsGroups.get(`a2a:${channelId}`);
|
|
67517
67974
|
if (mlsGroup?.isInitialized) {
|
|
67518
67975
|
const plaintextBytes = new TextEncoder().encode(text);
|
|
67519
|
-
const ciphertext = await
|
|
67976
|
+
const ciphertext = await this._encryptWithCatchUp(
|
|
67977
|
+
mlsGroup,
|
|
67978
|
+
`a2a:${channelId}`,
|
|
67979
|
+
entry.mlsGroupId,
|
|
67980
|
+
`A2A ${channelId.slice(0, 8)}`,
|
|
67981
|
+
plaintextBytes
|
|
67982
|
+
);
|
|
67520
67983
|
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
67521
67984
|
this._ws.send(JSON.stringify({
|
|
67522
67985
|
event: "a2a_message_mls",
|
|
@@ -67962,6 +68425,9 @@ var init_channel = __esm2({
|
|
|
67962
68425
|
await this._pullDrDeliveryQueue();
|
|
67963
68426
|
await this._flushOutboundQueue();
|
|
67964
68427
|
this._setState("ready");
|
|
68428
|
+
void this._reconcileConversationGroups().catch((err) => {
|
|
68429
|
+
console.warn("[SecureChannel] Conversation-group reconcile failed:", err);
|
|
68430
|
+
});
|
|
67965
68431
|
void this._reconcileRoomsWithServer().catch(
|
|
67966
68432
|
(err) => console.warn(`[SecureChannel] room reconcile failed (ignored): ${err instanceof Error ? err.message : String(err)}`)
|
|
67967
68433
|
).then(() => this._quarantineOrphanedMlsGroups()).catch(
|
|
@@ -67989,7 +68455,7 @@ var init_channel = __esm2({
|
|
|
67989
68455
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
67990
68456
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
67991
68457
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
67992
|
-
pluginVersion: true ? "0.23.
|
|
68458
|
+
pluginVersion: true ? "0.23.27" : "0.0.0-dev"
|
|
67993
68459
|
});
|
|
67994
68460
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
67995
68461
|
}
|
|
@@ -68313,7 +68779,7 @@ var init_channel = __esm2({
|
|
|
68313
68779
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
68314
68780
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
68315
68781
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
68316
|
-
pluginVersion: true ? "0.23.
|
|
68782
|
+
pluginVersion: true ? "0.23.27" : "0.0.0-dev"
|
|
68317
68783
|
});
|
|
68318
68784
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
68319
68785
|
}
|
|
@@ -68679,6 +69145,7 @@ var init_channel = __esm2({
|
|
|
68679
69145
|
const convGroupId = data.conversation_group_id;
|
|
68680
69146
|
const groupId = data.group_id;
|
|
68681
69147
|
const senderDeviceId = data.sender_device_id;
|
|
69148
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
68682
69149
|
if (senderDeviceId === this._deviceId) return;
|
|
68683
69150
|
let mgr;
|
|
68684
69151
|
let mgrKey;
|
|
@@ -68756,9 +69223,15 @@ var init_channel = __esm2({
|
|
|
68756
69223
|
})),
|
|
68757
69224
|
no_history: filtered.length === 0
|
|
68758
69225
|
});
|
|
68759
|
-
if (mgr && mlsGroupId) {
|
|
69226
|
+
if (mgr && mgrKey && mlsGroupId) {
|
|
68760
69227
|
const responseBytes = new TextEncoder().encode(responsePayload);
|
|
68761
|
-
const cipher = await
|
|
69228
|
+
const cipher = await this._encryptWithCatchUp(
|
|
69229
|
+
mgr,
|
|
69230
|
+
mgrKey,
|
|
69231
|
+
mlsGroupId,
|
|
69232
|
+
`history catch-up ${mlsGroupId.slice(0, 8)}`,
|
|
69233
|
+
responseBytes
|
|
69234
|
+
);
|
|
68762
69235
|
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mgr.exportState()));
|
|
68763
69236
|
if (this._ws) {
|
|
68764
69237
|
this._ws.send(JSON.stringify({
|
|
@@ -69501,9 +69974,36 @@ ${messageText}`;
|
|
|
69501
69974
|
// ---------------------------------------------------------------------------
|
|
69502
69975
|
// MLS room message handlers
|
|
69503
69976
|
// ---------------------------------------------------------------------------
|
|
69977
|
+
/**
|
|
69978
|
+
* #1014: hand a room-framed frame to the handler that owns its group family.
|
|
69979
|
+
*
|
|
69980
|
+
* Returns true when the frame was routed. Routing means "give it to the right
|
|
69981
|
+
* handler" — NOT "decrypt it as a room message". Decrypting under the wrong
|
|
69982
|
+
* group is how MLS state gets corrupted, so an unplaceable group is left for
|
|
69983
|
+
* the caller to self-heal rather than guessed at.
|
|
69984
|
+
*/
|
|
69985
|
+
async _routeNonRoomGroupMessage(data, groupId) {
|
|
69986
|
+
const target = this._resolveGroupFamily(groupId);
|
|
69987
|
+
if (!target || target.family === "room") return false;
|
|
69988
|
+
console.log(
|
|
69989
|
+
`[SecureChannel] Room-framed message belongs to ${target.label} \u2014 routing to the handler that owns it (#1014)`
|
|
69990
|
+
);
|
|
69991
|
+
switch (target.family) {
|
|
69992
|
+
case "shared1to1":
|
|
69993
|
+
await this._handleMessageMLS({ ...data, conversation_group_id: target.id });
|
|
69994
|
+
return true;
|
|
69995
|
+
case "conversation":
|
|
69996
|
+
await this._handleMessageMLS({ ...data, conversation_id: target.id });
|
|
69997
|
+
return true;
|
|
69998
|
+
case "a2a":
|
|
69999
|
+
await this._handleA2AMessageMLS({ ...data, a2a_channel_id: target.id });
|
|
70000
|
+
return true;
|
|
70001
|
+
}
|
|
70002
|
+
}
|
|
69504
70003
|
async _handleRoomMessageMLS(data) {
|
|
69505
70004
|
const groupId = data.group_id;
|
|
69506
70005
|
const senderDeviceId = data.sender_device_id;
|
|
70006
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
69507
70007
|
if (senderDeviceId === this._deviceId) return;
|
|
69508
70008
|
let roomId;
|
|
69509
70009
|
for (const [rid, room] of Object.entries(this._persisted?.rooms ?? {})) {
|
|
@@ -69517,7 +70017,9 @@ ${messageText}`;
|
|
|
69517
70017
|
roomId = data.room_id;
|
|
69518
70018
|
console.log(`[SecureChannel] Room ${roomId.slice(0, 8)} matched by room_id (group_id ${groupId?.slice(0, 8)} mismatch)`);
|
|
69519
70019
|
} else {
|
|
70020
|
+
if (await this._routeNonRoomGroupMessage(data, groupId)) return;
|
|
69520
70021
|
console.warn(`[SecureChannel] No room found for MLS group ${groupId?.slice(0, 8)}`);
|
|
70022
|
+
if (groupId) void this._requestWelcomeSelfHeal(groupId);
|
|
69521
70023
|
return;
|
|
69522
70024
|
}
|
|
69523
70025
|
}
|
|
@@ -69705,63 +70207,63 @@ ${messageText}`;
|
|
|
69705
70207
|
console.warn(`[SecureChannel] Failed to send history catchup response:`, sendErr);
|
|
69706
70208
|
}
|
|
69707
70209
|
}
|
|
70210
|
+
/**
|
|
70211
|
+
* Apply an inbound MLS commit to whichever group it addresses.
|
|
70212
|
+
*
|
|
70213
|
+
* Returns TRUE when the commit was applied, buffered, or handed to the
|
|
70214
|
+
* failure ladder — i.e. when this agent has taken responsibility for it and
|
|
70215
|
+
* the delivery-queue row may safely be acked. Returns FALSE when the commit
|
|
70216
|
+
* names a group we hold no state for at all, so the caller must NACK and let
|
|
70217
|
+
* the row survive.
|
|
70218
|
+
*
|
|
70219
|
+
* #1012: this used to loop `rooms` then `a2aChannels` and then END. A shared
|
|
70220
|
+
* 1:1 commit (`mlsGroups`) and a legacy per-conversation commit
|
|
70221
|
+
* (`mlsConversations`) matched neither. The call fell off the bottom with no
|
|
70222
|
+
* log, no buffer and no error — and because it did not THROW,
|
|
70223
|
+
* `_pullDeliveryQueue` acked the row and the server deleted it. An MLS commit
|
|
70224
|
+
* cannot be regenerated, so the agent stayed at epoch N while the group moved
|
|
70225
|
+
* to N+1 and every later send() encrypted at an epoch nobody could decrypt.
|
|
70226
|
+
*
|
|
70227
|
+
* The families are enumerated in ONE list resolved by ONE loop, because two
|
|
70228
|
+
* hand-copied loops are exactly how the 1:1 path fell two families behind.
|
|
70229
|
+
*/
|
|
69708
70230
|
async _handleMlsCommit(data) {
|
|
69709
70231
|
const groupId = data.group_id;
|
|
69710
70232
|
const epoch = typeof data.epoch === "number" ? data.epoch : 0;
|
|
69711
|
-
|
|
69712
|
-
|
|
69713
|
-
|
|
69714
|
-
|
|
69715
|
-
|
|
69716
|
-
|
|
69717
|
-
|
|
69718
|
-
|
|
69719
|
-
|
|
69720
|
-
this._mlsCommitFailCounts.delete(roomId);
|
|
69721
|
-
} catch (err) {
|
|
69722
|
-
await this._onCommitFailure(
|
|
69723
|
-
roomId,
|
|
69724
|
-
groupId,
|
|
69725
|
-
err,
|
|
69726
|
-
`room ${roomId.slice(0, 8)}`,
|
|
69727
|
-
data.epoch,
|
|
69728
|
-
mlsGroup.epoch
|
|
69729
|
-
);
|
|
69730
|
-
}
|
|
69731
|
-
} else {
|
|
69732
|
-
this._bufferMlsCommit(groupId, epoch, data);
|
|
69733
|
-
console.log(`[SecureChannel] Buffered MLS commit for room ${roomId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
|
|
69734
|
-
}
|
|
69735
|
-
return;
|
|
70233
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
70234
|
+
const target = this._resolveGroupFamily(groupId);
|
|
70235
|
+
if (target) {
|
|
70236
|
+
const { managerKey: groupKey, label } = target;
|
|
70237
|
+
const mlsGroup = this._mlsGroups.get(groupKey);
|
|
70238
|
+
if (!mlsGroup?.isInitialized) {
|
|
70239
|
+
this._bufferMlsCommit(groupId, epoch, data);
|
|
70240
|
+
console.log(`[SecureChannel] Buffered MLS commit for ${label} (epoch=${epoch}, group not initialized)`);
|
|
70241
|
+
return true;
|
|
69736
70242
|
}
|
|
69737
|
-
|
|
69738
|
-
|
|
69739
|
-
|
|
69740
|
-
|
|
69741
|
-
|
|
69742
|
-
|
|
69743
|
-
|
|
69744
|
-
|
|
69745
|
-
|
|
69746
|
-
|
|
69747
|
-
|
|
69748
|
-
|
|
69749
|
-
|
|
69750
|
-
|
|
69751
|
-
|
|
69752
|
-
err,
|
|
69753
|
-
`A2A ${chId.slice(0, 8)}`,
|
|
69754
|
-
data.epoch,
|
|
69755
|
-
mlsGroup.epoch
|
|
69756
|
-
);
|
|
69757
|
-
}
|
|
69758
|
-
} else {
|
|
69759
|
-
this._bufferMlsCommit(groupId, epoch, data);
|
|
69760
|
-
console.log(`[SecureChannel] Buffered MLS commit for A2A ${chId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
|
|
69761
|
-
}
|
|
69762
|
-
return;
|
|
70243
|
+
try {
|
|
70244
|
+
const commitBytes = new Uint8Array(Buffer.from(data.payload, "hex"));
|
|
70245
|
+
await mlsGroup.processCommit(commitBytes);
|
|
70246
|
+
await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
|
|
70247
|
+
console.log(`[SecureChannel] MLS commit processed for ${label} (epoch=${mlsGroup.epoch})`);
|
|
70248
|
+
this._mlsCommitFailCounts.delete(groupKey);
|
|
70249
|
+
} catch (err) {
|
|
70250
|
+
await this._onCommitFailure(
|
|
70251
|
+
groupKey,
|
|
70252
|
+
groupId,
|
|
70253
|
+
err,
|
|
70254
|
+
label,
|
|
70255
|
+
data.epoch,
|
|
70256
|
+
mlsGroup.epoch
|
|
70257
|
+
);
|
|
69763
70258
|
}
|
|
70259
|
+
return true;
|
|
69764
70260
|
}
|
|
70261
|
+
this._bufferMlsCommit(groupId, epoch, data);
|
|
70262
|
+
console.warn(
|
|
70263
|
+
`[SecureChannel] MLS commit for UNKNOWN group ${String(groupId).slice(0, 8)} (epoch=${epoch}) \u2014 buffered, requesting a Welcome, NOT acked`
|
|
70264
|
+
);
|
|
70265
|
+
if (groupId) void this._requestWelcomeSelfHeal(groupId);
|
|
70266
|
+
return false;
|
|
69765
70267
|
}
|
|
69766
70268
|
/** Buffer an MLS commit for replay after Welcome join. Max 50 per group. */
|
|
69767
70269
|
_bufferMlsCommit(groupId, epoch, data) {
|
|
@@ -69951,6 +70453,229 @@ ${messageText}`;
|
|
|
69951
70453
|
* `1to1-group:<gid>`, `a2a:<channelId>`, …) — also the decrypt-fail-count key.
|
|
69952
70454
|
* @param groupId the server-side MLS group id used in the /sync URL.
|
|
69953
70455
|
*/
|
|
70456
|
+
/**
|
|
70457
|
+
* #1015 — the ONLY place this package fulfils a Welcome request.
|
|
70458
|
+
*
|
|
70459
|
+
* Add the members, transmit, and record `fulfilled` **only for the targets
|
|
70460
|
+
* whose frames actually went out**. Every one of the five sites that used to
|
|
70461
|
+
* do this by hand followed the same sequence and had the same three holes:
|
|
70462
|
+
* `this._ws` null (both sends skipped by an `if`), a CLOSING socket (`send`
|
|
70463
|
+
* returns without throwing), and a fulfil POST wrapped in `.catch(() => {})`.
|
|
70464
|
+
*
|
|
70465
|
+
* A fulfilled request is never re-requested. So a Welcome that evaporated
|
|
70466
|
+
* left the requester locked out, the fulfiller at epoch N+1, everyone else at
|
|
70467
|
+
* N, and the bridging commit untransmitted and impossible to regenerate — a
|
|
70468
|
+
* permanently wedged group in which every signal read success.
|
|
70469
|
+
*
|
|
70470
|
+
* Returns the number of requests actually fulfilled.
|
|
70471
|
+
*/
|
|
70472
|
+
async _fulfilWelcomeRequests(opts) {
|
|
70473
|
+
const { mlsGroup, mlsGroupId, label, welcomeFields, targets } = opts;
|
|
70474
|
+
if (targets.length === 0) return 0;
|
|
70475
|
+
if (!this._wsIsOpen()) {
|
|
70476
|
+
console.warn(
|
|
70477
|
+
`[SecureChannel] ${label}: socket is not OPEN \u2014 NOT advancing the group; ${targets.length} request(s) stay pending for the next pull`
|
|
70478
|
+
);
|
|
70479
|
+
return 0;
|
|
70480
|
+
}
|
|
70481
|
+
const snapshot = mlsGroup.exportState();
|
|
70482
|
+
const { commit, welcome } = await mlsGroup.addMembers(targets.map((t2) => t2.keyPackage));
|
|
70483
|
+
const commitSent = await this._sendFrameConfirmed(
|
|
70484
|
+
{
|
|
70485
|
+
event: "mls_commit",
|
|
70486
|
+
data: {
|
|
70487
|
+
group_id: mlsGroupId,
|
|
70488
|
+
epoch: Number(mlsGroup.epoch),
|
|
70489
|
+
payload: Buffer.from(commit).toString("hex")
|
|
70490
|
+
}
|
|
70491
|
+
},
|
|
70492
|
+
`${label} commit`
|
|
70493
|
+
);
|
|
70494
|
+
if (!commitSent) {
|
|
70495
|
+
mlsGroup.importState(snapshot);
|
|
70496
|
+
console.warn(
|
|
70497
|
+
`[SecureChannel] ${label}: commit was NOT written \u2014 rolled back to epoch ${Number(mlsGroup.epoch)}, nothing fulfilled, requests stay pending`
|
|
70498
|
+
);
|
|
70499
|
+
return 0;
|
|
70500
|
+
}
|
|
70501
|
+
await saveMlsState(this.config.dataDir, mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70502
|
+
if (!welcome) {
|
|
70503
|
+
console.warn(
|
|
70504
|
+
`[SecureChannel] ${label}: addMembers produced NO Welcome \u2014 ${targets.length} request(s) left pending rather than marked fulfilled`
|
|
70505
|
+
);
|
|
70506
|
+
return 0;
|
|
70507
|
+
}
|
|
70508
|
+
let fulfilled = 0;
|
|
70509
|
+
for (const t2 of targets) {
|
|
70510
|
+
const sent = await this._sendFrameConfirmed(
|
|
70511
|
+
{
|
|
70512
|
+
event: "mls_welcome",
|
|
70513
|
+
data: {
|
|
70514
|
+
target_device_id: t2.deviceId,
|
|
70515
|
+
group_id: mlsGroupId,
|
|
70516
|
+
...welcomeFields,
|
|
70517
|
+
payload: Buffer.from(welcome).toString("hex")
|
|
70518
|
+
}
|
|
70519
|
+
},
|
|
70520
|
+
`${label} welcome\u2192${t2.deviceId.slice(0, 8)}`
|
|
70521
|
+
);
|
|
70522
|
+
if (!sent) {
|
|
70523
|
+
console.warn(
|
|
70524
|
+
`[SecureChannel] ${label}: Welcome for ${t2.deviceId.slice(0, 8)} was NOT written \u2014 leaving the request PENDING so the next pull retries it`
|
|
70525
|
+
);
|
|
70526
|
+
continue;
|
|
70527
|
+
}
|
|
70528
|
+
if (await this._postFulfilWelcome(mlsGroupId, t2.requestId, `${label} ${t2.deviceId.slice(0, 8)}`)) {
|
|
70529
|
+
fulfilled++;
|
|
70530
|
+
}
|
|
70531
|
+
}
|
|
70532
|
+
return fulfilled;
|
|
70533
|
+
}
|
|
70534
|
+
/**
|
|
70535
|
+
* Resolve an MLS group id to the family that owns it.
|
|
70536
|
+
*
|
|
70537
|
+
* There are four: rooms, A2A channels, shared 1:1 groups, and the legacy
|
|
70538
|
+
* per-conversation groups. They all store the MLS group id under
|
|
70539
|
+
* `mlsGroupId`; they differ only in the key their live `MLSGroupManager` sits
|
|
70540
|
+
* under in `_mlsGroups`.
|
|
70541
|
+
*
|
|
70542
|
+
* ONE resolver, because hand-copying this enumeration is what produced
|
|
70543
|
+
* #1012 (`_handleMlsCommit` handled two families of four), #1014 (a
|
|
70544
|
+
* room-framed 1:1 replay matched nothing) and #1016 (a rejoin dropped only
|
|
70545
|
+
* the room manager). Three bugs, one missing abstraction.
|
|
70546
|
+
*/
|
|
70547
|
+
_resolveGroupFamily(mlsGroupId) {
|
|
70548
|
+
if (!mlsGroupId) return null;
|
|
70549
|
+
const families = [
|
|
70550
|
+
{ family: "room", entries: this._persisted?.rooms, managerKey: (id) => id, label: "room" },
|
|
70551
|
+
{ family: "a2a", entries: this._persisted?.a2aChannels, managerKey: (id) => `a2a:${id}`, label: "A2A" },
|
|
70552
|
+
{ family: "shared1to1", entries: this._persisted?.mlsGroups, managerKey: (id) => `1to1-group:${id}`, label: "1:1 group" },
|
|
70553
|
+
{ family: "conversation", entries: this._persisted?.mlsConversations, managerKey: (id) => `conv:${id}`, label: "conversation" }
|
|
70554
|
+
];
|
|
70555
|
+
for (const f2 of families) {
|
|
70556
|
+
for (const [id, entry] of Object.entries(f2.entries ?? {})) {
|
|
70557
|
+
if (entry?.mlsGroupId !== mlsGroupId) continue;
|
|
70558
|
+
return {
|
|
70559
|
+
family: f2.family,
|
|
70560
|
+
id,
|
|
70561
|
+
managerKey: f2.managerKey(id),
|
|
70562
|
+
label: `${f2.label} ${id.slice(0, 8)}`
|
|
70563
|
+
};
|
|
70564
|
+
}
|
|
70565
|
+
}
|
|
70566
|
+
return null;
|
|
70567
|
+
}
|
|
70568
|
+
/** #1015: is the socket in a state that can actually take a write? */
|
|
70569
|
+
_wsIsOpen() {
|
|
70570
|
+
return this._ws?.readyState === 1;
|
|
70571
|
+
}
|
|
70572
|
+
/**
|
|
70573
|
+
* #1015: write one frame and report whether the socket took it.
|
|
70574
|
+
*
|
|
70575
|
+
* `ws.send()` on a CLOSING or CLOSED socket returns WITHOUT throwing — the
|
|
70576
|
+
* error surfaces only through the callback, and every fulfilment site passed
|
|
70577
|
+
* none. So a Welcome could be "sent" into a dead socket and the requester
|
|
70578
|
+
* still marked `fulfilled`, which it never re-requests.
|
|
70579
|
+
*/
|
|
70580
|
+
_sendFrameConfirmed(frame, label) {
|
|
70581
|
+
const ws = this._ws;
|
|
70582
|
+
if (!ws || ws.readyState !== 1) {
|
|
70583
|
+
console.warn(
|
|
70584
|
+
`[SecureChannel] ${label}: socket is not OPEN (readyState=${ws?.readyState ?? "null"}) \u2014 frame NOT written`
|
|
70585
|
+
);
|
|
70586
|
+
return Promise.resolve(false);
|
|
70587
|
+
}
|
|
70588
|
+
return new Promise((resolve3) => {
|
|
70589
|
+
try {
|
|
70590
|
+
ws.send(JSON.stringify(frame), (err) => {
|
|
70591
|
+
if (err) {
|
|
70592
|
+
console.error(`[SecureChannel] ${label}: socket refused the frame:`, err);
|
|
70593
|
+
resolve3(false);
|
|
70594
|
+
} else {
|
|
70595
|
+
resolve3(true);
|
|
70596
|
+
}
|
|
70597
|
+
});
|
|
70598
|
+
} catch (err) {
|
|
70599
|
+
console.error(`[SecureChannel] ${label}: send threw:`, err);
|
|
70600
|
+
resolve3(false);
|
|
70601
|
+
}
|
|
70602
|
+
});
|
|
70603
|
+
}
|
|
70604
|
+
/**
|
|
70605
|
+
* #1015: record a Welcome as fulfilled — and say so when it fails.
|
|
70606
|
+
*
|
|
70607
|
+
* The batched shared-1:1 site posted this with `.catch(() => {})`, so an HTTP
|
|
70608
|
+
* failure here was invisible on top of the sends being invisible.
|
|
70609
|
+
*/
|
|
70610
|
+
async _postFulfilWelcome(mlsGroupId, requestId, label) {
|
|
70611
|
+
try {
|
|
70612
|
+
const res = await fetch(
|
|
70613
|
+
`${this.config.apiUrl}/api/v1/mls/groups/${mlsGroupId}/fulfill-welcome`,
|
|
70614
|
+
{
|
|
70615
|
+
method: "POST",
|
|
70616
|
+
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70617
|
+
body: JSON.stringify({ request_id: requestId })
|
|
70618
|
+
}
|
|
70619
|
+
);
|
|
70620
|
+
if (!res.ok) {
|
|
70621
|
+
console.warn(`[SecureChannel] ${label}: fulfill-welcome returned ${res.status}`);
|
|
70622
|
+
return false;
|
|
70623
|
+
}
|
|
70624
|
+
return true;
|
|
70625
|
+
} catch (err) {
|
|
70626
|
+
console.warn(`[SecureChannel] ${label}: fulfill-welcome failed:`, err);
|
|
70627
|
+
return false;
|
|
70628
|
+
}
|
|
70629
|
+
}
|
|
70630
|
+
/**
|
|
70631
|
+
* #1013: fold an epoch seen on an inbound frame into the running hint.
|
|
70632
|
+
*
|
|
70633
|
+
* Anything unusable (missing, non-numeric, negative, fractional) is ignored
|
|
70634
|
+
* rather than defaulted. A defaulted epoch here would be a guess, and a guess
|
|
70635
|
+
* that reads LOW silently disables the check it exists to drive.
|
|
70636
|
+
*/
|
|
70637
|
+
_noteGroupEpoch(mlsGroupId, epoch) {
|
|
70638
|
+
if (typeof mlsGroupId !== "string" || !mlsGroupId) return;
|
|
70639
|
+
const current = this._observedGroupEpochs.get(mlsGroupId) ?? 0;
|
|
70640
|
+
const next = noteObservedEpoch(current, epoch);
|
|
70641
|
+
if (next > current) this._observedGroupEpochs.set(mlsGroupId, next);
|
|
70642
|
+
}
|
|
70643
|
+
/**
|
|
70644
|
+
* #1013 — the ONLY place this package encrypts an MLS application message.
|
|
70645
|
+
*
|
|
70646
|
+
* Catch up first when the group is known to have moved past us, then encrypt.
|
|
70647
|
+
* Hermes has done this since #440 and the browser since #996; the plugin —
|
|
70648
|
+
* the implementation that actually ships to customers — did not, so an agent
|
|
70649
|
+
* that missed one commit encrypted every later message at an epoch nobody
|
|
70650
|
+
* could read, while the server stored and relayed it and every signal read
|
|
70651
|
+
* 200.
|
|
70652
|
+
*
|
|
70653
|
+
* Funnelling every sender through one method is the point. A guard applied at
|
|
70654
|
+
* seven call sites decays to a guard applied at six: that is exactly how
|
|
70655
|
+
* `_handleMlsCommit` ended up handling two group families out of four (#1012).
|
|
70656
|
+
*
|
|
70657
|
+
* Best-effort by design. A catch-up that cannot replay — or that throws — must
|
|
70658
|
+
* NOT block the send; we fall through and encrypt at the local epoch, which is
|
|
70659
|
+
* precisely today's behaviour, so the worst case is no regression.
|
|
70660
|
+
*/
|
|
70661
|
+
async _encryptWithCatchUp(mlsGroup, groupKey, mlsGroupId, label, plaintextBytes) {
|
|
70662
|
+
const observed = this._observedGroupEpochs.get(mlsGroupId);
|
|
70663
|
+
if (observed !== void 0 && needsPreSendResync(Number(mlsGroup.epoch), observed)) {
|
|
70664
|
+
console.warn(
|
|
70665
|
+
`[SecureChannel] ${label}: local epoch ${Number(mlsGroup.epoch)} is behind the observed group epoch ${observed} \u2014 catching up before encrypt (#1013)`
|
|
70666
|
+
);
|
|
70667
|
+
try {
|
|
70668
|
+
if (!await this._resyncOnDivergence(groupKey, mlsGroupId)) {
|
|
70669
|
+
console.warn(
|
|
70670
|
+
`[SecureChannel] ${label}: catch-up could not replay \u2014 sending at epoch ${Number(mlsGroup.epoch)}. Peers past this epoch will not read it.`
|
|
70671
|
+
);
|
|
70672
|
+
}
|
|
70673
|
+
} catch (err) {
|
|
70674
|
+
console.warn(`[SecureChannel] ${label}: catch-up failed:`, err);
|
|
70675
|
+
}
|
|
70676
|
+
}
|
|
70677
|
+
return await mlsGroup.encrypt(plaintextBytes);
|
|
70678
|
+
}
|
|
69954
70679
|
async _resyncOnDivergence(groupKey, groupId) {
|
|
69955
70680
|
const mgr = this._mlsGroups.get(groupKey);
|
|
69956
70681
|
if (!mgr?.isInitialized) return false;
|
|
@@ -69977,11 +70702,24 @@ ${messageText}`;
|
|
|
69977
70702
|
applied++;
|
|
69978
70703
|
}
|
|
69979
70704
|
if (applied === 0) {
|
|
69980
|
-
|
|
70705
|
+
const strikes = (this._resyncNoOpStrikes.get(groupKey) ?? 0) + 1;
|
|
70706
|
+
this._resyncNoOpStrikes.set(groupKey, strikes);
|
|
70707
|
+
if (strikes < _SecureChannel.MAX_RESYNC_NOOPS) {
|
|
70708
|
+
this._mlsDecryptFailCounts.delete(groupKey);
|
|
70709
|
+
console.log(
|
|
70710
|
+
`[SecureChannel] Resync for ${groupId.slice(0, 8)}: already current at epoch=${mgr.epoch} (strike ${strikes}/${_SecureChannel.MAX_RESYNC_NOOPS}) \u2014 NOT re-keying (#1084)`
|
|
70711
|
+
);
|
|
70712
|
+
return true;
|
|
70713
|
+
}
|
|
70714
|
+
this._resyncNoOpStrikes.delete(groupKey);
|
|
70715
|
+
console.warn(
|
|
70716
|
+
`[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`
|
|
70717
|
+
);
|
|
69981
70718
|
return false;
|
|
69982
70719
|
}
|
|
69983
70720
|
await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mgr.exportState()));
|
|
69984
70721
|
this._mlsDecryptFailCounts.delete(groupKey);
|
|
70722
|
+
this._resyncNoOpStrikes.delete(groupKey);
|
|
69985
70723
|
console.log(
|
|
69986
70724
|
`[SecureChannel] Resync-on-divergence: applied ${applied} missed commit(s) for ${groupId.slice(0, 8)}, now epoch=${mgr.epoch}`
|
|
69987
70725
|
);
|
|
@@ -70368,6 +71106,7 @@ ${messageText}`;
|
|
|
70368
71106
|
const ackedIds = [];
|
|
70369
71107
|
const nackedIds = [];
|
|
70370
71108
|
for (const msg of messages) {
|
|
71109
|
+
let handled = true;
|
|
70371
71110
|
try {
|
|
70372
71111
|
if (msg.sender_device_id === this._deviceId) {
|
|
70373
71112
|
ackedIds.push(msg.queue_id);
|
|
@@ -70384,7 +71123,7 @@ ${messageText}`;
|
|
|
70384
71123
|
a2a_channel_id: msg.a2a_channel_id
|
|
70385
71124
|
});
|
|
70386
71125
|
} else if (msg.message_type === "commit") {
|
|
70387
|
-
await this._handleMlsCommit({
|
|
71126
|
+
handled = await this._handleMlsCommit({
|
|
70388
71127
|
group_id: msg.group_id,
|
|
70389
71128
|
sender_device_id: msg.sender_device_id,
|
|
70390
71129
|
epoch: msg.epoch,
|
|
@@ -70422,9 +71161,18 @@ ${messageText}`;
|
|
|
70422
71161
|
conversation_group_id: msg.conversation_group_id,
|
|
70423
71162
|
created_at: msg.created_at
|
|
70424
71163
|
});
|
|
71164
|
+
} else {
|
|
71165
|
+
handled = false;
|
|
71166
|
+
console.warn(
|
|
71167
|
+
`[SecureChannel] Delivery application row ${String(msg.message_id).slice(0, 8)} names no room, A2A channel or conversation \u2014 NOT acked`
|
|
71168
|
+
);
|
|
70425
71169
|
}
|
|
70426
71170
|
}
|
|
70427
|
-
|
|
71171
|
+
if (handled) {
|
|
71172
|
+
ackedIds.push(msg.queue_id);
|
|
71173
|
+
} else {
|
|
71174
|
+
nackedIds.push(msg.queue_id);
|
|
71175
|
+
}
|
|
70428
71176
|
} catch (err) {
|
|
70429
71177
|
console.warn(`[SecureChannel] Delivery ${msg.message_type} processing failed:`, err);
|
|
70430
71178
|
nackedIds.push(msg.queue_id);
|
|
@@ -70493,38 +71241,20 @@ ${messageText}`;
|
|
|
70493
71241
|
if (!kpHex) continue;
|
|
70494
71242
|
const kpBytes = new Uint8Array(Buffer.from(kpHex, "hex"));
|
|
70495
71243
|
const memberKp = MLSGroupManager.deserializeKeyPackage(kpBytes);
|
|
70496
|
-
const
|
|
70497
|
-
|
|
70498
|
-
|
|
70499
|
-
|
|
70500
|
-
|
|
70501
|
-
|
|
70502
|
-
|
|
70503
|
-
|
|
70504
|
-
|
|
70505
|
-
}
|
|
70506
|
-
}
|
|
70507
|
-
if (
|
|
70508
|
-
|
|
70509
|
-
event: "mls_welcome",
|
|
70510
|
-
data: {
|
|
70511
|
-
target_device_id: req.requesting_device_id,
|
|
70512
|
-
group_id: roomState.mlsGroupId,
|
|
70513
|
-
room_id: roomId,
|
|
70514
|
-
payload: Buffer.from(welcome).toString("hex")
|
|
70515
|
-
}
|
|
70516
|
-
}));
|
|
71244
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71245
|
+
mlsGroup,
|
|
71246
|
+
mlsGroupId: roomState.mlsGroupId,
|
|
71247
|
+
label: `room ${roomId.slice(0, 8)}`,
|
|
71248
|
+
welcomeFields: { room_id: roomId },
|
|
71249
|
+
targets: [{
|
|
71250
|
+
requestId: req.id,
|
|
71251
|
+
deviceId: req.requesting_device_id,
|
|
71252
|
+
keyPackage: memberKp
|
|
71253
|
+
}]
|
|
71254
|
+
});
|
|
71255
|
+
if (done > 0) {
|
|
71256
|
+
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in room ${roomId.slice(0, 8)}`);
|
|
70517
71257
|
}
|
|
70518
|
-
await fetch(
|
|
70519
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${roomState.mlsGroupId}/fulfill-welcome`,
|
|
70520
|
-
{
|
|
70521
|
-
method: "POST",
|
|
70522
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70523
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70524
|
-
}
|
|
70525
|
-
);
|
|
70526
|
-
await saveMlsState(this.config.dataDir, roomState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70527
|
-
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in room ${roomId.slice(0, 8)}`);
|
|
70528
71258
|
} catch (fulfillErr) {
|
|
70529
71259
|
console.warn(`[SecureChannel] Welcome fulfill failed for ${req.requesting_device_id.slice(0, 8)}:`, fulfillErr);
|
|
70530
71260
|
}
|
|
@@ -70563,65 +71293,38 @@ ${messageText}`;
|
|
|
70563
71293
|
validReqs.push({ req, kp: MLSGroupManager.deserializeKeyPackage(kpBytes) });
|
|
70564
71294
|
}
|
|
70565
71295
|
if (validReqs.length === 0) continue;
|
|
71296
|
+
const sharedTargets = validReqs.map((v2) => ({
|
|
71297
|
+
requestId: v2.req.id,
|
|
71298
|
+
deviceId: v2.req.requesting_device_id,
|
|
71299
|
+
keyPackage: v2.kp
|
|
71300
|
+
}));
|
|
70566
71301
|
try {
|
|
70567
|
-
const
|
|
70568
|
-
|
|
70569
|
-
|
|
70570
|
-
|
|
70571
|
-
|
|
70572
|
-
|
|
70573
|
-
|
|
70574
|
-
|
|
70575
|
-
|
|
70576
|
-
}));
|
|
70577
|
-
}
|
|
70578
|
-
if (welcome && this._ws) {
|
|
70579
|
-
for (const { req } of validReqs) {
|
|
70580
|
-
this._ws.send(JSON.stringify({
|
|
70581
|
-
event: "mls_welcome",
|
|
70582
|
-
data: {
|
|
70583
|
-
target_device_id: req.requesting_device_id,
|
|
70584
|
-
group_id: entry.mlsGroupId,
|
|
70585
|
-
conversation_group_id: gid,
|
|
70586
|
-
payload: Buffer.from(welcome).toString("hex")
|
|
70587
|
-
}
|
|
70588
|
-
}));
|
|
70589
|
-
}
|
|
70590
|
-
}
|
|
70591
|
-
for (const { req } of validReqs) {
|
|
70592
|
-
await fetch(
|
|
70593
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${entry.mlsGroupId}/fulfill-welcome`,
|
|
70594
|
-
{
|
|
70595
|
-
method: "POST",
|
|
70596
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70597
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70598
|
-
}
|
|
70599
|
-
).catch(() => {
|
|
70600
|
-
});
|
|
71302
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71303
|
+
mlsGroup,
|
|
71304
|
+
mlsGroupId: entry.mlsGroupId,
|
|
71305
|
+
label: `shared group ${gid.slice(0, 8)}`,
|
|
71306
|
+
welcomeFields: { conversation_group_id: gid },
|
|
71307
|
+
targets: sharedTargets
|
|
71308
|
+
});
|
|
71309
|
+
if (done > 0) {
|
|
71310
|
+
console.log(`[SecureChannel] Batched Welcome for ${done}/${validReqs.length} devices in shared group ${gid.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
|
|
70601
71311
|
}
|
|
70602
|
-
await saveMlsState(this.config.dataDir, entry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70603
|
-
console.log(`[SecureChannel] Batched Welcome for ${validReqs.length} devices in shared group ${gid.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
|
|
70604
71312
|
} catch (batchErr) {
|
|
70605
71313
|
console.warn(`[SecureChannel] Batched Welcome failed for shared group ${gid.slice(0, 8)}, falling back to individual:`, batchErr);
|
|
70606
|
-
for (const
|
|
71314
|
+
for (const target of sharedTargets) {
|
|
70607
71315
|
try {
|
|
70608
|
-
const
|
|
70609
|
-
|
|
70610
|
-
|
|
70611
|
-
|
|
70612
|
-
|
|
70613
|
-
|
|
70614
|
-
}
|
|
70615
|
-
await fetch(`${this.config.apiUrl}/api/v1/mls/groups/${entry.mlsGroupId}/fulfill-welcome`, {
|
|
70616
|
-
method: "POST",
|
|
70617
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70618
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70619
|
-
}).catch(() => {
|
|
71316
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71317
|
+
mlsGroup,
|
|
71318
|
+
mlsGroupId: entry.mlsGroupId,
|
|
71319
|
+
label: `shared group ${gid.slice(0, 8)}`,
|
|
71320
|
+
welcomeFields: { conversation_group_id: gid },
|
|
71321
|
+
targets: [target]
|
|
70620
71322
|
});
|
|
70621
|
-
|
|
70622
|
-
|
|
71323
|
+
if (done > 0) {
|
|
71324
|
+
console.log(`[SecureChannel] Individual Welcome for ${target.deviceId.slice(0, 8)} in shared group ${gid.slice(0, 8)}`);
|
|
71325
|
+
}
|
|
70623
71326
|
} catch (indivErr) {
|
|
70624
|
-
console.warn(`[SecureChannel] Individual Welcome failed for ${
|
|
71327
|
+
console.warn(`[SecureChannel] Individual Welcome failed for ${target.deviceId.slice(0, 8)}:`, indivErr);
|
|
70625
71328
|
}
|
|
70626
71329
|
}
|
|
70627
71330
|
}
|
|
@@ -70653,38 +71356,20 @@ ${messageText}`;
|
|
|
70653
71356
|
if (!kpHex) continue;
|
|
70654
71357
|
const kpBytes = new Uint8Array(Buffer.from(kpHex, "hex"));
|
|
70655
71358
|
const memberKp = MLSGroupManager.deserializeKeyPackage(kpBytes);
|
|
70656
|
-
const
|
|
70657
|
-
|
|
70658
|
-
|
|
70659
|
-
|
|
70660
|
-
|
|
70661
|
-
|
|
70662
|
-
|
|
70663
|
-
|
|
70664
|
-
|
|
70665
|
-
}
|
|
70666
|
-
}
|
|
70667
|
-
if (
|
|
70668
|
-
|
|
70669
|
-
event: "mls_welcome",
|
|
70670
|
-
data: {
|
|
70671
|
-
target_device_id: req.requesting_device_id,
|
|
70672
|
-
group_id: convEntry.mlsGroupId,
|
|
70673
|
-
conversation_id: convId,
|
|
70674
|
-
payload: Buffer.from(welcome).toString("hex")
|
|
70675
|
-
}
|
|
70676
|
-
}));
|
|
71359
|
+
const done = await this._fulfilWelcomeRequests({
|
|
71360
|
+
mlsGroup,
|
|
71361
|
+
mlsGroupId: convEntry.mlsGroupId,
|
|
71362
|
+
label: `conv ${convId.slice(0, 8)}`,
|
|
71363
|
+
welcomeFields: { conversation_id: convId },
|
|
71364
|
+
targets: [{
|
|
71365
|
+
requestId: req.id,
|
|
71366
|
+
deviceId: req.requesting_device_id,
|
|
71367
|
+
keyPackage: memberKp
|
|
71368
|
+
}]
|
|
71369
|
+
});
|
|
71370
|
+
if (done > 0) {
|
|
71371
|
+
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in conv ${convId.slice(0, 8)}`);
|
|
70677
71372
|
}
|
|
70678
|
-
await fetch(
|
|
70679
|
-
`${this.config.apiUrl}/api/v1/mls/groups/${convEntry.mlsGroupId}/fulfill-welcome`,
|
|
70680
|
-
{
|
|
70681
|
-
method: "POST",
|
|
70682
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70683
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70684
|
-
}
|
|
70685
|
-
);
|
|
70686
|
-
await saveMlsState(this.config.dataDir, convEntry.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70687
|
-
console.log(`[SecureChannel] Fulfilled Welcome for ${req.requesting_device_id.slice(0, 8)} in conv ${convId.slice(0, 8)}`);
|
|
70688
71373
|
} catch (fulfillErr) {
|
|
70689
71374
|
console.warn(`[SecureChannel] Conv Welcome fulfill failed for ${req.requesting_device_id.slice(0, 8)}:`, fulfillErr);
|
|
70690
71375
|
}
|
|
@@ -70865,12 +71550,13 @@ ${messageText}`;
|
|
|
70865
71550
|
} catch (distErr) {
|
|
70866
71551
|
console.warn(`[SecureChannel] A2A distribute network error:`, distErr);
|
|
70867
71552
|
}
|
|
70868
|
-
if (!distributed && this.
|
|
70869
|
-
|
|
71553
|
+
if (!distributed && this._wsIsOpen()) {
|
|
71554
|
+
const a2aLabel = `A2A ${chId.slice(0, 8)}`;
|
|
71555
|
+
const commitSent = await this._sendFrameConfirmed({
|
|
70870
71556
|
event: "mls_commit",
|
|
70871
71557
|
data: { group_id: chState.mlsGroupId, epoch: Number(mlsGroup.epoch), payload: commitHex }
|
|
70872
|
-
})
|
|
70873
|
-
this.
|
|
71558
|
+
}, `${a2aLabel} commit`);
|
|
71559
|
+
const welcomeSent = commitSent && await this._sendFrameConfirmed({
|
|
70874
71560
|
event: "mls_welcome",
|
|
70875
71561
|
data: {
|
|
70876
71562
|
target_device_id: req.requesting_device_id,
|
|
@@ -70878,7 +71564,11 @@ ${messageText}`;
|
|
|
70878
71564
|
a2a_channel_id: chId,
|
|
70879
71565
|
payload: welcomeHex
|
|
70880
71566
|
}
|
|
70881
|
-
})
|
|
71567
|
+
}, `${a2aLabel} welcome`);
|
|
71568
|
+
if (!welcomeSent) {
|
|
71569
|
+
console.warn(`[SecureChannel] ${a2aLabel}: Welcome frames were NOT written \u2014 leaving the request pending`);
|
|
71570
|
+
continue;
|
|
71571
|
+
}
|
|
70882
71572
|
try {
|
|
70883
71573
|
await fetch(
|
|
70884
71574
|
`${this.config.apiUrl}/api/v1/mls/groups/${chState.mlsGroupId}/distribute`,
|
|
@@ -70901,16 +71591,15 @@ ${messageText}`;
|
|
|
70901
71591
|
console.warn(`[SecureChannel] Cannot deliver Welcome \u2014 HTTP distribute and WS both unavailable`);
|
|
70902
71592
|
continue;
|
|
70903
71593
|
}
|
|
70904
|
-
await
|
|
70905
|
-
|
|
70906
|
-
|
|
70907
|
-
|
|
70908
|
-
headers: { Authorization: `Bearer ${this._deviceJwt}`, "Content-Type": "application/json" },
|
|
70909
|
-
body: JSON.stringify({ request_id: req.id })
|
|
70910
|
-
}
|
|
71594
|
+
const a2aFulfilled = await this._postFulfilWelcome(
|
|
71595
|
+
chState.mlsGroupId,
|
|
71596
|
+
req.id,
|
|
71597
|
+
`A2A ${chId.slice(0, 8)}`
|
|
70911
71598
|
);
|
|
70912
71599
|
await saveMlsState(this.config.dataDir, chState.mlsGroupId, JSON.stringify(mlsGroup.exportState()));
|
|
70913
|
-
|
|
71600
|
+
if (a2aFulfilled) {
|
|
71601
|
+
console.log(`[SecureChannel] Fulfilled A2A Welcome for ${req.requesting_device_id.slice(0, 8)} in channel ${chId.slice(0, 8)}`);
|
|
71602
|
+
}
|
|
70914
71603
|
} catch (fulfillErr) {
|
|
70915
71604
|
console.log(`[SecureChannel] A2A Welcome fulfill failed: ${fulfillErr instanceof Error ? fulfillErr.message : String(fulfillErr)}`);
|
|
70916
71605
|
}
|
|
@@ -71084,12 +71773,12 @@ ${messageText}`;
|
|
|
71084
71773
|
console.log(`[SecureChannel] MLS sync: re-join needed for group ${groupId?.slice(0, 8)}`);
|
|
71085
71774
|
if (groupId) {
|
|
71086
71775
|
await deleteMlsState(this.config.dataDir, groupId);
|
|
71087
|
-
|
|
71088
|
-
|
|
71089
|
-
|
|
71090
|
-
|
|
71091
|
-
}
|
|
71776
|
+
const target = this._resolveGroupFamily(groupId);
|
|
71777
|
+
if (target) {
|
|
71778
|
+
this._mlsGroups.delete(target.managerKey);
|
|
71779
|
+
console.log(`[SecureChannel] MLS sync rejoin: dropped local state for ${target.label}`);
|
|
71092
71780
|
}
|
|
71781
|
+
void this._requestWelcomeSelfHeal(groupId);
|
|
71093
71782
|
}
|
|
71094
71783
|
} else if (action === "replay_complete") {
|
|
71095
71784
|
console.log(`[SecureChannel] MLS sync complete for group ${groupId?.slice(0, 8)} (${data.count} messages replayed)`);
|
|
@@ -71101,6 +71790,7 @@ ${messageText}`;
|
|
|
71101
71790
|
*/
|
|
71102
71791
|
async _handleA2AMessageMLS(data) {
|
|
71103
71792
|
const groupId = data.group_id;
|
|
71793
|
+
this._noteGroupEpoch(groupId, data.epoch);
|
|
71104
71794
|
const channelId = data.channel_id ?? data.a2a_channel_id;
|
|
71105
71795
|
let a2aChannelId = channelId;
|
|
71106
71796
|
if (!a2aChannelId) {
|
|
@@ -98324,7 +99014,7 @@ var init_index = __esm2({
|
|
|
98324
99014
|
init_skill_invoker();
|
|
98325
99015
|
await init_skill_telemetry();
|
|
98326
99016
|
await init_policy_enforcer();
|
|
98327
|
-
VERSION = true ? "0.23.
|
|
99017
|
+
VERSION = true ? "0.23.27" : "0.0.0-dev";
|
|
98328
99018
|
}
|
|
98329
99019
|
});
|
|
98330
99020
|
await init_index();
|
|
@@ -98965,6 +99655,40 @@ async function startRoomMcpServer(opts) {
|
|
|
98965
99655
|
};
|
|
98966
99656
|
}
|
|
98967
99657
|
|
|
99658
|
+
// src/service/self-uninstall.ts
|
|
99659
|
+
init_subcommand();
|
|
99660
|
+
init_backend();
|
|
99661
|
+
init_log();
|
|
99662
|
+
var DEVICE_IS_GONE = "device_revoked";
|
|
99663
|
+
function selfUninstallOnTerminal(reason, env, deps = {}) {
|
|
99664
|
+
if (reason !== DEVICE_IS_GONE) return false;
|
|
99665
|
+
const log = deps.log ?? ((m2) => logLine("[grok-bridge] " + m2));
|
|
99666
|
+
let label;
|
|
99667
|
+
try {
|
|
99668
|
+
label = labelFor(env);
|
|
99669
|
+
} catch (e) {
|
|
99670
|
+
log(
|
|
99671
|
+
`device was revoked, but ${e.message} \u2014 cannot tell which service to remove. Remove it by hand with: AV_AGENT_NAME=<agent-name> agentvault-grok-bridge uninstall`
|
|
99672
|
+
);
|
|
99673
|
+
return false;
|
|
99674
|
+
}
|
|
99675
|
+
try {
|
|
99676
|
+
if (!deps.backend && process.platform !== "darwin" && process.platform !== "linux") return false;
|
|
99677
|
+
const backend = deps.backend ?? selectBackend(process.platform);
|
|
99678
|
+
if (!backend.status(label).loaded) return false;
|
|
99679
|
+
backend.uninstall(label);
|
|
99680
|
+
log(
|
|
99681
|
+
`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.`
|
|
99682
|
+
);
|
|
99683
|
+
return true;
|
|
99684
|
+
} catch (e) {
|
|
99685
|
+
log(
|
|
99686
|
+
`device was revoked, but removing the service ${label} failed: ${e.message}. Remove it by hand with: AV_AGENT_NAME=<agent-name> agentvault-grok-bridge uninstall`
|
|
99687
|
+
);
|
|
99688
|
+
return false;
|
|
99689
|
+
}
|
|
99690
|
+
}
|
|
99691
|
+
|
|
98968
99692
|
// src/bridge.ts
|
|
98969
99693
|
var RoomHushState = class {
|
|
98970
99694
|
until = /* @__PURE__ */ new Map();
|
|
@@ -99020,11 +99744,13 @@ var ActiveTarget = class {
|
|
|
99020
99744
|
function attachLifecycle2(channel, opts = {}) {
|
|
99021
99745
|
const log = opts.log ?? (() => {
|
|
99022
99746
|
});
|
|
99747
|
+
const selfUninstall = opts.selfUninstall ?? selfUninstallOnTerminal;
|
|
99023
99748
|
const onTerminal = opts.onTerminal ?? ((_reason, o2) => process.exit(o2.restart ? 1 : 0));
|
|
99024
99749
|
const terminal = (reason, restart) => {
|
|
99025
99750
|
log(
|
|
99026
99751
|
`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.`
|
|
99027
99752
|
);
|
|
99753
|
+
selfUninstall(reason, process.env);
|
|
99028
99754
|
onTerminal(reason, { restart });
|
|
99029
99755
|
};
|
|
99030
99756
|
channel.on("auth_failed", (e) => terminal(e.reason, false));
|
|
@@ -99178,6 +99904,10 @@ var shutdown = async (code = 0) => {
|
|
|
99178
99904
|
}
|
|
99179
99905
|
};
|
|
99180
99906
|
async function main() {
|
|
99907
|
+
const { maybeRunDoctorSubcommand: maybeRunDoctorSubcommand2 } = await Promise.resolve().then(() => (init_doctor_cli(), doctor_cli_exports));
|
|
99908
|
+
if (await maybeRunDoctorSubcommand2(process.argv)) {
|
|
99909
|
+
process.exit(process.exitCode ?? 0);
|
|
99910
|
+
}
|
|
99181
99911
|
const { maybeRunServiceSubcommand: maybeRunServiceSubcommand2 } = await Promise.resolve().then(() => (init_subcommand(), subcommand_exports));
|
|
99182
99912
|
if (maybeRunServiceSubcommand2(process.argv, process.env, {
|
|
99183
99913
|
log: (m2) => logLine("[grok-bridge] " + m2)
|
|
@@ -99187,7 +99917,7 @@ async function main() {
|
|
|
99187
99917
|
const info = parseInfoFlags(process.argv.slice(2));
|
|
99188
99918
|
if (info.showVersion || info.showHelp) {
|
|
99189
99919
|
const version2 = buildClientVersion2(
|
|
99190
|
-
true ? "0.
|
|
99920
|
+
true ? "0.4.1" : void 0
|
|
99191
99921
|
);
|
|
99192
99922
|
if (info.showVersion) printLine(version2);
|
|
99193
99923
|
if (info.showHelp) {
|
|
@@ -99226,11 +99956,6 @@ async function main() {
|
|
|
99226
99956
|
process.exit(0);
|
|
99227
99957
|
}
|
|
99228
99958
|
const cfg = loadConfig(process.env, process.argv.slice(2));
|
|
99229
|
-
if (cfg.installAfterEnroll) {
|
|
99230
|
-
assertInstallableEntrypoint(process.argv[1]);
|
|
99231
|
-
const { selectBackend: selectBackend2 } = await Promise.resolve().then(() => (init_backend(), backend_exports));
|
|
99232
|
-
selectBackend2(process.platform);
|
|
99233
|
-
}
|
|
99234
99959
|
mkdirSync6(cfg.workspaceDir, { recursive: true });
|
|
99235
99960
|
mkdirSync6(cfg.fenceHome, { recursive: true });
|
|
99236
99961
|
seedFenceCredential({
|
|
@@ -99245,7 +99970,7 @@ async function main() {
|
|
|
99245
99970
|
agentName: cfg.agentName,
|
|
99246
99971
|
platform: "node",
|
|
99247
99972
|
clientVersion: buildClientVersion2(
|
|
99248
|
-
true ? "0.
|
|
99973
|
+
true ? "0.4.1" : void 0
|
|
99249
99974
|
)
|
|
99250
99975
|
});
|
|
99251
99976
|
channelRef = channel;
|
|
@@ -99336,23 +100061,6 @@ async function main() {
|
|
|
99336
100061
|
logLine("[grok-bridge] enrolled \u2014 pending invite consumed and removed");
|
|
99337
100062
|
}
|
|
99338
100063
|
});
|
|
99339
|
-
if (cfg.installAfterEnroll) {
|
|
99340
|
-
const { installOnReady: installOnReady2 } = await Promise.resolve().then(() => (init_install_on_ready(), install_on_ready_exports));
|
|
99341
|
-
installOnReady2(channel, cfg, {
|
|
99342
|
-
entrypoint: process.argv[1],
|
|
99343
|
-
env: process.env,
|
|
99344
|
-
// FOUND LIVE 2026-08-16: the owner's activation greeting is usually already
|
|
99345
|
-
// being answered when `ready` fires, and reaping mid-turn killed the reply
|
|
99346
|
-
// for good — the message was acked, so the service never saw it again.
|
|
99347
|
-
// GrokSession.idle() resolves when the queue drains.
|
|
99348
|
-
waitForIdle: () => session.idle(),
|
|
99349
|
-
// The one shared teardown. `exiting` is deliberately NOT set here: Ctrl-C
|
|
99350
|
-
// must keep working for the whole multi-second launchctl window.
|
|
99351
|
-
reap: reapAll,
|
|
99352
|
-
exit: (code) => process.exit(code),
|
|
99353
|
-
log: (m2) => logLine("[grok-bridge] " + m2)
|
|
99354
|
-
});
|
|
99355
|
-
}
|
|
99356
100064
|
await channel.start();
|
|
99357
100065
|
}
|
|
99358
100066
|
main().catch(async (err) => {
|