@agentvault/claude-bridge 0.7.4 → 0.7.6
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 +3 -0
- package/dist/index.js +309 -66
- package/package.json +2 -2
package/dist/bridge.d.ts
CHANGED
|
@@ -34,6 +34,9 @@ export interface ArmingSnapshot {
|
|
|
34
34
|
* its ABSENCE is how we identify a true owner↔agent 1:1 DM. */
|
|
35
35
|
export interface MessageMeta {
|
|
36
36
|
roomId?: string;
|
|
37
|
+
/** #621 — set by SecureChannel when the message decrypted under the owner's
|
|
38
|
+
* 1:1 material. Gates the full-access DM lane; see the `message` handler. */
|
|
39
|
+
ownerDm1to1?: boolean;
|
|
37
40
|
}
|
|
38
41
|
/** #392 room hush (advisory) — emitted by SecureChannel from the backend's
|
|
39
42
|
* `room_hushed` event. ``hushedUntil`` is an ISO string, or null when cleared. */
|
package/dist/index.js
CHANGED
|
@@ -452,12 +452,13 @@ import { join as join11 } from "node:path";
|
|
|
452
452
|
|
|
453
453
|
// ../plugin/dist/index.js
|
|
454
454
|
import * as nc from "node:crypto";
|
|
455
|
-
import {
|
|
455
|
+
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
456
|
+
import { readFile, rm } from "node:fs/promises";
|
|
456
457
|
import { join } from "node:path";
|
|
457
|
-
import {
|
|
458
|
+
import { readFile as readFile2, rename, rm as rm2 } from "node:fs/promises";
|
|
458
459
|
import { join as join2 } from "node:path";
|
|
459
460
|
import { Readable } from "node:stream";
|
|
460
|
-
import { readdir, readFile as readFile3, writeFile as
|
|
461
|
+
import { readdir, readFile as readFile3, writeFile as writeFile2, rename as rename2, stat, unlink, mkdir as mkdir2 } from "node:fs/promises";
|
|
461
462
|
import { join as join3 } from "node:path";
|
|
462
463
|
import { randomUUID } from "node:crypto";
|
|
463
464
|
import { EventEmitter } from "node:events";
|
|
@@ -465,7 +466,7 @@ import { createServer } from "node:http";
|
|
|
465
466
|
import { randomUUID as randomUUID2, createHash } from "node:crypto";
|
|
466
467
|
import { readFileSync } from "node:fs";
|
|
467
468
|
import { homedir as osHomedir } from "node:os";
|
|
468
|
-
import { writeFile as
|
|
469
|
+
import { writeFile as writeFile3, mkdir as mkdir3 } from "node:fs/promises";
|
|
469
470
|
import { join as join4 } from "node:path";
|
|
470
471
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
471
472
|
import WebSocket2 from "ws";
|
|
@@ -63664,16 +63665,39 @@ var init_dist = __esm2({
|
|
|
63664
63665
|
init_mls_delivery_order();
|
|
63665
63666
|
}
|
|
63666
63667
|
});
|
|
63668
|
+
async function ensureSecureDir(dir) {
|
|
63669
|
+
await mkdir(dir, { recursive: true, mode: DIR_MODE });
|
|
63670
|
+
try {
|
|
63671
|
+
await chmod(dir, DIR_MODE);
|
|
63672
|
+
} catch {
|
|
63673
|
+
}
|
|
63674
|
+
}
|
|
63675
|
+
async function writeSecureFile(filePath, data, flag) {
|
|
63676
|
+
await writeFile(filePath, data, { encoding: "utf-8", mode: FILE_MODE, ...flag ? { flag } : {} });
|
|
63677
|
+
try {
|
|
63678
|
+
await chmod(filePath, FILE_MODE);
|
|
63679
|
+
} catch {
|
|
63680
|
+
}
|
|
63681
|
+
}
|
|
63682
|
+
var DIR_MODE;
|
|
63683
|
+
var FILE_MODE;
|
|
63684
|
+
var init_secure_file = __esm2({
|
|
63685
|
+
"src/secure-file.ts"() {
|
|
63686
|
+
"use strict";
|
|
63687
|
+
DIR_MODE = 448;
|
|
63688
|
+
FILE_MODE = 384;
|
|
63689
|
+
}
|
|
63690
|
+
});
|
|
63667
63691
|
function syncLockPath(dataDir, channelId) {
|
|
63668
63692
|
const safe = channelId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
63669
63693
|
return join(dataDir, `mls-a2a-sync-${safe}.lock`);
|
|
63670
63694
|
}
|
|
63671
63695
|
async function acquireA2ASyncLock(dataDir, channelId) {
|
|
63672
|
-
await
|
|
63696
|
+
await ensureSecureDir(dataDir);
|
|
63673
63697
|
const lockFile = syncLockPath(dataDir, channelId);
|
|
63674
63698
|
const content = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
|
|
63675
63699
|
try {
|
|
63676
|
-
await
|
|
63700
|
+
await writeSecureFile(lockFile, content, "wx");
|
|
63677
63701
|
return true;
|
|
63678
63702
|
} catch (err) {
|
|
63679
63703
|
if (err.code !== "EEXIST") throw err;
|
|
@@ -63684,7 +63708,7 @@ async function acquireA2ASyncLock(dataDir, channelId) {
|
|
|
63684
63708
|
await rm(lockFile).catch(() => {
|
|
63685
63709
|
});
|
|
63686
63710
|
try {
|
|
63687
|
-
await
|
|
63711
|
+
await writeSecureFile(lockFile, content, "wx");
|
|
63688
63712
|
return true;
|
|
63689
63713
|
} catch {
|
|
63690
63714
|
return false;
|
|
@@ -63714,7 +63738,7 @@ async function hasPendingWelcome(dataDir, groupId) {
|
|
|
63714
63738
|
}
|
|
63715
63739
|
}
|
|
63716
63740
|
async function savePendingKpBundle(dataDir, groupId, kp, serializePublicFn) {
|
|
63717
|
-
await
|
|
63741
|
+
await ensureSecureDir(dataDir);
|
|
63718
63742
|
const serialized = {
|
|
63719
63743
|
publicPackageBytes: Buffer.from(serializePublicFn(kp.publicPackage)).toString("base64"),
|
|
63720
63744
|
privatePackage: {
|
|
@@ -63723,7 +63747,7 @@ async function savePendingKpBundle(dataDir, groupId, kp, serializePublicFn) {
|
|
|
63723
63747
|
signaturePrivateKey: Buffer.from(kp.privatePackage.signaturePrivateKey).toString("base64")
|
|
63724
63748
|
}
|
|
63725
63749
|
};
|
|
63726
|
-
await
|
|
63750
|
+
await writeSecureFile(pendingKpPath(dataDir, groupId), JSON.stringify(serialized));
|
|
63727
63751
|
}
|
|
63728
63752
|
async function loadPendingKpBundle(dataDir, groupId) {
|
|
63729
63753
|
try {
|
|
@@ -63748,9 +63772,9 @@ async function clearPendingWelcome(dataDir, groupId) {
|
|
|
63748
63772
|
}
|
|
63749
63773
|
}
|
|
63750
63774
|
async function saveMlsState(dataDir, groupId, state) {
|
|
63751
|
-
await
|
|
63775
|
+
await ensureSecureDir(dataDir);
|
|
63752
63776
|
const filePath = join(dataDir, mlsFileName(groupId));
|
|
63753
|
-
await
|
|
63777
|
+
await writeSecureFile(filePath, state);
|
|
63754
63778
|
}
|
|
63755
63779
|
async function loadMlsState(dataDir, groupId) {
|
|
63756
63780
|
try {
|
|
@@ -63767,15 +63791,12 @@ async function deleteMlsState(dataDir, groupId) {
|
|
|
63767
63791
|
} catch {
|
|
63768
63792
|
}
|
|
63769
63793
|
}
|
|
63770
|
-
var FILE_MODE;
|
|
63771
|
-
var DIR_MODE;
|
|
63772
63794
|
var SYNC_LOCK_STALE_MS;
|
|
63773
63795
|
var pendingKpPath;
|
|
63774
63796
|
var init_mls_state = __esm2({
|
|
63775
63797
|
"src/mls-state.ts"() {
|
|
63776
63798
|
"use strict";
|
|
63777
|
-
|
|
63778
|
-
DIR_MODE = 448;
|
|
63799
|
+
init_secure_file();
|
|
63779
63800
|
SYNC_LOCK_STALE_MS = 5 * 60 * 1e3;
|
|
63780
63801
|
pendingKpPath = (dataDir, groupId) => join(dataDir, `mls-kp-pending-${groupId.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`);
|
|
63781
63802
|
}
|
|
@@ -63926,9 +63947,9 @@ var init_crypto_helpers = __esm2({
|
|
|
63926
63947
|
}
|
|
63927
63948
|
});
|
|
63928
63949
|
async function saveState(dataDir, state) {
|
|
63929
|
-
await
|
|
63950
|
+
await ensureSecureDir(dataDir);
|
|
63930
63951
|
const filePath = join2(dataDir, STATE_FILE);
|
|
63931
|
-
await
|
|
63952
|
+
await writeSecureFile(filePath, JSON.stringify(state, null, 2));
|
|
63932
63953
|
try {
|
|
63933
63954
|
await rm2(join2(dataDir, LEGACY_STATE_FILE));
|
|
63934
63955
|
} catch {
|
|
@@ -63945,8 +63966,8 @@ async function loadState(dataDir) {
|
|
|
63945
63966
|
try {
|
|
63946
63967
|
const raw = await readFile2(legacyPath, "utf-8");
|
|
63947
63968
|
const parsed = JSON.parse(raw);
|
|
63948
|
-
await
|
|
63949
|
-
await
|
|
63969
|
+
await ensureSecureDir(dataDir);
|
|
63970
|
+
await writeSecureFile(filePath, JSON.stringify(parsed, null, 2));
|
|
63950
63971
|
await rm2(legacyPath);
|
|
63951
63972
|
return parsed;
|
|
63952
63973
|
} catch {
|
|
@@ -63969,7 +63990,7 @@ async function backupState(dataDir) {
|
|
|
63969
63990
|
const tmp = join2(dataDir, `${STATE_FILE}.bak.tmp`);
|
|
63970
63991
|
try {
|
|
63971
63992
|
const data = await readFile2(src, "utf-8");
|
|
63972
|
-
await
|
|
63993
|
+
await writeSecureFile(tmp, data);
|
|
63973
63994
|
await rename(tmp, bak);
|
|
63974
63995
|
} catch {
|
|
63975
63996
|
}
|
|
@@ -63984,8 +64005,8 @@ async function restoreState(dataDir) {
|
|
|
63984
64005
|
if (!parsed || !parsed.deviceId || !parsed.deviceJwt || !parsed.sessions || Object.keys(parsed.sessions).length === 0) {
|
|
63985
64006
|
return false;
|
|
63986
64007
|
}
|
|
63987
|
-
await
|
|
63988
|
-
await
|
|
64008
|
+
await ensureSecureDir(dataDir);
|
|
64009
|
+
await writeSecureFile(join2(dataDir, STATE_FILE), data);
|
|
63989
64010
|
return true;
|
|
63990
64011
|
} catch {
|
|
63991
64012
|
return false;
|
|
@@ -64048,16 +64069,13 @@ async function clearState(dataDir) {
|
|
|
64048
64069
|
}
|
|
64049
64070
|
var STATE_FILE;
|
|
64050
64071
|
var LEGACY_STATE_FILE;
|
|
64051
|
-
var DIR_MODE2;
|
|
64052
|
-
var FILE_MODE2;
|
|
64053
64072
|
var init_state = __esm2({
|
|
64054
64073
|
async "src/state.ts"() {
|
|
64055
64074
|
"use strict";
|
|
64056
64075
|
await init_dist();
|
|
64076
|
+
init_secure_file();
|
|
64057
64077
|
STATE_FILE = "agentvault.json";
|
|
64058
64078
|
LEGACY_STATE_FILE = "secure-channel.json";
|
|
64059
|
-
DIR_MODE2 = 448;
|
|
64060
|
-
FILE_MODE2 = 384;
|
|
64061
64079
|
}
|
|
64062
64080
|
});
|
|
64063
64081
|
async function enrollDevice(apiUrl, inviteToken, identityPkHex, ephemeralPkHex, proofHex, platform) {
|
|
@@ -64486,11 +64504,11 @@ async function handleWorkspaceUpload(data, workspaceDir) {
|
|
|
64486
64504
|
if (!verified) {
|
|
64487
64505
|
return { status: "error", error: "Invalid signature \u2014 file may have been tampered with" };
|
|
64488
64506
|
}
|
|
64489
|
-
await
|
|
64507
|
+
await mkdir2(workspaceDir, { recursive: true });
|
|
64490
64508
|
const targetPath = join3(workspaceDir, data.filename);
|
|
64491
64509
|
const tempPath = join3(workspaceDir, `.tmp_${randomUUID()}_${data.filename}`);
|
|
64492
64510
|
try {
|
|
64493
|
-
await
|
|
64511
|
+
await writeFile2(tempPath, data.content, "utf-8");
|
|
64494
64512
|
await rename2(tempPath, targetPath);
|
|
64495
64513
|
} catch (err) {
|
|
64496
64514
|
try {
|
|
@@ -64616,6 +64634,8 @@ var OUTBOUND_DEDUPE_WINDOW_MS;
|
|
|
64616
64634
|
var OUTBOUND_DEDUPE_MAX_SUPPRESS_MS;
|
|
64617
64635
|
var OUTBOUND_DEDUPE_MAX;
|
|
64618
64636
|
var OUTBOUND_DEDUPE_TYPES;
|
|
64637
|
+
var RETRY_CANDIDATE_TTL_MS;
|
|
64638
|
+
var RETRY_CANDIDATE_MAX;
|
|
64619
64639
|
var SecureChannel;
|
|
64620
64640
|
var init_channel = __esm2({
|
|
64621
64641
|
async "src/channel.ts"() {
|
|
@@ -64653,6 +64673,8 @@ var init_channel = __esm2({
|
|
|
64653
64673
|
OUTBOUND_DEDUPE_MAX_SUPPRESS_MS = 15e3;
|
|
64654
64674
|
OUTBOUND_DEDUPE_MAX = 256;
|
|
64655
64675
|
OUTBOUND_DEDUPE_TYPES = /* @__PURE__ */ new Set(["text", "status_alert", "artifact", "attachment"]);
|
|
64676
|
+
RETRY_CANDIDATE_TTL_MS = 6e4;
|
|
64677
|
+
RETRY_CANDIDATE_MAX = 32;
|
|
64656
64678
|
SecureChannel = class _SecureChannel extends EventEmitter {
|
|
64657
64679
|
constructor(config22) {
|
|
64658
64680
|
super();
|
|
@@ -64772,6 +64794,20 @@ var init_channel = __esm2({
|
|
|
64772
64794
|
* single delivery. In-memory only (short window; no cross-restart persistence needed).
|
|
64773
64795
|
*/
|
|
64774
64796
|
_recentDeliveries = /* @__PURE__ */ new Map();
|
|
64797
|
+
/**
|
|
64798
|
+
* Last outbound plaintext per MLS group id (#732), so a message refused with
|
|
64799
|
+
* `unknown group` can be resent once the map is reconciled.
|
|
64800
|
+
*
|
|
64801
|
+
* PLAINTEXT, not the frame: the refused frame's `payload` is ciphertext
|
|
64802
|
+
* encrypted to a group that no longer exists, so replaying those bytes into
|
|
64803
|
+
* the replacement group would store something nobody can decrypt. The retry
|
|
64804
|
+
* has to encrypt again, which means keeping what was encrypted.
|
|
64805
|
+
*
|
|
64806
|
+
* In-memory only, TTL-bounded, and consumed on first use — this exists for
|
|
64807
|
+
* the ~50ms between a frame reaching the socket and the server refusing it,
|
|
64808
|
+
* not as a durable outbox (that is #688's dead-letter).
|
|
64809
|
+
*/
|
|
64810
|
+
_retryCandidates = /* @__PURE__ */ new Map();
|
|
64775
64811
|
_scanEngine = null;
|
|
64776
64812
|
_scanRuleSetVersion = 0;
|
|
64777
64813
|
_telemetryReporter = null;
|
|
@@ -64871,13 +64907,21 @@ var init_channel = __esm2({
|
|
|
64871
64907
|
* Non-fatal: a transient network error or 5xx keeps the current JWT and
|
|
64872
64908
|
* relies on the next reconnect. Terminal failures (401/403) surface via
|
|
64873
64909
|
* the `auth_failed` event so callers can prompt the user to recover.
|
|
64910
|
+
*
|
|
64911
|
+
* Returns TRUE when the credentials are terminally dead. #743: the caller
|
|
64912
|
+
* MUST NOT go on to open a WebSocket in that case — the server has already
|
|
64913
|
+
* told us this credential can never be served, and connecting anyway is what
|
|
64914
|
+
* produced `ws_guard action=denylist` on repeat in prod (device 0e87ff50, a
|
|
64915
|
+
* hard-deleted device, ~12/hour). The verdict is returned explicitly rather
|
|
64916
|
+
* than read back off `_authFailedThisSession`, whose lifetime is reset per
|
|
64917
|
+
* connect and is easy to get subtly wrong.
|
|
64874
64918
|
*/
|
|
64875
64919
|
async _maybeReissueDeviceJwt() {
|
|
64876
64920
|
const jwt22 = this._persisted?.deviceJwt ?? this._deviceJwt;
|
|
64877
|
-
if (!jwt22) return;
|
|
64921
|
+
if (!jwt22) return false;
|
|
64878
64922
|
const exp = this._decodeJwtExp(jwt22);
|
|
64879
64923
|
const nowSec = Math.floor(Date.now() / 1e3);
|
|
64880
|
-
if (exp - nowSec >= 30 * 86400) return;
|
|
64924
|
+
if (exp - nowSec >= 30 * 86400) return false;
|
|
64881
64925
|
try {
|
|
64882
64926
|
const resp = await fetch(`${this.config.apiUrl}/api/v1/auth/reissue`, {
|
|
64883
64927
|
method: "POST",
|
|
@@ -64897,7 +64941,7 @@ var init_channel = __esm2({
|
|
|
64897
64941
|
} else {
|
|
64898
64942
|
console.warn("[SecureChannel] reissue returned 200 but no device_jwt in response");
|
|
64899
64943
|
}
|
|
64900
|
-
return;
|
|
64944
|
+
return false;
|
|
64901
64945
|
}
|
|
64902
64946
|
if (resp.status === 401 || resp.status === 403) {
|
|
64903
64947
|
console.warn(`[SecureChannel] reissue rejected (${resp.status}); credentials need attention`);
|
|
@@ -64905,12 +64949,13 @@ var init_channel = __esm2({
|
|
|
64905
64949
|
const reason = resp.status === 401 ? "device_jwt_expired" : "device_revoked";
|
|
64906
64950
|
await this._postAuthFailedToBackend(reason);
|
|
64907
64951
|
this.emit("auth_failed", { reason });
|
|
64908
|
-
return;
|
|
64952
|
+
return true;
|
|
64909
64953
|
}
|
|
64910
64954
|
console.warn(`[SecureChannel] reissue ${resp.status}; keeping current jwt`);
|
|
64911
64955
|
} catch (err) {
|
|
64912
64956
|
console.warn("[SecureChannel] reissue network error; keeping current jwt:", err);
|
|
64913
64957
|
}
|
|
64958
|
+
return false;
|
|
64914
64959
|
}
|
|
64915
64960
|
/**
|
|
64916
64961
|
* Wraps `fetch` for authenticated AV REST endpoints with reactive device-JWT
|
|
@@ -65396,7 +65441,9 @@ var init_channel = __esm2({
|
|
|
65396
65441
|
}
|
|
65397
65442
|
scanStatus = scanResult.status;
|
|
65398
65443
|
}
|
|
65399
|
-
|
|
65444
|
+
if (!options?.isResend) {
|
|
65445
|
+
this._appendHistory("agent", plaintext, topicId);
|
|
65446
|
+
}
|
|
65400
65447
|
const roomConvIds = /* @__PURE__ */ new Set();
|
|
65401
65448
|
if (this._persisted?.rooms) {
|
|
65402
65449
|
for (const room of Object.values(this._persisted.rooms)) {
|
|
@@ -65410,6 +65457,7 @@ var init_channel = __esm2({
|
|
|
65410
65457
|
let sentCount = 0;
|
|
65411
65458
|
const pendingWsSends = [];
|
|
65412
65459
|
const sentSharedGroupIds = /* @__PURE__ */ new Set();
|
|
65460
|
+
const addressedMlsGroupIds = [];
|
|
65413
65461
|
if (this._persisted?.mlsGroups && this._state === "ready" && this._ws) {
|
|
65414
65462
|
const targetSharedGid = targetConvId ? this._sessionGroupIds?.get(targetConvId) : void 0;
|
|
65415
65463
|
for (const [gid, entry] of Object.entries(this._persisted.mlsGroups)) {
|
|
@@ -65435,6 +65483,7 @@ var init_channel = __esm2({
|
|
|
65435
65483
|
if (this._persisted?.hubAddress) payload.hub_address = this._persisted.hubAddress;
|
|
65436
65484
|
if (this._persisted?.hubId) payload.sender_hub_id = this._persisted.hubId;
|
|
65437
65485
|
pendingWsSends.push(JSON.stringify({ event: "message_mls", data: payload }));
|
|
65486
|
+
addressedMlsGroupIds.push(entry.mlsGroupId);
|
|
65438
65487
|
sentSharedGroupIds.add(gid);
|
|
65439
65488
|
sentCount++;
|
|
65440
65489
|
console.log(`[SecureChannel] Shared MLS group send for group ${gid.slice(0, 8)} (${entry.mlsGroupId.slice(0, 8)})`);
|
|
@@ -65487,6 +65536,7 @@ var init_channel = __esm2({
|
|
|
65487
65536
|
data: payload
|
|
65488
65537
|
})
|
|
65489
65538
|
);
|
|
65539
|
+
addressedMlsGroupIds.push(mlsGroupId);
|
|
65490
65540
|
if (convGroupId) sentSharedGroupIds.add(convGroupId);
|
|
65491
65541
|
} else {
|
|
65492
65542
|
const encrypted = session.ratchet.encrypt(plaintext);
|
|
@@ -65572,6 +65622,7 @@ var init_channel = __esm2({
|
|
|
65572
65622
|
if (this._persisted?.hubAddress) payload.hub_address = this._persisted.hubAddress;
|
|
65573
65623
|
if (this._persisted?.hubId) payload.sender_hub_id = this._persisted.hubId;
|
|
65574
65624
|
pendingWsSends.push(JSON.stringify({ event: "message_mls", data: payload }));
|
|
65625
|
+
addressedMlsGroupIds.push(resolvedMlsGroupId);
|
|
65575
65626
|
sentCount++;
|
|
65576
65627
|
if (mlsOnlyConvGroupId) sentSharedGroupIds.add(mlsOnlyConvGroupId);
|
|
65577
65628
|
console.log(`[SecureChannel] MLS-only send for conv ${mlsConvId.slice(0, 8)} (no DR session)`);
|
|
@@ -65587,6 +65638,28 @@ var init_channel = __esm2({
|
|
|
65587
65638
|
for (const frame of pendingWsSends) {
|
|
65588
65639
|
this._ws.send(frame);
|
|
65589
65640
|
}
|
|
65641
|
+
if (!options?.isResend) {
|
|
65642
|
+
for (const mlsGroupId of addressedMlsGroupIds) {
|
|
65643
|
+
this._rememberRetryCandidate(mlsGroupId, plaintext, options);
|
|
65644
|
+
}
|
|
65645
|
+
}
|
|
65646
|
+
}
|
|
65647
|
+
/**
|
|
65648
|
+
* Retain one outbound plaintext against the MLS group it was sent to (#732).
|
|
65649
|
+
* Evicts expired entries, then the oldest, so the map cannot grow unbounded
|
|
65650
|
+
* on an agent that sends steadily and is never refused.
|
|
65651
|
+
*/
|
|
65652
|
+
_rememberRetryCandidate(mlsGroupId, plaintext, options) {
|
|
65653
|
+
const now = Date.now();
|
|
65654
|
+
for (const [k2, v22] of this._retryCandidates) {
|
|
65655
|
+
if (now - v22.ts > RETRY_CANDIDATE_TTL_MS) this._retryCandidates.delete(k2);
|
|
65656
|
+
}
|
|
65657
|
+
this._retryCandidates.set(mlsGroupId, { plaintext, options, ts: now });
|
|
65658
|
+
while (this._retryCandidates.size > RETRY_CANDIDATE_MAX) {
|
|
65659
|
+
const oldest = this._retryCandidates.keys().next().value;
|
|
65660
|
+
if (!oldest) break;
|
|
65661
|
+
this._retryCandidates.delete(oldest);
|
|
65662
|
+
}
|
|
65590
65663
|
}
|
|
65591
65664
|
/**
|
|
65592
65665
|
* Send a typing indicator to all owner devices.
|
|
@@ -65614,7 +65687,7 @@ var init_channel = __esm2({
|
|
|
65614
65687
|
*/
|
|
65615
65688
|
sendActivitySpan(spanData) {
|
|
65616
65689
|
if (!this._ws || this._ws.readyState !== WebSocket2.OPEN) return;
|
|
65617
|
-
const pluginVersion = true ? "0.23.
|
|
65690
|
+
const pluginVersion = true ? "0.23.18" : "0.0.0-dev";
|
|
65618
65691
|
const agentName = this.config.agentName ?? "Agent";
|
|
65619
65692
|
const resource = {
|
|
65620
65693
|
"service.name": "agentvault-agent",
|
|
@@ -65959,13 +66032,20 @@ var init_channel = __esm2({
|
|
|
65959
66032
|
* entry, then persist.
|
|
65960
66033
|
*/
|
|
65961
66034
|
async _handleServerError(payload) {
|
|
65962
|
-
|
|
66035
|
+
const detail = payload?.detail;
|
|
66036
|
+
if (detail !== "conversation deleted" && detail !== "unknown group") return;
|
|
65963
66037
|
const mlsGroupId = payload.group_id;
|
|
65964
66038
|
if (!mlsGroupId) return;
|
|
65965
66039
|
const groups = this._persisted?.mlsGroups;
|
|
65966
66040
|
if (!groups) return;
|
|
65967
66041
|
const gid = Object.keys(groups).find((k2) => groups[k2]?.mlsGroupId === mlsGroupId);
|
|
65968
66042
|
if (!gid) return;
|
|
66043
|
+
if (detail === "unknown group") {
|
|
66044
|
+
const changed = await this._reconcileConversationGroups(gid, mlsGroupId);
|
|
66045
|
+
if (changed) await this._retryAfterReconcile(mlsGroupId);
|
|
66046
|
+
return;
|
|
66047
|
+
}
|
|
66048
|
+
this._retryCandidates.delete(mlsGroupId);
|
|
65969
66049
|
try {
|
|
65970
66050
|
await deleteMlsState(this.config.dataDir, mlsGroupId);
|
|
65971
66051
|
} catch {
|
|
@@ -65978,6 +66058,120 @@ var init_channel = __esm2({
|
|
|
65978
66058
|
);
|
|
65979
66059
|
this.emit("dm_group_pruned", { conversationGroupId: gid, mlsGroupId });
|
|
65980
66060
|
}
|
|
66061
|
+
/**
|
|
66062
|
+
* Re-resolve the conversation→group map from the server (#719).
|
|
66063
|
+
*
|
|
66064
|
+
* `_persisted.conversationGroupIds` and `_persisted.groupId` are built ONCE,
|
|
66065
|
+
* in `_activate()`, from the activation response. `_activate` runs at
|
|
66066
|
+
* enrollment and never again, so when the owner deletes a conversation and a
|
|
66067
|
+
* new one is created the agent keeps addressing the OLD group forever.
|
|
66068
|
+
* Nothing reconciled it — not restart (the stale map is reloaded from disk),
|
|
66069
|
+
* not reconnect, not the heartbeat.
|
|
66070
|
+
*
|
|
66071
|
+
* wren, 2026-07-31: `groupId` 53798dd2 had no `mls_groups` row while three
|
|
66072
|
+
* ACTIVE conversations sat on 99faf707. Inbound worked, every reply was
|
|
66073
|
+
* refused `unknown group` and dropped. Silent since 2026-07-02 — the #460
|
|
66074
|
+
* fan-out was spraying all 12 known groups, so a live one always got hit.
|
|
66075
|
+
*
|
|
66076
|
+
* Best-effort and conservative, mirroring `_refreshRoomRoster`: on any
|
|
66077
|
+
* failure the existing map is kept. The prune is NARROW — only the group the
|
|
66078
|
+
* server named, and only once the server has positively reported an active
|
|
66079
|
+
* conversation on some other group. Absence of data is never treated as
|
|
66080
|
+
* evidence of deletion.
|
|
66081
|
+
*
|
|
66082
|
+
* Returns TRUE only when the PRIMARY group pointer actually moved, which is
|
|
66083
|
+
* the signal #732's resend is gated on. Deliberately narrower than "anything
|
|
66084
|
+
* in the map changed": if some other group went stale we cannot tell which
|
|
66085
|
+
* conversation replaced it, and delivering a reply to the wrong counterparty
|
|
66086
|
+
* is strictly worse than losing it.
|
|
66087
|
+
*/
|
|
66088
|
+
async _reconcileConversationGroups(staleGid, staleMlsGroupId) {
|
|
66089
|
+
const jwt22 = this._deviceJwt ?? this._persisted?.deviceJwt;
|
|
66090
|
+
if (!jwt22 || !this._persisted) return false;
|
|
66091
|
+
let rows;
|
|
66092
|
+
try {
|
|
66093
|
+
const res = await fetch(`${this.config.apiUrl}/api/v1/conversations`, {
|
|
66094
|
+
headers: { Authorization: `Bearer ${jwt22}` }
|
|
66095
|
+
});
|
|
66096
|
+
if (!res.ok) return false;
|
|
66097
|
+
rows = await res.json();
|
|
66098
|
+
} catch (err) {
|
|
66099
|
+
console.warn("[SecureChannel] Conversation re-resolve failed:", err);
|
|
66100
|
+
return false;
|
|
66101
|
+
}
|
|
66102
|
+
if (!Array.isArray(rows)) return false;
|
|
66103
|
+
const mine = rows.filter(
|
|
66104
|
+
(r22) => r22?.agent_device_id === this._deviceId && r22?.status === "active" && typeof r22?.group_id === "string" && typeof r22?.id === "string"
|
|
66105
|
+
);
|
|
66106
|
+
if (mine.length === 0) return false;
|
|
66107
|
+
const before = this._persisted.groupId;
|
|
66108
|
+
this._sessionGroupIds = new Map(mine.map((r22) => [r22.id, r22.group_id]));
|
|
66109
|
+
this._persisted.conversationGroupIds = Object.fromEntries(this._sessionGroupIds);
|
|
66110
|
+
const primary = mine.find((r22) => r22.id === this._persisted.primaryConversationId) ?? mine[0];
|
|
66111
|
+
this._persisted.groupId = primary.group_id;
|
|
66112
|
+
this._persisted.primaryConversationId = primary.id;
|
|
66113
|
+
const liveGroupIds = new Set(mine.map((r22) => r22.group_id));
|
|
66114
|
+
if (!liveGroupIds.has(staleGid)) {
|
|
66115
|
+
try {
|
|
66116
|
+
await deleteMlsState(this.config.dataDir, staleMlsGroupId);
|
|
66117
|
+
} catch {
|
|
66118
|
+
}
|
|
66119
|
+
this._mlsGroups.delete(`1to1-group:${staleGid}`);
|
|
66120
|
+
delete this._persisted.mlsGroups?.[staleGid];
|
|
66121
|
+
this.emit("dm_group_pruned", {
|
|
66122
|
+
conversationGroupId: staleGid,
|
|
66123
|
+
mlsGroupId: staleMlsGroupId
|
|
66124
|
+
});
|
|
66125
|
+
}
|
|
66126
|
+
await this._persistState();
|
|
66127
|
+
console.log(
|
|
66128
|
+
`[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))`
|
|
66129
|
+
);
|
|
66130
|
+
return this._persisted.groupId !== before && before === staleGid;
|
|
66131
|
+
}
|
|
66132
|
+
/**
|
|
66133
|
+
* Resend the message that a now-reconciled `unknown group` refusal killed (#732).
|
|
66134
|
+
*
|
|
66135
|
+
* Called ONLY after `_reconcileConversationGroups` reported that the primary
|
|
66136
|
+
* group pointer moved off the refused group, so there is exactly one sensible
|
|
66137
|
+
* destination: the conversation the reconcile settled on.
|
|
66138
|
+
*
|
|
66139
|
+
* The candidate is consumed BEFORE the resend, not after — one attempt is the
|
|
66140
|
+
* bound, and that must hold even if the resend itself throws. A retry that
|
|
66141
|
+
* re-armed on failure would turn a persistent refusal into an infinite loop,
|
|
66142
|
+
* which is a worse failure than the dropped message it set out to fix.
|
|
66143
|
+
*
|
|
66144
|
+
* The resend is always TARGETED, never a broadcast: the original send may
|
|
66145
|
+
* have fanned out to several groups of which only one was refused, and
|
|
66146
|
+
* re-broadcasting would duplicate the message in all the others.
|
|
66147
|
+
*/
|
|
66148
|
+
async _retryAfterReconcile(staleMlsGroupId) {
|
|
66149
|
+
const candidate = this._retryCandidates.get(staleMlsGroupId);
|
|
66150
|
+
this._retryCandidates.delete(staleMlsGroupId);
|
|
66151
|
+
if (!candidate) return;
|
|
66152
|
+
if (Date.now() - candidate.ts > RETRY_CANDIDATE_TTL_MS) {
|
|
66153
|
+
console.warn(
|
|
66154
|
+
`[deliver] delivery_result=LOST group=${staleMlsGroupId.slice(0, 8)} reason=retry-window-expired`
|
|
66155
|
+
);
|
|
66156
|
+
return;
|
|
66157
|
+
}
|
|
66158
|
+
const conversationId = this._persisted?.primaryConversationId;
|
|
66159
|
+
if (!conversationId) return;
|
|
66160
|
+
try {
|
|
66161
|
+
await this.send(candidate.plaintext, {
|
|
66162
|
+
...candidate.options,
|
|
66163
|
+
conversationId,
|
|
66164
|
+
isResend: true
|
|
66165
|
+
});
|
|
66166
|
+
console.log(
|
|
66167
|
+
`[deliver] delivery_result=RESENT group=${staleMlsGroupId.slice(0, 8)} \u2192 ${String(this._persisted?.groupId).slice(0, 8)} conv=${conversationId.slice(0, 8)}`
|
|
66168
|
+
);
|
|
66169
|
+
} catch (err) {
|
|
66170
|
+
console.error(
|
|
66171
|
+
`[deliver] delivery_result=LOST group=${staleMlsGroupId.slice(0, 8)} reason=resend-failed detail=${err instanceof Error ? err.message : String(err)}`
|
|
66172
|
+
);
|
|
66173
|
+
}
|
|
66174
|
+
}
|
|
65981
66175
|
/**
|
|
65982
66176
|
* Return info for all joined rooms.
|
|
65983
66177
|
*/
|
|
@@ -66350,7 +66544,7 @@ var init_channel = __esm2({
|
|
|
66350
66544
|
});
|
|
66351
66545
|
}
|
|
66352
66546
|
const targetLabel = resolved.kind === "owner" ? "owner" : `${resolved.kind}:${resolved.id?.slice(0, 8)}...`;
|
|
66353
|
-
console.log(`[deliver] target=${targetLabel} content=${content.type} result=
|
|
66547
|
+
console.log(`[deliver] target=${targetLabel} content=${content.type} result=sent`);
|
|
66354
66548
|
if (dedupeKey) {
|
|
66355
66549
|
try {
|
|
66356
66550
|
const now = Date.now();
|
|
@@ -67244,13 +67438,12 @@ var init_channel = __esm2({
|
|
|
67244
67438
|
sessions,
|
|
67245
67439
|
messageHistory: this._persisted.messageHistory ?? []
|
|
67246
67440
|
};
|
|
67247
|
-
if (
|
|
67248
|
-
|
|
67249
|
-
|
|
67250
|
-
this._persisted.groupId = firstConv.group_id;
|
|
67441
|
+
if (primary) {
|
|
67442
|
+
if (primary.group_id) {
|
|
67443
|
+
this._persisted.groupId = primary.group_id;
|
|
67251
67444
|
}
|
|
67252
|
-
if (
|
|
67253
|
-
this._persisted.defaultTopicId =
|
|
67445
|
+
if (primary.default_topic_id) {
|
|
67446
|
+
this._persisted.defaultTopicId = primary.default_topic_id;
|
|
67254
67447
|
}
|
|
67255
67448
|
}
|
|
67256
67449
|
this._sessionGroupIds = /* @__PURE__ */ new Map();
|
|
@@ -67312,7 +67505,13 @@ var init_channel = __esm2({
|
|
|
67312
67505
|
this._ws = null;
|
|
67313
67506
|
}
|
|
67314
67507
|
this._setState("connecting");
|
|
67315
|
-
await this._maybeReissueDeviceJwt()
|
|
67508
|
+
if (await this._maybeReissueDeviceJwt()) {
|
|
67509
|
+
console.warn(
|
|
67510
|
+
"[SecureChannel] credentials are terminally dead \u2014 aborting connect (no WS attempt)"
|
|
67511
|
+
);
|
|
67512
|
+
this._setState("error");
|
|
67513
|
+
return;
|
|
67514
|
+
}
|
|
67316
67515
|
const wsUrl = this.config.apiUrl.replace(/^http/, "ws");
|
|
67317
67516
|
const url22 = `${wsUrl}/api/v1/ws?token=${encodeURIComponent(this._deviceJwt)}&device_id=${this._deviceId}`;
|
|
67318
67517
|
const ws = new WebSocket2(url22);
|
|
@@ -67353,7 +67552,7 @@ var init_channel = __esm2({
|
|
|
67353
67552
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
67354
67553
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
67355
67554
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
67356
|
-
pluginVersion: true ? "0.23.
|
|
67555
|
+
pluginVersion: true ? "0.23.18" : "0.0.0-dev"
|
|
67357
67556
|
});
|
|
67358
67557
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
67359
67558
|
}
|
|
@@ -67422,7 +67621,12 @@ var init_channel = __esm2({
|
|
|
67422
67621
|
try {
|
|
67423
67622
|
const data = JSON.parse(raw.toString());
|
|
67424
67623
|
if (data.event && data.event !== "ping" && data.event !== "typing") {
|
|
67425
|
-
|
|
67624
|
+
const d22 = data.data ?? {};
|
|
67625
|
+
const conv = (data.conversation_id || d22.conversation_id || "").toString();
|
|
67626
|
+
const parts = [`conv=${conv.slice(0, 8)}`];
|
|
67627
|
+
if (d22.group_id) parts.push(`group=${String(d22.group_id).slice(0, 8)}`);
|
|
67628
|
+
if (d22.detail) parts.push(`detail=${d22.detail}`);
|
|
67629
|
+
console.log(`[SecureChannel] WS event: ${data.event} ${parts.join(" ")}`);
|
|
67426
67630
|
}
|
|
67427
67631
|
if (data.event === "ping") {
|
|
67428
67632
|
ws.send(JSON.stringify({ event: "pong" }));
|
|
@@ -67671,7 +67875,7 @@ var init_channel = __esm2({
|
|
|
67671
67875
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
67672
67876
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
67673
67877
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
67674
|
-
pluginVersion: true ? "0.23.
|
|
67878
|
+
pluginVersion: true ? "0.23.18" : "0.0.0-dev"
|
|
67675
67879
|
});
|
|
67676
67880
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
67677
67881
|
}
|
|
@@ -67980,6 +68184,10 @@ var init_channel = __esm2({
|
|
|
67980
68184
|
}
|
|
67981
68185
|
if (data.event === "error") {
|
|
67982
68186
|
const detail = data.data?.detail || data.detail || "Unknown server error";
|
|
68187
|
+
const rejectedGroup = String(data.data?.group_id ?? "").slice(0, 8);
|
|
68188
|
+
console.error(
|
|
68189
|
+
`[deliver] delivery_result=REJECTED group=${rejectedGroup || "unknown"} detail=${detail}`
|
|
68190
|
+
);
|
|
67983
68191
|
console.error(`[SecureChannel] Server error: ${detail}`);
|
|
67984
68192
|
await this._handleServerError(data.data || data);
|
|
67985
68193
|
this.emit("error", new Error(`Server: ${detail}`));
|
|
@@ -68151,6 +68359,10 @@ var init_channel = __esm2({
|
|
|
68151
68359
|
conversationId: convId ?? this._primaryConversationId,
|
|
68152
68360
|
timestamp: data.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
68153
68361
|
topicId,
|
|
68362
|
+
// #621: positive attestation that this decrypted under the OWNER'S 1:1
|
|
68363
|
+
// material (MLS 1to1-group map, or a Double Ratchet session), NOT merely
|
|
68364
|
+
// that a roomId was absent. The bridge's full-access DM lane requires it.
|
|
68365
|
+
ownerDm1to1: true,
|
|
68154
68366
|
messageType
|
|
68155
68367
|
};
|
|
68156
68368
|
this.emit("message", text, metadata);
|
|
@@ -68393,7 +68605,7 @@ ${messageText}`;
|
|
|
68393
68605
|
*/
|
|
68394
68606
|
async _downloadAndDecryptAttachment(info) {
|
|
68395
68607
|
const attachDir = join4(this.config.dataDir, "attachments");
|
|
68396
|
-
await
|
|
68608
|
+
await mkdir3(attachDir, { recursive: true });
|
|
68397
68609
|
const url22 = `${this.config.apiUrl}${info.blobUrl}`;
|
|
68398
68610
|
const res = await fetch(url22, {
|
|
68399
68611
|
headers: { Authorization: `Bearer ${this._deviceJwt}` }
|
|
@@ -68411,7 +68623,7 @@ ${messageText}`;
|
|
|
68411
68623
|
const fileNonce = base64ToBytes(info.fileNonce);
|
|
68412
68624
|
const decrypted = decryptFile(encryptedData, fileKey, fileNonce);
|
|
68413
68625
|
const filePath = join4(attachDir, info.filename);
|
|
68414
|
-
await
|
|
68626
|
+
await writeFile3(filePath, decrypted);
|
|
68415
68627
|
console.log(`[SecureChannel] Attachment saved: ${filePath} (${decrypted.length} bytes)`);
|
|
68416
68628
|
return { filePath, decrypted };
|
|
68417
68629
|
}
|
|
@@ -69066,10 +69278,10 @@ ${messageText}`;
|
|
|
69066
69278
|
await mlsGroup.processCommit(commitBytes);
|
|
69067
69279
|
await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
|
|
69068
69280
|
console.log(`[SecureChannel] MLS commit processed for room ${roomId.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
|
|
69281
|
+
this._mlsCommitFailCounts.delete(roomId);
|
|
69069
69282
|
} catch (err) {
|
|
69070
69283
|
await this._onCommitFailure(roomId, groupId, err, `room ${roomId.slice(0, 8)}`);
|
|
69071
69284
|
}
|
|
69072
|
-
this._mlsCommitFailCounts.delete(roomId);
|
|
69073
69285
|
} else {
|
|
69074
69286
|
this._bufferMlsCommit(groupId, epoch, data);
|
|
69075
69287
|
console.log(`[SecureChannel] Buffered MLS commit for room ${roomId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
|
|
@@ -69086,10 +69298,10 @@ ${messageText}`;
|
|
|
69086
69298
|
await mlsGroup.processCommit(commitBytes);
|
|
69087
69299
|
await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mlsGroup.exportState()));
|
|
69088
69300
|
console.log(`[SecureChannel] MLS commit processed for A2A ${chId.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
|
|
69301
|
+
this._mlsCommitFailCounts.delete(`a2a:${chId}`);
|
|
69089
69302
|
} catch (err) {
|
|
69090
69303
|
await this._onCommitFailure(`a2a:${chId}`, groupId, err, `A2A ${chId.slice(0, 8)}`);
|
|
69091
69304
|
}
|
|
69092
|
-
this._mlsCommitFailCounts.delete(`a2a:${chId}`);
|
|
69093
69305
|
} else {
|
|
69094
69306
|
this._bufferMlsCommit(groupId, epoch, data);
|
|
69095
69307
|
console.log(`[SecureChannel] Buffered MLS commit for A2A ${chId.slice(0, 8)} (epoch=${epoch}, group not initialized)`);
|
|
@@ -69431,7 +69643,7 @@ ${messageText}`;
|
|
|
69431
69643
|
console.warn("[SecureChannel] KeyPackage pool replenish after join failed:", err);
|
|
69432
69644
|
});
|
|
69433
69645
|
console.log(`[SecureChannel] Welcome joined: group=${groupId?.slice(0, 8)} kpSource=${kpSource} welcomeLen=${welcomeBytes.length} candidates=${candidateCount} poolRemaining=${this._pendingKpBundles.length}`);
|
|
69434
|
-
if (conversationGroupId) {
|
|
69646
|
+
if (conversationGroupId && !data.room_id) {
|
|
69435
69647
|
const key = `1to1-group:${conversationGroupId}`;
|
|
69436
69648
|
this._mlsGroups.set(key, mgr);
|
|
69437
69649
|
await saveMlsState(this.config.dataDir, groupId, JSON.stringify(mgr.exportState()));
|
|
@@ -70252,7 +70464,11 @@ ${messageText}`;
|
|
|
70252
70464
|
messageId: msg.message_id,
|
|
70253
70465
|
conversationId: msg.conversation_id,
|
|
70254
70466
|
timestamp: msg.created_at,
|
|
70255
|
-
topicId
|
|
70467
|
+
topicId,
|
|
70468
|
+
// #621: positive attestation that this decrypted under the OWNER'S 1:1
|
|
70469
|
+
// material (MLS 1to1-group map, or a Double Ratchet session), NOT merely
|
|
70470
|
+
// that a roomId was absent. The bridge's full-access DM lane requires it.
|
|
70471
|
+
ownerDm1to1: true
|
|
70256
70472
|
};
|
|
70257
70473
|
this.emit("message", messageText, metadata);
|
|
70258
70474
|
Promise.resolve(this.config.onMessage?.(messageText, metadata)).catch((err) => {
|
|
@@ -70584,7 +70800,11 @@ ${messageText}`;
|
|
|
70584
70800
|
messageId: msg.id,
|
|
70585
70801
|
conversationId: msg.conversation_id,
|
|
70586
70802
|
timestamp: msg.created_at,
|
|
70587
|
-
topicId
|
|
70803
|
+
topicId,
|
|
70804
|
+
// #621: positive attestation that this decrypted under the OWNER'S 1:1
|
|
70805
|
+
// material (MLS 1to1-group map, or a Double Ratchet session), NOT merely
|
|
70806
|
+
// that a roomId was absent. The bridge's full-access DM lane requires it.
|
|
70807
|
+
ownerDm1to1: true
|
|
70588
70808
|
};
|
|
70589
70809
|
this.emit("message", messageText, metadata);
|
|
70590
70810
|
Promise.resolve(this.config.onMessage?.(messageText, metadata)).catch((err) => {
|
|
@@ -70865,7 +71085,11 @@ ${messageText}`;
|
|
|
70865
71085
|
messageId: msg.id,
|
|
70866
71086
|
conversationId: msg.conversation_id,
|
|
70867
71087
|
timestamp: msg.created_at,
|
|
70868
|
-
topicId
|
|
71088
|
+
topicId,
|
|
71089
|
+
// #621: positive attestation that this decrypted under the OWNER'S 1:1
|
|
71090
|
+
// material (MLS 1to1-group map, or a Double Ratchet session), NOT merely
|
|
71091
|
+
// that a roomId was absent. The bridge's full-access DM lane requires it.
|
|
71092
|
+
ownerDm1to1: true
|
|
70869
71093
|
};
|
|
70870
71094
|
this.emit("message", messageText, metadata);
|
|
70871
71095
|
Promise.resolve(this.config.onMessage?.(messageText, metadata)).catch((err) => {
|
|
@@ -70959,7 +71183,7 @@ ${messageText}`;
|
|
|
70959
71183
|
return;
|
|
70960
71184
|
}
|
|
70961
71185
|
this._authFailedThisSession = true;
|
|
70962
|
-
const authReason = data?.reason
|
|
71186
|
+
const authReason = data?.reason ?? "device_revoked";
|
|
70963
71187
|
console.warn(
|
|
70964
71188
|
`[SecureChannel] connection_rejected (reason=${data?.reason ?? "unknown"}, retryable=false) \u2014 terminal; surfacing auth_failed`
|
|
70965
71189
|
);
|
|
@@ -71123,6 +71347,14 @@ function terminalReenrollMessage(agentName, reason) {
|
|
|
71123
71347
|
const who = agentName ? `[${agentName}] ` : "";
|
|
71124
71348
|
return `${who}device is no longer valid (${reason}) \u2014 it was revoked or replaced. Re-enroll with a fresh token from AgentVault; the old credentials are dead. This agent will stop reconnecting.`;
|
|
71125
71349
|
}
|
|
71350
|
+
function formatChannelError(err) {
|
|
71351
|
+
const msg = String(err);
|
|
71352
|
+
const rejection = /Server: (.+)$/.exec(msg);
|
|
71353
|
+
if (rejection) {
|
|
71354
|
+
return `[AgentVault] DELIVERY FAILED \u2014 the server refused this frame: ${rejection[1]}. The connection survives; this message was dropped and nothing retries it.`;
|
|
71355
|
+
}
|
|
71356
|
+
return `[AgentVault] channel error (non-fatal to the connection): ${msg}`;
|
|
71357
|
+
}
|
|
71126
71358
|
function attachLifecycle(channel, opts = {}) {
|
|
71127
71359
|
const log = opts.log ?? (() => {
|
|
71128
71360
|
});
|
|
@@ -71323,7 +71555,7 @@ var init_openclaw_plugin = __esm2({
|
|
|
71323
71555
|
});
|
|
71324
71556
|
_channels.set(account.accountId, channel);
|
|
71325
71557
|
channel.on("error", (err) => {
|
|
71326
|
-
_log?.(
|
|
71558
|
+
_log?.(formatChannelError(err));
|
|
71327
71559
|
});
|
|
71328
71560
|
attachLifecycle(channel, {
|
|
71329
71561
|
agentName: account.agentName,
|
|
@@ -71460,6 +71692,11 @@ var init_fetch_interceptor = __esm2({
|
|
|
71460
71692
|
traceStore = new AsyncLocalStorage();
|
|
71461
71693
|
}
|
|
71462
71694
|
});
|
|
71695
|
+
var init_tool_audit = __esm2({
|
|
71696
|
+
"src/tool-audit.ts"() {
|
|
71697
|
+
"use strict";
|
|
71698
|
+
}
|
|
71699
|
+
});
|
|
71463
71700
|
var isUsingManagedRoutes;
|
|
71464
71701
|
var init_openclaw_entry = __esm2({
|
|
71465
71702
|
"src/openclaw-entry.ts"() {
|
|
@@ -71469,6 +71706,7 @@ var init_openclaw_entry = __esm2({
|
|
|
71469
71706
|
init_http_handlers();
|
|
71470
71707
|
init_openclaw_compat();
|
|
71471
71708
|
init_lifecycle();
|
|
71709
|
+
init_tool_audit();
|
|
71472
71710
|
init_types();
|
|
71473
71711
|
isUsingManagedRoutes = false;
|
|
71474
71712
|
}
|
|
@@ -97520,7 +97758,7 @@ var init_index = __esm2({
|
|
|
97520
97758
|
init_skill_invoker();
|
|
97521
97759
|
await init_skill_telemetry();
|
|
97522
97760
|
await init_policy_enforcer();
|
|
97523
|
-
VERSION = true ? "0.23.
|
|
97761
|
+
VERSION = true ? "0.23.18" : "0.0.0-dev";
|
|
97524
97762
|
}
|
|
97525
97763
|
});
|
|
97526
97764
|
await init_index();
|
|
@@ -118831,8 +119069,8 @@ import { mkdirSync as mkdirSync3, writeFileSync, rmSync as rmSync2, readdirSync
|
|
|
118831
119069
|
import { join as join6 } from "node:path";
|
|
118832
119070
|
import { homedir, hostname as hostname3 } from "node:os";
|
|
118833
119071
|
var TRUST_SUBDIR = "host-trust";
|
|
118834
|
-
var
|
|
118835
|
-
var
|
|
119072
|
+
var DIR_MODE2 = 448;
|
|
119073
|
+
var FILE_MODE2 = 384;
|
|
118836
119074
|
var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
118837
119075
|
var IDENTITY_FILES = ["agentvault.json", "secure-channel.json", "agentvault.json.bak"];
|
|
118838
119076
|
var HostTrustError = class extends Error {
|
|
@@ -118859,15 +119097,15 @@ function grant(deviceId, root3) {
|
|
|
118859
119097
|
const id = sanitize(deviceId);
|
|
118860
119098
|
const dir = trustDir(root3);
|
|
118861
119099
|
const p2 = join6(dir, id);
|
|
118862
|
-
mkdirSync3(dir, { recursive: true, mode:
|
|
119100
|
+
mkdirSync3(dir, { recursive: true, mode: DIR_MODE2 });
|
|
118863
119101
|
writeFileSync(
|
|
118864
119102
|
p2,
|
|
118865
119103
|
`granted_at=${(/* @__PURE__ */ new Date()).toISOString()} host=${hostname3()}
|
|
118866
119104
|
`,
|
|
118867
|
-
{ mode:
|
|
119105
|
+
{ mode: FILE_MODE2 }
|
|
118868
119106
|
);
|
|
118869
|
-
chmodSync2(dir,
|
|
118870
|
-
chmodSync2(p2,
|
|
119107
|
+
chmodSync2(dir, DIR_MODE2);
|
|
119108
|
+
chmodSync2(p2, FILE_MODE2);
|
|
118871
119109
|
}
|
|
118872
119110
|
function revoke(deviceId, root3) {
|
|
118873
119111
|
const id = sanitize(deviceId);
|
|
@@ -133641,9 +133879,14 @@ function wireBridge(channel, session, target, opts = {}) {
|
|
|
133641
133879
|
});
|
|
133642
133880
|
channel.on("message", (text, metadata) => {
|
|
133643
133881
|
if (metadata?.roomId) return;
|
|
133644
|
-
|
|
133882
|
+
const ownerAttested = metadata?.ownerDm1to1 === true;
|
|
133883
|
+
if (!ownerAttested) {
|
|
133884
|
+
log("inbound 1:1 DM WITHOUT owner attestation \u2014 delivering to the locked listener (no tools)");
|
|
133885
|
+
} else {
|
|
133886
|
+
log("inbound 1:1 DM from owner");
|
|
133887
|
+
}
|
|
133645
133888
|
target.setDm();
|
|
133646
|
-
session.push(text, target.snapshotReply(channel, log), { autoReplyOnText:
|
|
133889
|
+
session.push(text, target.snapshotReply(channel, log), { autoReplyOnText: ownerAttested });
|
|
133647
133890
|
});
|
|
133648
133891
|
const workerCapable = !!opts.workspaceDir;
|
|
133649
133892
|
const osIsolated = opts.osIsolated !== void 0 ? opts.osIsolated : process.env.AV_WORKER_OS_ISOLATED === "1" || process.env.AV_WORKER_OS_ISOLATED === "true";
|
|
@@ -133784,7 +134027,7 @@ async function main() {
|
|
|
133784
134027
|
"[bridge] warning: passing the invite token on the command line is visible to other local users via 'ps'. Prefer: AV_INVITE_TOKEN=\u2026 npx @agentvault/claude-bridge"
|
|
133785
134028
|
);
|
|
133786
134029
|
}
|
|
133787
|
-
console.error(`[bridge] version: ${true ? "0.7.
|
|
134030
|
+
console.error(`[bridge] version: ${true ? "0.7.6" : "dev"}`);
|
|
133788
134031
|
console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
|
|
133789
134032
|
console.error(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
|
|
133790
134033
|
if (cfg.armRoom) {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentvault/claude-bridge",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "AgentVault Claude Bridge
|
|
5
|
+
"description": "AgentVault Claude Bridge — daemon for bridging a Claude agent into secure E2E-encrypted AgentVault 1:1 direct messages and rooms.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"bin": {
|