@agentvault/agentvault 0.23.19 → 0.23.21
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/a2a-log-gate.d.ts +56 -0
- package/dist/a2a-log-gate.d.ts.map +1 -0
- package/dist/channel.d.ts +63 -0
- package/dist/channel.d.ts.map +1 -1
- package/dist/cli.js +235 -5
- package/dist/cli.js.map +4 -4
- package/dist/index.js +235 -5
- package/dist/index.js.map +4 -4
- package/dist/mls-membership-probe.d.ts +28 -0
- package/dist/mls-membership-probe.d.ts.map +1 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -63011,6 +63011,35 @@ async function deleteMlsState(dataDir, groupId) {
|
|
|
63011
63011
|
} catch {
|
|
63012
63012
|
}
|
|
63013
63013
|
}
|
|
63014
|
+
async function quarantineOrphanedMlsState(dataDir, isStillMember, opts = {}) {
|
|
63015
|
+
const max = opts.max ?? 20;
|
|
63016
|
+
const quarantined = [];
|
|
63017
|
+
let names;
|
|
63018
|
+
try {
|
|
63019
|
+
names = await readdir(dataDir);
|
|
63020
|
+
} catch {
|
|
63021
|
+
return quarantined;
|
|
63022
|
+
}
|
|
63023
|
+
for (const name of names) {
|
|
63024
|
+
if (quarantined.length >= max) break;
|
|
63025
|
+
const m2 = /^mls-(?!kp-pending-)([A-Za-z0-9_-]+)\.json$/.exec(name);
|
|
63026
|
+
if (!m2) continue;
|
|
63027
|
+
const groupId = m2[1];
|
|
63028
|
+
let member;
|
|
63029
|
+
try {
|
|
63030
|
+
member = await isStillMember(groupId);
|
|
63031
|
+
} catch {
|
|
63032
|
+
continue;
|
|
63033
|
+
}
|
|
63034
|
+
if (member === null || member === true) continue;
|
|
63035
|
+
try {
|
|
63036
|
+
await rename(join(dataDir, name), join(dataDir, `${name}.orphaned`));
|
|
63037
|
+
quarantined.push(groupId);
|
|
63038
|
+
} catch {
|
|
63039
|
+
}
|
|
63040
|
+
}
|
|
63041
|
+
return quarantined;
|
|
63042
|
+
}
|
|
63014
63043
|
var SYNC_LOCK_STALE_MS, pendingKpPath;
|
|
63015
63044
|
var init_mls_state = __esm({
|
|
63016
63045
|
"src/mls-state.ts"() {
|
|
@@ -63355,6 +63384,45 @@ var init_transport2 = __esm({
|
|
|
63355
63384
|
}
|
|
63356
63385
|
});
|
|
63357
63386
|
|
|
63387
|
+
// src/a2a-log-gate.ts
|
|
63388
|
+
function a2aChannelSignature(channels) {
|
|
63389
|
+
return JSON.stringify(
|
|
63390
|
+
channels.map((c2) => ({
|
|
63391
|
+
id: c2.channelId?.slice(0, 8),
|
|
63392
|
+
status: c2.status,
|
|
63393
|
+
participants: c2.participant_count
|
|
63394
|
+
})).sort((a2, b2) => (a2.id ?? "").localeCompare(b2.id ?? ""))
|
|
63395
|
+
);
|
|
63396
|
+
}
|
|
63397
|
+
var A2ALogGate;
|
|
63398
|
+
var init_a2a_log_gate = __esm({
|
|
63399
|
+
"src/a2a-log-gate.ts"() {
|
|
63400
|
+
"use strict";
|
|
63401
|
+
A2ALogGate = class {
|
|
63402
|
+
last = null;
|
|
63403
|
+
/** True when this state differs from the last reported one. Records it. */
|
|
63404
|
+
shouldReport(channels) {
|
|
63405
|
+
const sig = a2aChannelSignature(channels);
|
|
63406
|
+
if (sig === this.last) return false;
|
|
63407
|
+
this.last = sig;
|
|
63408
|
+
return true;
|
|
63409
|
+
}
|
|
63410
|
+
};
|
|
63411
|
+
}
|
|
63412
|
+
});
|
|
63413
|
+
|
|
63414
|
+
// src/mls-membership-probe.ts
|
|
63415
|
+
function mlsMembershipFromStatus(status) {
|
|
63416
|
+
if (status === 200) return true;
|
|
63417
|
+
if (status === 403 || status === 404) return false;
|
|
63418
|
+
return null;
|
|
63419
|
+
}
|
|
63420
|
+
var init_mls_membership_probe = __esm({
|
|
63421
|
+
"src/mls-membership-probe.ts"() {
|
|
63422
|
+
"use strict";
|
|
63423
|
+
}
|
|
63424
|
+
});
|
|
63425
|
+
|
|
63358
63426
|
// src/openclaw-compat.ts
|
|
63359
63427
|
var openclaw_compat_exports = {};
|
|
63360
63428
|
__export(openclaw_compat_exports, {
|
|
@@ -63917,6 +63985,8 @@ var init_channel = __esm({
|
|
|
63917
63985
|
await init_crypto_helpers();
|
|
63918
63986
|
await init_state();
|
|
63919
63987
|
init_transport2();
|
|
63988
|
+
init_a2a_log_gate();
|
|
63989
|
+
init_mls_membership_probe();
|
|
63920
63990
|
ROOM_AGENT_TYPES = /* @__PURE__ */ new Set([
|
|
63921
63991
|
"message",
|
|
63922
63992
|
"text",
|
|
@@ -63982,6 +64052,11 @@ var init_channel = __esm({
|
|
|
63982
64052
|
_heartbeatTimer = null;
|
|
63983
64053
|
_heartbeatCallback = null;
|
|
63984
64054
|
_heartbeatIntervalSeconds = 0;
|
|
64055
|
+
/** #798: report the A2A channel set only when it CHANGES. This line polls
|
|
64056
|
+
* every 30s and produced 90.2% of bridge.log (90,841/100,702 lines) and a
|
|
64057
|
+
* 27.9MB gateway.log on wren, every one reading `channels=0`. Not deleted —
|
|
64058
|
+
* #794's A2A watch needs the transitions. */
|
|
64059
|
+
_a2aLogGate = new A2ALogGate();
|
|
63985
64060
|
_wakeDetectorTimer = null;
|
|
63986
64061
|
_lastWakeTick = Date.now();
|
|
63987
64062
|
_trustToken = null;
|
|
@@ -63991,6 +64066,17 @@ var init_channel = __esm({
|
|
|
63991
64066
|
_pendingPollTimer = null;
|
|
63992
64067
|
_syncMessageIds = null;
|
|
63993
64068
|
_deliveryHeartbeat = null;
|
|
64069
|
+
/**
|
|
64070
|
+
* How often to re-check the KeyPackage pool against the server (#826).
|
|
64071
|
+
*
|
|
64072
|
+
* KeyPackages expire after 7 days and, before this, only a reconnect
|
|
64073
|
+
* republished them — so an agent that simply stayed up ran to zero usable
|
|
64074
|
+
* and silently stopped accepting new MLS 1:1 conversations while looking
|
|
64075
|
+
* perfectly healthy. 6h gives ~28 attempts inside one lifetime, so a run of
|
|
64076
|
+
* transient failures cannot age the pool out. Cost is one GET per interval.
|
|
64077
|
+
*/
|
|
64078
|
+
static _KP_REPLENISH_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
64079
|
+
_kpReplenishTimer = null;
|
|
63994
64080
|
_a2aReconcileTimer = null;
|
|
63995
64081
|
_deliveryPulling = false;
|
|
63996
64082
|
_drDeliveryPulling = false;
|
|
@@ -64955,7 +65041,7 @@ var init_channel = __esm({
|
|
|
64955
65041
|
*/
|
|
64956
65042
|
sendActivitySpan(spanData) {
|
|
64957
65043
|
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;
|
|
64958
|
-
const pluginVersion = true ? "0.23.
|
|
65044
|
+
const pluginVersion = true ? "0.23.21" : "0.0.0-dev";
|
|
64959
65045
|
const agentName = this.config.agentName ?? "Agent";
|
|
64960
65046
|
const resource = {
|
|
64961
65047
|
"service.name": "agentvault-agent",
|
|
@@ -65275,6 +65361,47 @@ var init_channel = __esm({
|
|
|
65275
65361
|
);
|
|
65276
65362
|
this.emit("rooms_reconciled", { pruned: stale });
|
|
65277
65363
|
}
|
|
65364
|
+
/**
|
|
65365
|
+
* #629 — quarantine MLS state for groups this device is no longer in.
|
|
65366
|
+
*
|
|
65367
|
+
* `_reconcileRoomsWithServer` above covers ROOMS: anything absent from
|
|
65368
|
+
* `GET /rooms` is pruned. It cannot see a group that was never a room — a 1:1
|
|
65369
|
+
* conversation group or an A2A channel group — so that state lingered with
|
|
65370
|
+
* nothing to reconcile it. Measured on loopita 2026-08-08: one group-state
|
|
65371
|
+
* file whose group had no server-side row at all.
|
|
65372
|
+
*
|
|
65373
|
+
* `quarantineOrphanedMlsState` was written for exactly this and shipped with
|
|
65374
|
+
* 11 passing tests, but nothing ever called it. This is that call site.
|
|
65375
|
+
*
|
|
65376
|
+
* Runs AFTER the room reconcile so rooms are already gone and this only sees
|
|
65377
|
+
* genuine non-room orphans.
|
|
65378
|
+
*
|
|
65379
|
+
* Quarantine RENAMES to `.orphaned` rather than deleting: if the verdict is
|
|
65380
|
+
* ever wrong the state is recoverable by hand, which deletion would not be.
|
|
65381
|
+
*/
|
|
65382
|
+
async _quarantineOrphanedMlsGroups() {
|
|
65383
|
+
if (!this._deviceJwt) return;
|
|
65384
|
+
const quarantined = await quarantineOrphanedMlsState(
|
|
65385
|
+
this.config.dataDir,
|
|
65386
|
+
async (groupId) => {
|
|
65387
|
+
try {
|
|
65388
|
+
const res = await fetch(
|
|
65389
|
+
`${this.config.apiUrl}/api/v1/mls/groups/${encodeURIComponent(groupId)}`,
|
|
65390
|
+
{ headers: { Authorization: `Bearer ${this._deviceJwt}` } }
|
|
65391
|
+
);
|
|
65392
|
+
return mlsMembershipFromStatus(res.status);
|
|
65393
|
+
} catch {
|
|
65394
|
+
return null;
|
|
65395
|
+
}
|
|
65396
|
+
}
|
|
65397
|
+
);
|
|
65398
|
+
if (quarantined.length > 0) {
|
|
65399
|
+
console.log(
|
|
65400
|
+
`[SecureChannel] Quarantined ${quarantined.length} orphaned MLS group state file(s): ` + quarantined.map((g2) => g2.slice(0, 8)).join(", ")
|
|
65401
|
+
);
|
|
65402
|
+
this.emit("mls_state_quarantined", { groupIds: quarantined });
|
|
65403
|
+
}
|
|
65404
|
+
}
|
|
65278
65405
|
/**
|
|
65279
65406
|
* Handle an in-band `{event:"error"}` frame from the server.
|
|
65280
65407
|
*
|
|
@@ -66435,7 +66562,9 @@ var init_channel = __esm({
|
|
|
66435
66562
|
}
|
|
66436
66563
|
const myHub = this._persisted.hubAddress || "";
|
|
66437
66564
|
const myHubId = this._persisted.hubId || "";
|
|
66438
|
-
|
|
66565
|
+
if (this._a2aLogGate.shouldReport(channels)) {
|
|
66566
|
+
console.log(`[listA2AChannels] myHub=${myHub}, channels=${channels.length}, raw=${JSON.stringify(channels.map((c2) => ({ id: c2.channelId?.slice(0, 8), status: c2.status, participants: c2.participant_count })))}`);
|
|
66567
|
+
}
|
|
66439
66568
|
for (const ch of channels) {
|
|
66440
66569
|
if (ch.status === "active" || ch.status === "approved") {
|
|
66441
66570
|
const myParticipant = ch.participants?.find(
|
|
@@ -66797,6 +66926,8 @@ var init_channel = __esm({
|
|
|
66797
66926
|
this._setState("ready");
|
|
66798
66927
|
void this._reconcileRoomsWithServer().catch(
|
|
66799
66928
|
(err) => console.warn(`[SecureChannel] room reconcile failed (ignored): ${err instanceof Error ? err.message : String(err)}`)
|
|
66929
|
+
).then(() => this._quarantineOrphanedMlsGroups()).catch(
|
|
66930
|
+
(err) => console.warn(`[SecureChannel] MLS orphan quarantine failed (ignored): ${err instanceof Error ? err.message : String(err)}`)
|
|
66800
66931
|
);
|
|
66801
66932
|
if (this.config.enableScanning) {
|
|
66802
66933
|
this._scanEngine = new ScanEngine();
|
|
@@ -66820,7 +66951,7 @@ var init_channel = __esm({
|
|
|
66820
66951
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
66821
66952
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
66822
66953
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
66823
|
-
pluginVersion: true ? "0.23.
|
|
66954
|
+
pluginVersion: true ? "0.23.21" : "0.0.0-dev"
|
|
66824
66955
|
});
|
|
66825
66956
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
66826
66957
|
}
|
|
@@ -66829,6 +66960,7 @@ var init_channel = __esm({
|
|
|
66829
66960
|
console.warn("[SecureChannel] A2A channel sync on connect failed:", err);
|
|
66830
66961
|
});
|
|
66831
66962
|
await this._ensureKpPoolPublished();
|
|
66963
|
+
this._startKpReplenishTimer();
|
|
66832
66964
|
this._pullDeliveryQueue().catch((err) => {
|
|
66833
66965
|
console.warn("[SecureChannel] Initial delivery pull failed:", err);
|
|
66834
66966
|
});
|
|
@@ -67143,7 +67275,7 @@ var init_channel = __esm({
|
|
|
67143
67275
|
agentVersion: this.config.agentVersion ?? "0.0.0",
|
|
67144
67276
|
// __AV_VERSION__ is injected by esbuild from package.json at build time.
|
|
67145
67277
|
// Falls back to "0.0.0-dev" in non-bundled contexts (tests).
|
|
67146
|
-
pluginVersion: true ? "0.23.
|
|
67278
|
+
pluginVersion: true ? "0.23.21" : "0.0.0-dev"
|
|
67147
67279
|
});
|
|
67148
67280
|
this._telemetryReporter.startAutoFlush(3e4);
|
|
67149
67281
|
}
|
|
@@ -67486,6 +67618,7 @@ var init_channel = __esm({
|
|
|
67486
67618
|
this._deliveryHeartbeat = null;
|
|
67487
67619
|
}
|
|
67488
67620
|
this._stopA2aReconcileTimer();
|
|
67621
|
+
this._stopKpReplenishTimer();
|
|
67489
67622
|
if (this._stopped) return;
|
|
67490
67623
|
this._setState("disconnected");
|
|
67491
67624
|
this._scheduleReconnect();
|
|
@@ -68879,6 +69012,103 @@ ${messageText}`;
|
|
|
68879
69012
|
this._kpPoolFilling = false;
|
|
68880
69013
|
}
|
|
68881
69014
|
}
|
|
69015
|
+
/** Start the periodic KeyPackage replenish (#826). Idempotent. */
|
|
69016
|
+
_startKpReplenishTimer() {
|
|
69017
|
+
this._stopKpReplenishTimer();
|
|
69018
|
+
this._kpReplenishTimer = setInterval(() => {
|
|
69019
|
+
void this._replenishKpPoolFromServer();
|
|
69020
|
+
}, _SecureChannel._KP_REPLENISH_INTERVAL_MS);
|
|
69021
|
+
}
|
|
69022
|
+
_stopKpReplenishTimer() {
|
|
69023
|
+
if (this._kpReplenishTimer) {
|
|
69024
|
+
clearInterval(this._kpReplenishTimer);
|
|
69025
|
+
this._kpReplenishTimer = null;
|
|
69026
|
+
}
|
|
69027
|
+
}
|
|
69028
|
+
/**
|
|
69029
|
+
* The server's EXPIRY-AWARE usable KeyPackage count, or null when unknown.
|
|
69030
|
+
*
|
|
69031
|
+
* `count_unconsumed` filters `expires_at > utcnow()`, which is the only count
|
|
69032
|
+
* that reflects what a peer can actually fetch. Returns null (not 0) on any
|
|
69033
|
+
* failure so callers can tell "server says empty" from "we could not ask" —
|
|
69034
|
+
* guessing 0 here would publish a fresh pool on every transient 503.
|
|
69035
|
+
*/
|
|
69036
|
+
async _fetchServerKpCount() {
|
|
69037
|
+
try {
|
|
69038
|
+
const res = await fetch(
|
|
69039
|
+
`${this.config.apiUrl}/api/v1/mls/key-packages/count?device_id=${this._deviceId}`,
|
|
69040
|
+
{ headers: { Authorization: `Bearer ${this._deviceJwt}` } }
|
|
69041
|
+
);
|
|
69042
|
+
if (!res.ok) return null;
|
|
69043
|
+
const body = await res.json();
|
|
69044
|
+
const n2 = body?.count;
|
|
69045
|
+
return typeof n2 === "number" && Number.isFinite(n2) ? n2 : null;
|
|
69046
|
+
} catch {
|
|
69047
|
+
return null;
|
|
69048
|
+
}
|
|
69049
|
+
}
|
|
69050
|
+
/**
|
|
69051
|
+
* Top the pool up against the SERVER's count (#826).
|
|
69052
|
+
*
|
|
69053
|
+
* ⚠️ DO NOT replace this with a periodic call to `_ensureKpPoolPublished()`.
|
|
69054
|
+
* That function early-returns on `_pendingKpBundles.length >= _KP_POOL_TARGET`,
|
|
69055
|
+
* and that array holds PRIVATE bundles which never expire locally — so on an
|
|
69056
|
+
* agent that has been connected longer than the KeyPackage lifetime it is
|
|
69057
|
+
* still full while the server has zero usable. Measured on prod 2026-08-09:
|
|
69058
|
+
* cortina and eclaude were online and green with 10 unconsumed / 0 usable,
|
|
69059
|
+
* unable to accept any new MLS 1:1. A timer over the local guard would have
|
|
69060
|
+
* run forever and changed nothing.
|
|
69061
|
+
*
|
|
69062
|
+
* Publishes only the shortfall, and never throws — this runs on a timer and
|
|
69063
|
+
* must not be able to take the channel down.
|
|
69064
|
+
*/
|
|
69065
|
+
async _replenishKpPoolFromServer() {
|
|
69066
|
+
try {
|
|
69067
|
+
if (this._kpPoolFilling) return;
|
|
69068
|
+
const usable = await this._fetchServerKpCount();
|
|
69069
|
+
if (usable === null) return;
|
|
69070
|
+
const shortfall = _SecureChannel._KP_POOL_TARGET - usable;
|
|
69071
|
+
if (shortfall <= 0) return;
|
|
69072
|
+
this._kpPoolFilling = true;
|
|
69073
|
+
try {
|
|
69074
|
+
const identity = new TextEncoder().encode(this._deviceId);
|
|
69075
|
+
let published = 0;
|
|
69076
|
+
for (let i2 = 0; i2 < shortfall; i2++) {
|
|
69077
|
+
const mlsMgr = new MLSGroupManager();
|
|
69078
|
+
const kp = await mlsMgr.generateKeyPackage(identity);
|
|
69079
|
+
const kpHex = Buffer.from(
|
|
69080
|
+
MLSGroupManager.serializeKeyPackage(kp.publicPackage)
|
|
69081
|
+
).toString("hex");
|
|
69082
|
+
const res = await fetch(`${this.config.apiUrl}/api/v1/mls/key-packages`, {
|
|
69083
|
+
method: "POST",
|
|
69084
|
+
headers: {
|
|
69085
|
+
Authorization: `Bearer ${this._deviceJwt}`,
|
|
69086
|
+
"Content-Type": "application/json"
|
|
69087
|
+
},
|
|
69088
|
+
body: JSON.stringify({ key_package: kpHex })
|
|
69089
|
+
});
|
|
69090
|
+
if (!res.ok) {
|
|
69091
|
+
console.warn(
|
|
69092
|
+
`[SecureChannel] KeyPackage replenish rejected (status ${res.status}) after ${published}/${shortfall}`
|
|
69093
|
+
);
|
|
69094
|
+
break;
|
|
69095
|
+
}
|
|
69096
|
+
this._recordPublishedKp(kp);
|
|
69097
|
+
if (!this._mlsKeyPackage) this._mlsKeyPackage = kp;
|
|
69098
|
+
published++;
|
|
69099
|
+
}
|
|
69100
|
+
if (published > 0) {
|
|
69101
|
+
console.log(
|
|
69102
|
+
`[SecureChannel] MLS KeyPackage pool replenished (${published} new; server had ${usable}/${_SecureChannel._KP_POOL_TARGET} usable)`
|
|
69103
|
+
);
|
|
69104
|
+
}
|
|
69105
|
+
} finally {
|
|
69106
|
+
this._kpPoolFilling = false;
|
|
69107
|
+
}
|
|
69108
|
+
} catch (err) {
|
|
69109
|
+
console.warn("[SecureChannel] KeyPackage replenish failed:", err);
|
|
69110
|
+
}
|
|
69111
|
+
}
|
|
68882
69112
|
async _handleMlsWelcome(data) {
|
|
68883
69113
|
const groupId = data.group_id;
|
|
68884
69114
|
const conversationId = data.conversation_id;
|
|
@@ -97472,7 +97702,7 @@ var init_index = __esm({
|
|
|
97472
97702
|
init_skill_invoker();
|
|
97473
97703
|
await init_skill_telemetry();
|
|
97474
97704
|
await init_policy_enforcer();
|
|
97475
|
-
VERSION = true ? "0.23.
|
|
97705
|
+
VERSION = true ? "0.23.21" : "0.0.0-dev";
|
|
97476
97706
|
}
|
|
97477
97707
|
});
|
|
97478
97708
|
await init_index();
|