@agentvault/claude-bridge 0.7.10 → 0.7.12
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/index.js +198 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -63791,6 +63791,35 @@ async function deleteMlsState(dataDir, groupId) {
|
|
|
63791
63791
|
} catch {
|
|
63792
63792
|
}
|
|
63793
63793
|
}
|
|
63794
|
+
async function quarantineOrphanedMlsState(dataDir, isStillMember, opts = {}) {
|
|
63795
|
+
const max = opts.max ?? 20;
|
|
63796
|
+
const quarantined = [];
|
|
63797
|
+
let names;
|
|
63798
|
+
try {
|
|
63799
|
+
names = await readdir(dataDir);
|
|
63800
|
+
} catch {
|
|
63801
|
+
return quarantined;
|
|
63802
|
+
}
|
|
63803
|
+
for (const name of names) {
|
|
63804
|
+
if (quarantined.length >= max) break;
|
|
63805
|
+
const m22 = /^mls-(?!kp-pending-)([A-Za-z0-9_-]+)\.json$/.exec(name);
|
|
63806
|
+
if (!m22) continue;
|
|
63807
|
+
const groupId = m22[1];
|
|
63808
|
+
let member;
|
|
63809
|
+
try {
|
|
63810
|
+
member = await isStillMember(groupId);
|
|
63811
|
+
} catch {
|
|
63812
|
+
continue;
|
|
63813
|
+
}
|
|
63814
|
+
if (member === null || member === true) continue;
|
|
63815
|
+
try {
|
|
63816
|
+
await rename(join(dataDir, name), join(dataDir, `${name}.orphaned`));
|
|
63817
|
+
quarantined.push(groupId);
|
|
63818
|
+
} catch {
|
|
63819
|
+
}
|
|
63820
|
+
}
|
|
63821
|
+
return quarantined;
|
|
63822
|
+
}
|
|
63794
63823
|
var SYNC_LOCK_STALE_MS;
|
|
63795
63824
|
var pendingKpPath;
|
|
63796
63825
|
var init_mls_state = __esm2({
|
|
@@ -64149,6 +64178,16 @@ var init_a2a_log_gate = __esm2({
|
|
|
64149
64178
|
};
|
|
64150
64179
|
}
|
|
64151
64180
|
});
|
|
64181
|
+
function mlsMembershipFromStatus(status) {
|
|
64182
|
+
if (status === 200) return true;
|
|
64183
|
+
if (status === 403 || status === 404) return false;
|
|
64184
|
+
return null;
|
|
64185
|
+
}
|
|
64186
|
+
var init_mls_membership_probe = __esm2({
|
|
64187
|
+
"src/mls-membership-probe.ts"() {
|
|
64188
|
+
"use strict";
|
|
64189
|
+
}
|
|
64190
|
+
});
|
|
64152
64191
|
var openclaw_compat_exports = {};
|
|
64153
64192
|
__export2(openclaw_compat_exports, {
|
|
64154
64193
|
AGENT_EVENT_CANDIDATES: () => AGENT_EVENT_CANDIDATES,
|
|
@@ -64709,6 +64748,7 @@ var init_channel = __esm2({
|
|
|
64709
64748
|
await init_state();
|
|
64710
64749
|
init_transport2();
|
|
64711
64750
|
init_a2a_log_gate();
|
|
64751
|
+
init_mls_membership_probe();
|
|
64712
64752
|
ROOM_AGENT_TYPES = /* @__PURE__ */ new Set([
|
|
64713
64753
|
"message",
|
|
64714
64754
|
"text",
|
|
@@ -64788,6 +64828,17 @@ var init_channel = __esm2({
|
|
|
64788
64828
|
_pendingPollTimer = null;
|
|
64789
64829
|
_syncMessageIds = null;
|
|
64790
64830
|
_deliveryHeartbeat = null;
|
|
64831
|
+
/**
|
|
64832
|
+
* How often to re-check the KeyPackage pool against the server (#826).
|
|
64833
|
+
*
|
|
64834
|
+
* KeyPackages expire after 7 days and, before this, only a reconnect
|
|
64835
|
+
* republished them — so an agent that simply stayed up ran to zero usable
|
|
64836
|
+
* and silently stopped accepting new MLS 1:1 conversations while looking
|
|
64837
|
+
* perfectly healthy. 6h gives ~28 attempts inside one lifetime, so a run of
|
|
64838
|
+
* transient failures cannot age the pool out. Cost is one GET per interval.
|
|
64839
|
+
*/
|
|
64840
|
+
static _KP_REPLENISH_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
64841
|
+
_kpReplenishTimer = null;
|
|
64791
64842
|
_a2aReconcileTimer = null;
|
|
64792
64843
|
_deliveryPulling = false;
|
|
64793
64844
|
_drDeliveryPulling = false;
|
|
@@ -65752,7 +65803,7 @@ var init_channel = __esm2({
|
|
|
65752
65803
|
*/
|
|
65753
65804
|
sendActivitySpan(spanData) {
|
|
65754
65805
|
if (!this._ws || this._ws.readyState !== WebSocket2.OPEN) return;
|
|
65755
|
-
const pluginVersion = true ? "0.23.
|
|
65806
|
+
const pluginVersion = true ? "0.23.21" : "0.0.0-dev";
|
|
65756
65807
|
const agentName = this.config.agentName ?? "Agent";
|
|
65757
65808
|
const resource = {
|
|
65758
65809
|
"service.name": "agentvault-agent",
|
|
@@ -66072,6 +66123,47 @@ var init_channel = __esm2({
|
|
|
66072
66123
|
);
|
|
66073
66124
|
this.emit("rooms_reconciled", { pruned: stale });
|
|
66074
66125
|
}
|
|
66126
|
+
/**
|
|
66127
|
+
* #629 — quarantine MLS state for groups this device is no longer in.
|
|
66128
|
+
*
|
|
66129
|
+
* `_reconcileRoomsWithServer` above covers ROOMS: anything absent from
|
|
66130
|
+
* `GET /rooms` is pruned. It cannot see a group that was never a room — a 1:1
|
|
66131
|
+
* conversation group or an A2A channel group — so that state lingered with
|
|
66132
|
+
* nothing to reconcile it. Measured on loopita 2026-08-08: one group-state
|
|
66133
|
+
* file whose group had no server-side row at all.
|
|
66134
|
+
*
|
|
66135
|
+
* `quarantineOrphanedMlsState` was written for exactly this and shipped with
|
|
66136
|
+
* 11 passing tests, but nothing ever called it. This is that call site.
|
|
66137
|
+
*
|
|
66138
|
+
* Runs AFTER the room reconcile so rooms are already gone and this only sees
|
|
66139
|
+
* genuine non-room orphans.
|
|
66140
|
+
*
|
|
66141
|
+
* Quarantine RENAMES to `.orphaned` rather than deleting: if the verdict is
|
|
66142
|
+
* ever wrong the state is recoverable by hand, which deletion would not be.
|
|
66143
|
+
*/
|
|
66144
|
+
async _quarantineOrphanedMlsGroups() {
|
|
66145
|
+
if (!this._deviceJwt) return;
|
|
66146
|
+
const quarantined = await quarantineOrphanedMlsState(
|
|
66147
|
+
this.config.dataDir,
|
|
66148
|
+
async (groupId) => {
|
|
66149
|
+
try {
|
|
66150
|
+
const res = await fetch(
|
|
66151
|
+
`${this.config.apiUrl}/api/v1/mls/groups/${encodeURIComponent(groupId)}`,
|
|
66152
|
+
{ headers: { Authorization: `Bearer ${this._deviceJwt}` } }
|
|
66153
|
+
);
|
|
66154
|
+
return mlsMembershipFromStatus(res.status);
|
|
66155
|
+
} catch {
|
|
66156
|
+
return null;
|
|
66157
|
+
}
|
|
66158
|
+
}
|
|
66159
|
+
);
|
|
66160
|
+
if (quarantined.length > 0) {
|
|
66161
|
+
console.log(
|
|
66162
|
+
`[SecureChannel] Quarantined ${quarantined.length} orphaned MLS group state file(s): ` + quarantined.map((g22) => g22.slice(0, 8)).join(", ")
|
|
66163
|
+
);
|
|
66164
|
+
this.emit("mls_state_quarantined", { groupIds: quarantined });
|
|
66165
|
+
}
|
|
66166
|
+
}
|
|
66075
66167
|
/**
|
|
66076
66168
|
* Handle an in-band `{event:"error"}` frame from the server.
|
|
66077
66169
|
*
|
|
@@ -67596,6 +67688,8 @@ var init_channel = __esm2({
|
|
|
67596
67688
|
this._setState("ready");
|
|
67597
67689
|
void this._reconcileRoomsWithServer().catch(
|
|
67598
67690
|
(err) => console.warn(`[SecureChannel] room reconcile failed (ignored): ${err instanceof Error ? err.message : String(err)}`)
|
|
67691
|
+
).then(() => this._quarantineOrphanedMlsGroups()).catch(
|
|
67692
|
+
(err) => console.warn(`[SecureChannel] MLS orphan quarantine failed (ignored): ${err instanceof Error ? err.message : String(err)}`)
|
|
67599
67693
|
);
|
|
67600
67694
|
if (this.config.enableScanning) {
|
|
67601
67695
|
this._scanEngine = new ScanEngine();
|
|
@@ -67619,7 +67713,7 @@ var init_channel = __esm2({
|
|
|
67619
67713
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
67620
67714
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
67621
67715
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
67622
|
-
pluginVersion: true ? "0.23.
|
|
67716
|
+
pluginVersion: true ? "0.23.21" : "0.0.0-dev"
|
|
67623
67717
|
});
|
|
67624
67718
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
67625
67719
|
}
|
|
@@ -67628,6 +67722,7 @@ var init_channel = __esm2({
|
|
|
67628
67722
|
console.warn("[SecureChannel] A2A channel sync on connect failed:", err);
|
|
67629
67723
|
});
|
|
67630
67724
|
await this._ensureKpPoolPublished();
|
|
67725
|
+
this._startKpReplenishTimer();
|
|
67631
67726
|
this._pullDeliveryQueue().catch((err) => {
|
|
67632
67727
|
console.warn("[SecureChannel] Initial delivery pull failed:", err);
|
|
67633
67728
|
});
|
|
@@ -67942,7 +68037,7 @@ var init_channel = __esm2({
|
|
|
67942
68037
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
67943
68038
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
67944
68039
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
67945
|
-
pluginVersion: true ? "0.23.
|
|
68040
|
+
pluginVersion: true ? "0.23.21" : "0.0.0-dev"
|
|
67946
68041
|
});
|
|
67947
68042
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
67948
68043
|
}
|
|
@@ -68285,6 +68380,7 @@ var init_channel = __esm2({
|
|
|
68285
68380
|
this._deliveryHeartbeat = null;
|
|
68286
68381
|
}
|
|
68287
68382
|
this._stopA2aReconcileTimer();
|
|
68383
|
+
this._stopKpReplenishTimer();
|
|
68288
68384
|
if (this._stopped) return;
|
|
68289
68385
|
this._setState("disconnected");
|
|
68290
68386
|
this._scheduleReconnect();
|
|
@@ -69678,6 +69774,103 @@ ${messageText}`;
|
|
|
69678
69774
|
this._kpPoolFilling = false;
|
|
69679
69775
|
}
|
|
69680
69776
|
}
|
|
69777
|
+
/** Start the periodic KeyPackage replenish (#826). Idempotent. */
|
|
69778
|
+
_startKpReplenishTimer() {
|
|
69779
|
+
this._stopKpReplenishTimer();
|
|
69780
|
+
this._kpReplenishTimer = setInterval(() => {
|
|
69781
|
+
void this._replenishKpPoolFromServer();
|
|
69782
|
+
}, _SecureChannel._KP_REPLENISH_INTERVAL_MS);
|
|
69783
|
+
}
|
|
69784
|
+
_stopKpReplenishTimer() {
|
|
69785
|
+
if (this._kpReplenishTimer) {
|
|
69786
|
+
clearInterval(this._kpReplenishTimer);
|
|
69787
|
+
this._kpReplenishTimer = null;
|
|
69788
|
+
}
|
|
69789
|
+
}
|
|
69790
|
+
/**
|
|
69791
|
+
* The server's EXPIRY-AWARE usable KeyPackage count, or null when unknown.
|
|
69792
|
+
*
|
|
69793
|
+
* `count_unconsumed` filters `expires_at > utcnow()`, which is the only count
|
|
69794
|
+
* that reflects what a peer can actually fetch. Returns null (not 0) on any
|
|
69795
|
+
* failure so callers can tell "server says empty" from "we could not ask" —
|
|
69796
|
+
* guessing 0 here would publish a fresh pool on every transient 503.
|
|
69797
|
+
*/
|
|
69798
|
+
async _fetchServerKpCount() {
|
|
69799
|
+
try {
|
|
69800
|
+
const res = await fetch(
|
|
69801
|
+
`${this.config.apiUrl}/api/v1/mls/key-packages/count?device_id=${this._deviceId}`,
|
|
69802
|
+
{ headers: { Authorization: `Bearer ${this._deviceJwt}` } }
|
|
69803
|
+
);
|
|
69804
|
+
if (!res.ok) return null;
|
|
69805
|
+
const body = await res.json();
|
|
69806
|
+
const n22 = body?.count;
|
|
69807
|
+
return typeof n22 === "number" && Number.isFinite(n22) ? n22 : null;
|
|
69808
|
+
} catch {
|
|
69809
|
+
return null;
|
|
69810
|
+
}
|
|
69811
|
+
}
|
|
69812
|
+
/**
|
|
69813
|
+
* Top the pool up against the SERVER's count (#826).
|
|
69814
|
+
*
|
|
69815
|
+
* ⚠️ DO NOT replace this with a periodic call to `_ensureKpPoolPublished()`.
|
|
69816
|
+
* That function early-returns on `_pendingKpBundles.length >= _KP_POOL_TARGET`,
|
|
69817
|
+
* and that array holds PRIVATE bundles which never expire locally — so on an
|
|
69818
|
+
* agent that has been connected longer than the KeyPackage lifetime it is
|
|
69819
|
+
* still full while the server has zero usable. Measured on prod 2026-08-09:
|
|
69820
|
+
* cortina and eclaude were online and green with 10 unconsumed / 0 usable,
|
|
69821
|
+
* unable to accept any new MLS 1:1. A timer over the local guard would have
|
|
69822
|
+
* run forever and changed nothing.
|
|
69823
|
+
*
|
|
69824
|
+
* Publishes only the shortfall, and never throws — this runs on a timer and
|
|
69825
|
+
* must not be able to take the channel down.
|
|
69826
|
+
*/
|
|
69827
|
+
async _replenishKpPoolFromServer() {
|
|
69828
|
+
try {
|
|
69829
|
+
if (this._kpPoolFilling) return;
|
|
69830
|
+
const usable = await this._fetchServerKpCount();
|
|
69831
|
+
if (usable === null) return;
|
|
69832
|
+
const shortfall = _SecureChannel._KP_POOL_TARGET - usable;
|
|
69833
|
+
if (shortfall <= 0) return;
|
|
69834
|
+
this._kpPoolFilling = true;
|
|
69835
|
+
try {
|
|
69836
|
+
const identity = new TextEncoder().encode(this._deviceId);
|
|
69837
|
+
let published = 0;
|
|
69838
|
+
for (let i2 = 0; i2 < shortfall; i2++) {
|
|
69839
|
+
const mlsMgr = new MLSGroupManager();
|
|
69840
|
+
const kp = await mlsMgr.generateKeyPackage(identity);
|
|
69841
|
+
const kpHex = Buffer.from(
|
|
69842
|
+
MLSGroupManager.serializeKeyPackage(kp.publicPackage)
|
|
69843
|
+
).toString("hex");
|
|
69844
|
+
const res = await fetch(`${this.config.apiUrl}/api/v1/mls/key-packages`, {
|
|
69845
|
+
method: "POST",
|
|
69846
|
+
headers: {
|
|
69847
|
+
Authorization: `Bearer ${this._deviceJwt}`,
|
|
69848
|
+
"Content-Type": "application/json"
|
|
69849
|
+
},
|
|
69850
|
+
body: JSON.stringify({ key_package: kpHex })
|
|
69851
|
+
});
|
|
69852
|
+
if (!res.ok) {
|
|
69853
|
+
console.warn(
|
|
69854
|
+
`[SecureChannel] KeyPackage replenish rejected (status ${res.status}) after ${published}/${shortfall}`
|
|
69855
|
+
);
|
|
69856
|
+
break;
|
|
69857
|
+
}
|
|
69858
|
+
this._recordPublishedKp(kp);
|
|
69859
|
+
if (!this._mlsKeyPackage) this._mlsKeyPackage = kp;
|
|
69860
|
+
published++;
|
|
69861
|
+
}
|
|
69862
|
+
if (published > 0) {
|
|
69863
|
+
console.log(
|
|
69864
|
+
`[SecureChannel] MLS KeyPackage pool replenished (${published} new; server had ${usable}/${_SecureChannel._KP_POOL_TARGET} usable)`
|
|
69865
|
+
);
|
|
69866
|
+
}
|
|
69867
|
+
} finally {
|
|
69868
|
+
this._kpPoolFilling = false;
|
|
69869
|
+
}
|
|
69870
|
+
} catch (err) {
|
|
69871
|
+
console.warn("[SecureChannel] KeyPackage replenish failed:", err);
|
|
69872
|
+
}
|
|
69873
|
+
}
|
|
69681
69874
|
async _handleMlsWelcome(data) {
|
|
69682
69875
|
const groupId = data.group_id;
|
|
69683
69876
|
const conversationId = data.conversation_id;
|
|
@@ -97831,7 +98024,7 @@ var init_index = __esm2({
|
|
|
97831
98024
|
init_skill_invoker();
|
|
97832
98025
|
await init_skill_telemetry();
|
|
97833
98026
|
await init_policy_enforcer();
|
|
97834
|
-
VERSION = true ? "0.23.
|
|
98027
|
+
VERSION = true ? "0.23.21" : "0.0.0-dev";
|
|
97835
98028
|
}
|
|
97836
98029
|
});
|
|
97837
98030
|
await init_index();
|
|
@@ -134185,7 +134378,7 @@ async function main() {
|
|
|
134185
134378
|
"[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"
|
|
134186
134379
|
);
|
|
134187
134380
|
}
|
|
134188
|
-
logLine(`[bridge] version: ${true ? "0.7.
|
|
134381
|
+
logLine(`[bridge] version: ${true ? "0.7.12" : "dev"}`);
|
|
134189
134382
|
logLine(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
|
|
134190
134383
|
logLine(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
|
|
134191
134384
|
if (cfg.armRoom) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentvault/claude-bridge",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgentVault Claude Bridge \u2014 daemon for bridging a Claude agent into secure E2E-encrypted AgentVault 1:1 direct messages and rooms.",
|
|
6
6
|
"main": "dist/index.js",
|