@securecoms/agent-bridge 0.8.0 → 0.10.0
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/bin.js +442 -88
- package/dist/bridge-agent.d.ts +62 -0
- package/dist/config.d.ts +4 -0
- package/dist/http-server.d.ts +4 -4
- package/dist/index.js +417 -63
- package/dist/memory/openclaw-provider.d.ts +10 -1
- package/dist/memory/openclaw-workspace-facts.d.ts +38 -0
- package/dist/memory/provider.d.ts +2 -1
- package/package.json +1 -1
- package/connectors/hermes/__pycache__/__init__.cpython-311.pyc +0 -0
- package/connectors/hermes/__pycache__/adapter.cpython-311.pyc +0 -0
- package/connectors/hermes/__pycache__/gate.cpython-311.pyc +0 -0
- package/connectors/hermes/__pycache__/securecoms_gate.cpython-311.pyc +0 -0
- package/connectors/hermes/__pycache__/securecoms_transport.cpython-311.pyc +0 -0
- package/connectors/hermes/__pycache__/transport.cpython-311.pyc +0 -0
- package/connectors/hermes/tests/__pycache__/conftest.cpython-311-pytest-8.4.0.pyc +0 -0
- package/connectors/hermes/tests/__pycache__/test_adapter_wiring.cpython-311-pytest-8.4.0.pyc +0 -0
- package/connectors/hermes/tests/__pycache__/test_gate.cpython-311-pytest-8.4.0.pyc +0 -0
- package/connectors/hermes/tests/__pycache__/test_securecoms_gate.cpython-311-pytest-8.4.0.pyc +0 -0
- package/connectors/hermes/tests/__pycache__/test_securecoms_transport.cpython-311-pytest-8.4.0.pyc +0 -0
- package/connectors/hermes/tests/__pycache__/test_transport.cpython-311-pytest-8.4.0.pyc +0 -0
package/dist/bin.js
CHANGED
|
@@ -27,6 +27,8 @@ function loadConfig(opts) {
|
|
|
27
27
|
let memoryProvider;
|
|
28
28
|
let memoryBaseDir;
|
|
29
29
|
let memoryAgent;
|
|
30
|
+
let memoryWorkspaceDir;
|
|
31
|
+
let memoryFactsCharBudget;
|
|
30
32
|
if (memProvRaw === "none") {
|
|
31
33
|
memoryProvider = "none";
|
|
32
34
|
} else if (memProvRaw === "hermes") {
|
|
@@ -40,6 +42,10 @@ function loadConfig(opts) {
|
|
|
40
42
|
memoryBaseDir = typeof bd === "string" && bd ? bd : "~/.openclaw";
|
|
41
43
|
const ag = oc?.agent;
|
|
42
44
|
memoryAgent = typeof ag === "string" && ag ? ag : void 0;
|
|
45
|
+
const wd = oc?.workspace_dir;
|
|
46
|
+
memoryWorkspaceDir = typeof wd === "string" && wd ? wd : void 0;
|
|
47
|
+
const fb = oc?.facts_char_budget;
|
|
48
|
+
memoryFactsCharBudget = typeof fb === "number" && Number.isInteger(fb) && fb >= 0 ? fb : void 0;
|
|
43
49
|
} else {
|
|
44
50
|
memoryProvider = "securecoms";
|
|
45
51
|
}
|
|
@@ -56,7 +62,9 @@ function loadConfig(opts) {
|
|
|
56
62
|
rateLimitRefillPerMinute,
|
|
57
63
|
memoryProvider,
|
|
58
64
|
memoryBaseDir,
|
|
59
|
-
memoryAgent
|
|
65
|
+
memoryAgent,
|
|
66
|
+
memoryWorkspaceDir,
|
|
67
|
+
memoryFactsCharBudget
|
|
60
68
|
};
|
|
61
69
|
}
|
|
62
70
|
function arg(argv, flag) {
|
|
@@ -1379,6 +1387,18 @@ var LeafRosterMismatchError = class extends Error {
|
|
|
1379
1387
|
this.name = "LeafRosterMismatchError";
|
|
1380
1388
|
}
|
|
1381
1389
|
};
|
|
1390
|
+
var AmbiguousOwnLeafError = class extends Error {
|
|
1391
|
+
deviceId;
|
|
1392
|
+
totalLeaves;
|
|
1393
|
+
candidateLeaves;
|
|
1394
|
+
constructor(deviceId, totalLeaves, candidateLeaves) {
|
|
1395
|
+
super(`removeDeviceFromLiveGroup: cannot identify device ${deviceId}'s leaf \u2014 the identity owns ${totalLeaves} leaves and ${candidateLeaves} remain after excluding this committer's own. Leaf credentials carry the identity, not the device, so the target is genuinely ambiguous. Refusing rather than evicting a device that was never revoked.`);
|
|
1396
|
+
this.deviceId = deviceId;
|
|
1397
|
+
this.totalLeaves = totalLeaves;
|
|
1398
|
+
this.candidateLeaves = candidateLeaves;
|
|
1399
|
+
this.name = "AmbiguousOwnLeafError";
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1382
1402
|
var HealAddAfterRemoveError = class extends Error {
|
|
1383
1403
|
constructor(cause) {
|
|
1384
1404
|
super("healDivergedMember: Add failed after Remove already landed \u2014 member detached, re-run heal to recover");
|
|
@@ -1398,7 +1418,7 @@ var ForgedCredentialError = class extends Error {
|
|
|
1398
1418
|
this.name = "ForgedCredentialError";
|
|
1399
1419
|
}
|
|
1400
1420
|
};
|
|
1401
|
-
var MlsGroupKeyProvider = class {
|
|
1421
|
+
var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
1402
1422
|
o;
|
|
1403
1423
|
suite;
|
|
1404
1424
|
managers = /* @__PURE__ */ new Map();
|
|
@@ -1548,9 +1568,25 @@ var MlsGroupKeyProvider = class {
|
|
|
1548
1568
|
* Snapshot of per-channel decrypt-error / stuck state (ADR-0002 observability).
|
|
1549
1569
|
* Surfaces a wedged channel that would otherwise "look idle": the daemon serves
|
|
1550
1570
|
* it on GET /v1/metrics and the web shell reads it via a selector. A channel is
|
|
1551
|
-
* `stuck` once it has had `stuckThreshold`
|
|
1552
|
-
* intervening successful decrypt. Returns a plain object keyed by
|
|
1553
|
-
* absence of a key means zero errors observed for that channel.
|
|
1571
|
+
* `stuck` once it has had `stuckThreshold` GENUINELY-WEDGED decrypt failures
|
|
1572
|
+
* with no intervening successful decrypt. Returns a plain object keyed by
|
|
1573
|
+
* channelId; absence of a key means zero errors observed for that channel.
|
|
1574
|
+
*
|
|
1575
|
+
* `stuck` gates on `wedgedRun`, NOT `consecutiveErrors` — the same correction
|
|
1576
|
+
* the quarantine gate already got (P0a). `consecutiveErrors` is bumped at the
|
|
1577
|
+
* top of the catch for EVERY failure, INCLUDING the benign past-epoch
|
|
1578
|
+
* duplicate-commit churn that the `permanentlyDead` path immediately and
|
|
1579
|
+
* safely auto-drains. Deriving the user-visible health signal from it meant
|
|
1580
|
+
* ordinary at-least-once / committer-race traffic painted a perfectly healthy
|
|
1581
|
+
* channel red.
|
|
1582
|
+
*
|
|
1583
|
+
* That is the prod symptom of 2026-07-31: a freshly-welcomed federated partner
|
|
1584
|
+
* was queued the very Add commit its Welcome already contained. It joined the
|
|
1585
|
+
* correct group at the correct epoch, correctly identified the replayed commit
|
|
1586
|
+
* as dead and drained it — and still showed "This channel is stuck", because
|
|
1587
|
+
* the drain happened AFTER the error had been counted. `wedgedRun` counts only
|
|
1588
|
+
* rows left UN-acked (actually stuck), and a clean decrypt clears it, so a
|
|
1589
|
+
* genuine wedge still reports exactly as before.
|
|
1554
1590
|
*/
|
|
1555
1591
|
errorStats() {
|
|
1556
1592
|
const out = {};
|
|
@@ -1559,7 +1595,7 @@ var MlsGroupKeyProvider = class {
|
|
|
1559
1595
|
decryptErrors: e.decryptErrors,
|
|
1560
1596
|
consecutiveErrors: e.consecutiveErrors,
|
|
1561
1597
|
lastErrorAt: e.lastErrorAt,
|
|
1562
|
-
stuck: e.
|
|
1598
|
+
stuck: e.wedgedRun >= this.stuckThreshold,
|
|
1563
1599
|
quarantined: e.quarantined
|
|
1564
1600
|
};
|
|
1565
1601
|
}
|
|
@@ -2224,6 +2260,25 @@ var MlsGroupKeyProvider = class {
|
|
|
2224
2260
|
* `removed_device_id` (instead of removed_identity_id); the server resolves the
|
|
2225
2261
|
* device's identity, asserts it matches, and purges that ONE device.
|
|
2226
2262
|
*/
|
|
2263
|
+
/**
|
|
2264
|
+
* This committer's OWN leaf index in the given group's ratchet tree, or null
|
|
2265
|
+
* when it cannot be read.
|
|
2266
|
+
*
|
|
2267
|
+
* `MLSGroupManager.state` is marked `@internal` but is a declared (non-private)
|
|
2268
|
+
* field, and `state.privatePath.leafIndex` is the same value ts-mls itself uses
|
|
2269
|
+
* as the sender leaf when protecting application data. Reading it here avoids a
|
|
2270
|
+
* change to `@agentvault/crypto`; if a public accessor is added later this
|
|
2271
|
+
* should delegate to it.
|
|
2272
|
+
*
|
|
2273
|
+
* Returns null rather than throwing so every caller must decide explicitly what
|
|
2274
|
+
* an unknown own-leaf means. The one caller today FAILS CLOSED on null by
|
|
2275
|
+
* falling back to the positional path only for FOREIGN identities — it never
|
|
2276
|
+
* uses null to justify guessing among an own identity's leaves.
|
|
2277
|
+
*/
|
|
2278
|
+
ownLeafIndex(mgr) {
|
|
2279
|
+
const idx = mgr.state?.privatePath?.leafIndex;
|
|
2280
|
+
return typeof idx === "number" ? idx : null;
|
|
2281
|
+
}
|
|
2227
2282
|
async removeDeviceFromLiveGroup(channelId, deviceId, opts) {
|
|
2228
2283
|
return this.locks.run(channelId, () => this.evictOnThrow(channelId, () => this.removeDeviceFromLiveGroupUnlocked(channelId, deviceId, opts, 0)));
|
|
2229
2284
|
}
|
|
@@ -2236,7 +2291,17 @@ var MlsGroupKeyProvider = class {
|
|
|
2236
2291
|
if (leaves.length !== opts.orderedDeviceIds.length) {
|
|
2237
2292
|
throw new LeafRosterMismatchError();
|
|
2238
2293
|
}
|
|
2239
|
-
|
|
2294
|
+
let leafIndex;
|
|
2295
|
+
const ownLeaf = this.ownLeafIndex(mgr);
|
|
2296
|
+
if (ownLeaf !== null && leaves.includes(ownLeaf)) {
|
|
2297
|
+
const others = leaves.filter((i) => i !== ownLeaf);
|
|
2298
|
+
if (others.length !== 1) {
|
|
2299
|
+
throw new AmbiguousOwnLeafError(deviceId, leaves.length, others.length);
|
|
2300
|
+
}
|
|
2301
|
+
leafIndex = others[0];
|
|
2302
|
+
} else {
|
|
2303
|
+
leafIndex = leaves[pos];
|
|
2304
|
+
}
|
|
2240
2305
|
const init = await this.o.backend.initConnectChannelGroup(channelId, [this.o.deviceId]);
|
|
2241
2306
|
const priorEpoch = Number(mgr.epoch);
|
|
2242
2307
|
const { commit } = await mgr.removeMembers([leafIndex]);
|
|
@@ -2489,7 +2554,7 @@ var MlsGroupKeyProvider = class {
|
|
|
2489
2554
|
});
|
|
2490
2555
|
await this.o.store.save(channelId, mgr.exportState());
|
|
2491
2556
|
this.managers.set(channelId, mgr);
|
|
2492
|
-
|
|
2557
|
+
return { staleMemberDeviceIds: reinit.stale_member_device_ids ?? [] };
|
|
2493
2558
|
});
|
|
2494
2559
|
}
|
|
2495
2560
|
/** Returns true when an in-memory MLS manager is live for the channel.
|
|
@@ -2565,7 +2630,60 @@ var MlsGroupKeyProvider = class {
|
|
|
2565
2630
|
* ⇒ refuse, never call verify), then structural signer check, then attestation
|
|
2566
2631
|
* verify against the NON-EMPTY pinned key, then post-join group-id assertion.
|
|
2567
2632
|
*/
|
|
2633
|
+
/** C4 (split-brain Task 4): back-off for REFUSED supersedes, keyed
|
|
2634
|
+
* `${channelId}:${groupId}`. A refused supersede leaves its row un-acked by
|
|
2635
|
+
* design (it is not a stray), but pre-back-off that meant a full re-verify —
|
|
2636
|
+
* including a fetchResetAttestation HTTP round-trip — on EVERY receive poll,
|
|
2637
|
+
* forever, for a request that can never succeed (the nine production split
|
|
2638
|
+
* channels all carry NULL-attestation legacy groups).
|
|
2639
|
+
*
|
|
2640
|
+
* ⚠️ ONLY POST-FETCH refusals enter the back-off. The step-0 pin guard and
|
|
2641
|
+
* the retired-group check run BEFORE fetchResetAttestation, so they
|
|
2642
|
+
* contribute zero HTTP to the storm — and the pin guard's precondition is
|
|
2643
|
+
* TRANSIENT (cold owner cache / momentarily anomalous roster; the bridge
|
|
2644
|
+
* warms the cache before receiveOnce for exactly this reason). Backing a
|
|
2645
|
+
* pre-fetch refusal off buys nothing and converts a one-cycle stall into a
|
|
2646
|
+
* ten-minute outage on a valid recovery.
|
|
2647
|
+
*
|
|
2648
|
+
* Of the backed-off refusals: no-attestation, unparseable content,
|
|
2649
|
+
* channel/group mismatch and post-join mismatch are DETERMINISTIC per
|
|
2650
|
+
* (channel, group) — the attestation columns are written at group creation
|
|
2651
|
+
* and never change. Signer-mismatch, signature-verify and no-bundle are
|
|
2652
|
+
* ASSUMED stable for the window; they are pin- or bundle-dependent, but by
|
|
2653
|
+
* the time they run the fetch has already been paid, so suppressing the
|
|
2654
|
+
* window bounds the storm at the cost of a ≤10-minute delay if the pin or
|
|
2655
|
+
* bundle changes. Note the suppression means the reason is not RE-OBSERVED
|
|
2656
|
+
* during the window — the underlying precondition can change freely; the
|
|
2657
|
+
* window is a cost bound, not a claim of immutability.
|
|
2658
|
+
*
|
|
2659
|
+
* The reason lives in the VALUE for observability; the clearing rule lives
|
|
2660
|
+
* in the KEY: the owner minting a FIXED replacement group arrives with a
|
|
2661
|
+
* NEW group id — a different key, tried immediately. The un-openable path
|
|
2662
|
+
* (verified Welcome, KP mismatch) is EXEMPT: onSupersedeUnopenable's own
|
|
2663
|
+
* cooldown governs it. In-memory only: a reload/restart costs one extra
|
|
2664
|
+
* fetch per stranded row — reload-bounded, not poll-bounded. */
|
|
2665
|
+
supersedeRefusals = /* @__PURE__ */ new Map();
|
|
2666
|
+
static SUPERSEDE_REFUSAL_BACKOFF_MS = 10 * 6e4;
|
|
2667
|
+
/** True while (channelId, groupId) is inside its refusal back-off window.
|
|
2668
|
+
* @internal exposed for diagnostics only — not part of the stable surface. */
|
|
2669
|
+
supersedeBackoffActive(channelId, groupId) {
|
|
2670
|
+
const rec = this.supersedeRefusals.get(`${channelId}:${groupId}`);
|
|
2671
|
+
return rec !== void 0 && Date.now() < rec.until;
|
|
2672
|
+
}
|
|
2568
2673
|
async trySupersede(channelId, row, heldGroupId) {
|
|
2674
|
+
const refuse = (msg) => {
|
|
2675
|
+
this.recordDecryptError(row, new Error(msg));
|
|
2676
|
+
const now = Date.now();
|
|
2677
|
+
for (const [k, v] of this.supersedeRefusals) {
|
|
2678
|
+
if (now >= v.until)
|
|
2679
|
+
this.supersedeRefusals.delete(k);
|
|
2680
|
+
}
|
|
2681
|
+
this.supersedeRefusals.set(`${channelId}:${row.group_id}`, {
|
|
2682
|
+
reason: msg,
|
|
2683
|
+
until: now + _MlsGroupKeyProvider.SUPERSEDE_REFUSAL_BACKOFF_MS
|
|
2684
|
+
});
|
|
2685
|
+
return false;
|
|
2686
|
+
};
|
|
2569
2687
|
const owner = this.o.onResolvePinnedOwner ? await this.o.onResolvePinnedOwner(channelId) : null;
|
|
2570
2688
|
if (!owner || !owner.publicKeyHex) {
|
|
2571
2689
|
this.recordDecryptError(row, new Error("supersede refused: no pinned owner key (step-0 guard)"));
|
|
@@ -2578,33 +2696,27 @@ var MlsGroupKeyProvider = class {
|
|
|
2578
2696
|
}
|
|
2579
2697
|
const att = await this.o.backend.fetchResetAttestation(row.group_id);
|
|
2580
2698
|
if (!att) {
|
|
2581
|
-
|
|
2582
|
-
return false;
|
|
2699
|
+
return refuse(`supersede refused: no attestation for group ${row.group_id}`);
|
|
2583
2700
|
}
|
|
2584
2701
|
if (att.signerIdentityId !== owner.identityId) {
|
|
2585
|
-
|
|
2586
|
-
return false;
|
|
2702
|
+
return refuse("supersede refused: signer is not the pinned owner");
|
|
2587
2703
|
}
|
|
2588
2704
|
let content;
|
|
2589
2705
|
try {
|
|
2590
2706
|
content = JSON.parse(att.signedContent);
|
|
2591
2707
|
} catch {
|
|
2592
|
-
|
|
2593
|
-
return false;
|
|
2708
|
+
return refuse("supersede refused: unparseable signed_content");
|
|
2594
2709
|
}
|
|
2595
2710
|
if (content.channel_id !== row.connect_channel_id || content.new_group_id !== row.group_id) {
|
|
2596
|
-
|
|
2597
|
-
return false;
|
|
2711
|
+
return refuse("supersede refused: attestation channel/group mismatch");
|
|
2598
2712
|
}
|
|
2599
2713
|
const ok = await verifyApprovalArtifact({ content, signature: att.signatureHex, signerPublicKey: owner.publicKeyHex, createdAt: "", version: 1 }, owner.publicKeyHex);
|
|
2600
2714
|
if (!ok) {
|
|
2601
|
-
|
|
2602
|
-
return false;
|
|
2715
|
+
return refuse("supersede refused: signature does not verify against pinned owner key");
|
|
2603
2716
|
}
|
|
2604
2717
|
const bundle = await this.o.store.loadPendingBundle(this.o.deviceId);
|
|
2605
2718
|
if (!bundle) {
|
|
2606
|
-
|
|
2607
|
-
return false;
|
|
2719
|
+
return refuse("supersede refused: no pending KeyPackage bundle to open the new Welcome");
|
|
2608
2720
|
}
|
|
2609
2721
|
return await this.locks.run(channelId, async () => {
|
|
2610
2722
|
const mgr = new MLSGroupManager();
|
|
@@ -2629,14 +2741,14 @@ var MlsGroupKeyProvider = class {
|
|
|
2629
2741
|
return false;
|
|
2630
2742
|
}
|
|
2631
2743
|
if (new TextDecoder().decode(mgr.groupId) !== row.group_id) {
|
|
2632
|
-
|
|
2633
|
-
return false;
|
|
2744
|
+
return refuse("supersede refused: post-join group id != attested new_group_id");
|
|
2634
2745
|
}
|
|
2635
2746
|
this.evictChannelState(channelId);
|
|
2636
2747
|
await this.o.store.delete(channelId);
|
|
2637
2748
|
await this.o.store.discardStaging(channelId);
|
|
2638
2749
|
await this.o.store.addRetiredGroupId(channelId, heldGroupId);
|
|
2639
2750
|
this.errors.delete(channelId);
|
|
2751
|
+
this.supersedeRefusals.delete(`${channelId}:${row.group_id}`);
|
|
2640
2752
|
this.managers.set(channelId, mgr);
|
|
2641
2753
|
await this.o.store.save(channelId, mgr.exportState());
|
|
2642
2754
|
await this.o.backend.ackDelivery(this.o.deviceId, [row.queue_id]);
|
|
@@ -2673,9 +2785,15 @@ var MlsGroupKeyProvider = class {
|
|
|
2673
2785
|
if (heldGroupId !== null && heldGroupId === row.group_id) {
|
|
2674
2786
|
handshakeAcked.push(row.queue_id);
|
|
2675
2787
|
} else if (heldGroupId !== null && heldGroupId !== row.group_id) {
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2788
|
+
if (this.supersedeBackoffActive(wcid, row.group_id)) {
|
|
2789
|
+
if (typeof process !== "undefined" && process.env?.SECURECOMS_TRACE_DELIVERY) {
|
|
2790
|
+
console.warn(`[trace] welcome ch=${wcid.slice(0, 8)} g=${row.group_id.slice(0, 8)} -> SKIP(refusal back-off)`);
|
|
2791
|
+
}
|
|
2792
|
+
} else {
|
|
2793
|
+
const ok = await this.trySupersede(wcid, row, heldGroupId);
|
|
2794
|
+
if (ok)
|
|
2795
|
+
supersededChannelIds.push(wcid);
|
|
2796
|
+
}
|
|
2679
2797
|
}
|
|
2680
2798
|
} else if (typeof process !== "undefined" && process.env?.SECURECOMS_TRACE_DELIVERY) {
|
|
2681
2799
|
console.warn(`[trace] welcome ch=${wcid.slice(0, 8)} rowGroup=${(row.group_id || "-").slice(0, 8)} -> NO-LOCAL-STATE (leave un-acked for join path)`);
|
|
@@ -3378,19 +3496,8 @@ var RestMlsBackend = class {
|
|
|
3378
3496
|
created: j.created === void 0 ? true : Boolean(j.created)
|
|
3379
3497
|
};
|
|
3380
3498
|
}
|
|
3381
|
-
async reinitConnectChannelGroup(
|
|
3382
|
-
|
|
3383
|
-
if (!committer)
|
|
3384
|
-
throw new Error("reinitConnectChannelGroup: empty member set");
|
|
3385
|
-
const j = await this.post(`/api/v1/mls/connect-channels/${channelId}/reinit`, {
|
|
3386
|
-
member_device_ids: memberDeviceIds,
|
|
3387
|
-
committer_device_id: committer
|
|
3388
|
-
});
|
|
3389
|
-
return {
|
|
3390
|
-
group_id: String(j.group_id),
|
|
3391
|
-
current_epoch: Number(j.current_epoch),
|
|
3392
|
-
member_device_ids: Array.isArray(j.member_device_ids) ? j.member_device_ids.map(String) : []
|
|
3393
|
-
};
|
|
3499
|
+
async reinitConnectChannelGroup() {
|
|
3500
|
+
throw new Error("reinitConnectChannelGroup was removed (it minted unattested MLS groups that orphaned channel members permanently). Use resetReinitConnectChannelGroup with an owner attestation.");
|
|
3394
3501
|
}
|
|
3395
3502
|
async resetReinitConnectChannelGroup(channelId, memberDeviceIds, att) {
|
|
3396
3503
|
const committer = memberDeviceIds[0];
|
|
@@ -3408,7 +3515,11 @@ var RestMlsBackend = class {
|
|
|
3408
3515
|
return {
|
|
3409
3516
|
group_id: String(j.group_id),
|
|
3410
3517
|
current_epoch: Number(j.current_epoch),
|
|
3411
|
-
member_device_ids: Array.isArray(j.member_device_ids) ? j.member_device_ids.map(String) : []
|
|
3518
|
+
member_device_ids: Array.isArray(j.member_device_ids) ? j.member_device_ids.map(String) : [],
|
|
3519
|
+
// Absent on a backend that predates the dropped-member report; an empty
|
|
3520
|
+
// list is the correct read of "the server told us nothing", since the old
|
|
3521
|
+
// server also never dropped anyone for an expired KeyPackage.
|
|
3522
|
+
stale_member_device_ids: Array.isArray(j.stale_member_device_ids) ? j.stale_member_device_ids.map(String) : []
|
|
3412
3523
|
};
|
|
3413
3524
|
}
|
|
3414
3525
|
async fetchKeyPackages(deviceIds) {
|
|
@@ -3947,7 +4058,7 @@ function loadPackageMeta(importMetaUrl) {
|
|
|
3947
4058
|
|
|
3948
4059
|
// src/bridge-agent.ts
|
|
3949
4060
|
import { promises as fsp } from "node:fs";
|
|
3950
|
-
import { join as
|
|
4061
|
+
import { join as join8, dirname as dirname3 } from "node:path";
|
|
3951
4062
|
|
|
3952
4063
|
// src/latency.ts
|
|
3953
4064
|
var DEFAULT_CAPACITY = 1024;
|
|
@@ -4460,29 +4571,153 @@ function readRecentTurns(baseDir, agent, sessionId, k) {
|
|
|
4460
4571
|
return k > 0 ? turns.slice(-k) : [];
|
|
4461
4572
|
}
|
|
4462
4573
|
|
|
4574
|
+
// src/memory/openclaw-workspace-facts.ts
|
|
4575
|
+
import { readdirSync, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
|
|
4576
|
+
import { join as join7 } from "node:path";
|
|
4577
|
+
var DEFAULT_FACTS_BUDGET = 8e3;
|
|
4578
|
+
var MEMORY_MD_CAP = 6e3;
|
|
4579
|
+
var MIN_FRAGMENT = 200;
|
|
4580
|
+
var MAX_SOURCE_BYTES = 1024 * 1024;
|
|
4581
|
+
var DAILY_RE = /^\d{4}-\d{2}-\d{2}\.md$/;
|
|
4582
|
+
function resolveWorkspaceDir(baseDir, agent, override) {
|
|
4583
|
+
if (override) return expandHome(override);
|
|
4584
|
+
const base = expandHome(baseDir);
|
|
4585
|
+
const isMain2 = agent === "" || agent === "main";
|
|
4586
|
+
try {
|
|
4587
|
+
const cfg = JSON.parse(readFileSync5(join7(base, "openclaw.json"), "utf8"));
|
|
4588
|
+
const agents = cfg?.agents;
|
|
4589
|
+
if (agents && typeof agents === "object") {
|
|
4590
|
+
const list = Array.isArray(agents.list) ? agents.list : [];
|
|
4591
|
+
const wanted = isMain2 ? "main" : agent;
|
|
4592
|
+
const entry = list.find((e) => e && typeof e === "object" && e.id === wanted);
|
|
4593
|
+
const ws = entry?.workspace;
|
|
4594
|
+
if (typeof ws === "string" && ws) return expandHome(ws);
|
|
4595
|
+
const def = agents.defaults?.workspace;
|
|
4596
|
+
if (typeof def === "string" && def) return expandHome(def);
|
|
4597
|
+
}
|
|
4598
|
+
} catch {
|
|
4599
|
+
}
|
|
4600
|
+
return isMain2 ? join7(base, "workspace") : join7(base, `workspace-${agent}`);
|
|
4601
|
+
}
|
|
4602
|
+
var TRUNC = "\u2026[truncated]";
|
|
4603
|
+
function headSlice(s, n) {
|
|
4604
|
+
if (s.length <= n) return { text: s, clipped: false };
|
|
4605
|
+
let out = s.slice(0, n);
|
|
4606
|
+
const last = out.charCodeAt(out.length - 1);
|
|
4607
|
+
if (last >= 55296 && last <= 56319) out = out.slice(0, -1);
|
|
4608
|
+
return { text: out, clipped: true };
|
|
4609
|
+
}
|
|
4610
|
+
function tailSlice(s, n) {
|
|
4611
|
+
if (s.length <= n) return { text: s, clipped: false };
|
|
4612
|
+
let out = s.slice(s.length - n);
|
|
4613
|
+
const first = out.charCodeAt(0);
|
|
4614
|
+
if (first >= 56320 && first <= 57343) out = out.slice(1);
|
|
4615
|
+
return { text: out, clipped: true };
|
|
4616
|
+
}
|
|
4617
|
+
function readBounded(path) {
|
|
4618
|
+
if (statSync2(path).size > MAX_SOURCE_BYTES) return { text: null, oversize: true };
|
|
4619
|
+
return { text: readFileSync5(path, "utf8"), oversize: false };
|
|
4620
|
+
}
|
|
4621
|
+
function readWorkspaceFacts(baseDir, agent, opts) {
|
|
4622
|
+
const raw = opts?.charBudget;
|
|
4623
|
+
const budget = typeof raw === "number" && Number.isInteger(raw) && raw >= 0 ? raw : DEFAULT_FACTS_BUDGET;
|
|
4624
|
+
if (budget === 0) return { facts: [], truncated: false, missing: false };
|
|
4625
|
+
const dir = resolveWorkspaceDir(baseDir, agent, opts?.workspaceDir);
|
|
4626
|
+
const facts = [];
|
|
4627
|
+
let truncated = false;
|
|
4628
|
+
let remaining = budget;
|
|
4629
|
+
try {
|
|
4630
|
+
const r = readBounded(join7(dir, "MEMORY.md"));
|
|
4631
|
+
if (r.oversize) truncated = true;
|
|
4632
|
+
else if (r.text && r.text.trim()) {
|
|
4633
|
+
const { text, clipped } = headSlice(r.text, Math.min(MEMORY_MD_CAP, budget));
|
|
4634
|
+
truncated ||= clipped;
|
|
4635
|
+
facts.push(`workspace MEMORY.md:
|
|
4636
|
+
${text}${clipped ? `
|
|
4637
|
+
${TRUNC}` : ""}`);
|
|
4638
|
+
remaining -= text.length;
|
|
4639
|
+
}
|
|
4640
|
+
} catch {
|
|
4641
|
+
}
|
|
4642
|
+
try {
|
|
4643
|
+
const dailies = readdirSync(join7(dir, "memory")).filter((f) => DAILY_RE.test(f)).sort().reverse().slice(0, 2);
|
|
4644
|
+
for (const f of dailies) {
|
|
4645
|
+
if (remaining < MIN_FRAGMENT) {
|
|
4646
|
+
truncated = true;
|
|
4647
|
+
break;
|
|
4648
|
+
}
|
|
4649
|
+
let r;
|
|
4650
|
+
try {
|
|
4651
|
+
r = readBounded(join7(dir, "memory", f));
|
|
4652
|
+
} catch {
|
|
4653
|
+
continue;
|
|
4654
|
+
}
|
|
4655
|
+
if (r.oversize) {
|
|
4656
|
+
truncated = true;
|
|
4657
|
+
continue;
|
|
4658
|
+
}
|
|
4659
|
+
if (!r.text || !r.text.trim()) continue;
|
|
4660
|
+
const { text, clipped } = tailSlice(r.text, remaining);
|
|
4661
|
+
truncated ||= clipped;
|
|
4662
|
+
facts.push(`workspace memory/${f} (daily):
|
|
4663
|
+
${clipped ? `${TRUNC}
|
|
4664
|
+
` : ""}${text}`);
|
|
4665
|
+
remaining -= text.length;
|
|
4666
|
+
}
|
|
4667
|
+
} catch {
|
|
4668
|
+
}
|
|
4669
|
+
return { facts, truncated, missing: facts.length === 0 };
|
|
4670
|
+
}
|
|
4671
|
+
|
|
4463
4672
|
// src/memory/openclaw-provider.ts
|
|
4464
4673
|
var DEFAULT_K2 = 200;
|
|
4465
4674
|
var OpenClawMemoryProvider = class {
|
|
4466
|
-
|
|
4675
|
+
// gauge: last successful returned total, including prefixes/markers
|
|
4676
|
+
constructor(store, baseDir, agent, factsOpts) {
|
|
4467
4677
|
this.store = store;
|
|
4468
4678
|
this.baseDir = baseDir;
|
|
4469
4679
|
this.agent = agent;
|
|
4680
|
+
this.factsOpts = factsOpts;
|
|
4470
4681
|
}
|
|
4471
4682
|
noSession = 0;
|
|
4472
4683
|
readFailed = 0;
|
|
4684
|
+
factsMissing = 0;
|
|
4685
|
+
// absence-shaped: resolved path missing or zero sources (per-call)
|
|
4686
|
+
factsReadFailed = 0;
|
|
4687
|
+
// anything THROWN out of the facts computation
|
|
4688
|
+
factsTruncated = 0;
|
|
4689
|
+
// budget clipped/dropped a source (incl. the 1MB skip)
|
|
4690
|
+
factsChars = 0;
|
|
4473
4691
|
async record(input) {
|
|
4474
4692
|
if (input.modality !== "voice") return { seq: 0, deduped: true };
|
|
4475
4693
|
return this.store.recordMemory({ ...input, text: `[voice] ${input.text}` });
|
|
4476
4694
|
}
|
|
4477
4695
|
async context(conversationId, limit) {
|
|
4478
4696
|
const k = limit ?? DEFAULT_K2;
|
|
4697
|
+
let wsFacts = [];
|
|
4698
|
+
try {
|
|
4699
|
+
const r = readWorkspaceFacts(this.baseDir, this.agent, {
|
|
4700
|
+
workspaceDir: this.factsOpts?.workspaceDir,
|
|
4701
|
+
charBudget: this.factsOpts?.factsCharBudget
|
|
4702
|
+
});
|
|
4703
|
+
wsFacts = r.facts;
|
|
4704
|
+
if (r.missing) this.factsMissing++;
|
|
4705
|
+
if (r.truncated) this.factsTruncated++;
|
|
4706
|
+
this.factsChars = r.facts.reduce((n, f) => n + f.length, 0);
|
|
4707
|
+
} catch {
|
|
4708
|
+
this.factsReadFailed++;
|
|
4709
|
+
}
|
|
4710
|
+
const withFacts = (res) => ({
|
|
4711
|
+
recent: res.recent,
|
|
4712
|
+
facts: [...wsFacts, ...res.facts]
|
|
4713
|
+
});
|
|
4479
4714
|
const voice = await this.store.memoryContext(conversationId, k);
|
|
4480
4715
|
let textTurns = [];
|
|
4481
4716
|
try {
|
|
4482
4717
|
const sid = resolveOwnerDmSession(this.baseDir, this.agent);
|
|
4483
4718
|
if (!sid) {
|
|
4484
4719
|
this.noSession++;
|
|
4485
|
-
return voice;
|
|
4720
|
+
return withFacts(voice);
|
|
4486
4721
|
}
|
|
4487
4722
|
textTurns = readRecentTurns(this.baseDir, this.agent, sid, k).map((t) => ({
|
|
4488
4723
|
seq: 0,
|
|
@@ -4492,7 +4727,7 @@ var OpenClawMemoryProvider = class {
|
|
|
4492
4727
|
}));
|
|
4493
4728
|
} catch {
|
|
4494
4729
|
this.readFailed++;
|
|
4495
|
-
return voice;
|
|
4730
|
+
return withFacts(voice);
|
|
4496
4731
|
}
|
|
4497
4732
|
const tagged = [
|
|
4498
4733
|
...textTurns.map((t, i) => ({ ...t, _src: 0, _i: i })),
|
|
@@ -4511,10 +4746,17 @@ var OpenClawMemoryProvider = class {
|
|
|
4511
4746
|
text: t.text,
|
|
4512
4747
|
ts: t.ts
|
|
4513
4748
|
}));
|
|
4514
|
-
return { recent, facts: voice.facts };
|
|
4749
|
+
return withFacts({ recent, facts: voice.facts });
|
|
4515
4750
|
}
|
|
4516
4751
|
stats() {
|
|
4517
|
-
return {
|
|
4752
|
+
return {
|
|
4753
|
+
openclaw_no_session: this.noSession,
|
|
4754
|
+
openclaw_read_failed: this.readFailed,
|
|
4755
|
+
openclaw_facts_missing: this.factsMissing,
|
|
4756
|
+
openclaw_facts_read_failed: this.factsReadFailed,
|
|
4757
|
+
openclaw_facts_truncated: this.factsTruncated,
|
|
4758
|
+
openclaw_facts_chars: this.factsChars
|
|
4759
|
+
};
|
|
4518
4760
|
}
|
|
4519
4761
|
};
|
|
4520
4762
|
|
|
@@ -4538,10 +4780,10 @@ var NullMemory = class {
|
|
|
4538
4780
|
return { recent: [], facts: [] };
|
|
4539
4781
|
}
|
|
4540
4782
|
};
|
|
4541
|
-
function createMemoryProvider(provider, store, baseDir, agent) {
|
|
4783
|
+
function createMemoryProvider(provider, store, baseDir, agent, openclawFacts) {
|
|
4542
4784
|
if (provider === "none") return new NullMemory();
|
|
4543
4785
|
if (provider === "hermes") return new HermesMemoryProvider(baseDir ?? "~/.hermes");
|
|
4544
|
-
if (provider === "openclaw") return new OpenClawMemoryProvider(store, baseDir ?? "~/.openclaw", agent ?? "");
|
|
4786
|
+
if (provider === "openclaw") return new OpenClawMemoryProvider(store, baseDir ?? "~/.openclaw", agent ?? "", openclawFacts);
|
|
4545
4787
|
return new SecureComsLocalMemory(store);
|
|
4546
4788
|
}
|
|
4547
4789
|
|
|
@@ -4649,7 +4891,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4649
4891
|
identityId: cfg.identityId,
|
|
4650
4892
|
cipherFor: () => new KeyedInboxCipher(deriveMlsStoreKey(seed))
|
|
4651
4893
|
});
|
|
4652
|
-
this.memory = createMemoryProvider(this.cfg.memoryProvider, this.store, this.cfg.memoryBaseDir, this.cfg.memoryAgent
|
|
4894
|
+
this.memory = createMemoryProvider(this.cfg.memoryProvider, this.store, this.cfg.memoryBaseDir, this.cfg.memoryAgent, {
|
|
4895
|
+
workspaceDir: this.cfg.memoryWorkspaceDir,
|
|
4896
|
+
factsCharBudget: this.cfg.memoryFactsCharBudget
|
|
4897
|
+
});
|
|
4653
4898
|
this.provider = new MlsGroupKeyProvider({
|
|
4654
4899
|
store: this.store,
|
|
4655
4900
|
backend: this.backend,
|
|
@@ -4729,7 +4974,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4729
4974
|
const j = await res.json();
|
|
4730
4975
|
return String(j.attachment_id);
|
|
4731
4976
|
};
|
|
4732
|
-
this.attachmentsDirImpl = deps.attachmentsDir ??
|
|
4977
|
+
this.attachmentsDirImpl = deps.attachmentsDir ?? join8(dirname3(cfg.storePath), "attachments");
|
|
4733
4978
|
this.action = {
|
|
4734
4979
|
begin: (channelId) => this.actionBegin(channelId),
|
|
4735
4980
|
start: (opts) => this.actionStart(opts),
|
|
@@ -5213,11 +5458,33 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
5213
5458
|
return;
|
|
5214
5459
|
}
|
|
5215
5460
|
this.providerCapabilityMissing = false;
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5461
|
+
const enclaves = await this.enclavesToCover();
|
|
5462
|
+
this.enclaveListEverSucceeded.add(this.cfg.enclaveId);
|
|
5463
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5464
|
+
const chans = [];
|
|
5465
|
+
const failed = [];
|
|
5466
|
+
let listedAny = false;
|
|
5467
|
+
for (const encId of enclaves) {
|
|
5468
|
+
try {
|
|
5469
|
+
for (const c of await this.listChannels(encId)) {
|
|
5470
|
+
if (seen.has(c.channel_id)) continue;
|
|
5471
|
+
seen.add(c.channel_id);
|
|
5472
|
+
chans.push(c);
|
|
5473
|
+
}
|
|
5474
|
+
listedAny = true;
|
|
5475
|
+
this.enclaveListEverSucceeded.add(encId);
|
|
5476
|
+
this.enclaveListWarned.delete(encId);
|
|
5477
|
+
} catch (e) {
|
|
5478
|
+
failed.push(encId);
|
|
5479
|
+
if (!this.enclaveListWarned.has(encId)) {
|
|
5480
|
+
this.enclaveListWarned.add(encId);
|
|
5481
|
+
console.warn(`[bridge] selfHealSweep: listChannels failed for enclave ${encId}; skipping (warn-once per streak):`, e);
|
|
5482
|
+
}
|
|
5483
|
+
}
|
|
5484
|
+
}
|
|
5485
|
+
this.sweepEnclavesFailed = failed;
|
|
5486
|
+
if (!listedAny) {
|
|
5487
|
+
console.warn("[bridge] selfHealSweep aborted: every enclave listing failed");
|
|
5221
5488
|
return;
|
|
5222
5489
|
}
|
|
5223
5490
|
for (const c of chans) {
|
|
@@ -5563,7 +5830,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
5563
5830
|
* the other enclaves in the set.
|
|
5564
5831
|
*/
|
|
5565
5832
|
async refreshChannelMeta() {
|
|
5566
|
-
const enclaves =
|
|
5833
|
+
const enclaves = await this.enclavesToCover();
|
|
5567
5834
|
const now = Date.now();
|
|
5568
5835
|
for (const encId of enclaves) {
|
|
5569
5836
|
try {
|
|
@@ -5576,6 +5843,89 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
5576
5843
|
}
|
|
5577
5844
|
}
|
|
5578
5845
|
}
|
|
5846
|
+
/** Cache for the server-side enclave enumeration (see enclavesToCover). */
|
|
5847
|
+
enclaveEnumCache = null;
|
|
5848
|
+
/** Enclaves whose channel listing failed on the NEWEST sweep (audit HIGH:
|
|
5849
|
+
* skip-one deleted the sweep's partial-coverage signal — observations_stale
|
|
5850
|
+
* is a global max, so one healthy enclave masked a failed one). Surfaced on
|
|
5851
|
+
* health() so partial coverage is never silent. */
|
|
5852
|
+
sweepEnclavesFailed = [];
|
|
5853
|
+
/** Enclave ids whose channel listing has EVER succeeded this process, seeded
|
|
5854
|
+
* with HOME (home is always expected to list; without the seed, a home
|
|
5855
|
+
* failure right after a restart would read as "never succeeded" and slip
|
|
5856
|
+
* through the expected-gap branch — weaker than the old abort-on-failure).
|
|
5857
|
+
* A failure of a member of this set is a REGRESSION and degrades health; a
|
|
5858
|
+
* failure of a never-succeeded enclave (partner: tenant-scoped 404, permanent
|
|
5859
|
+
* pending backend authz) is an EXPECTED gap — reported, not degrading, or
|
|
5860
|
+
* every federated agent would peg degraded forever. */
|
|
5861
|
+
enclaveListEverSucceeded = /* @__PURE__ */ new Set();
|
|
5862
|
+
/** Warn-once-per-failure-streak, keyed by enclave id (cleared on success):
|
|
5863
|
+
* a permanently-404ing partner enclave must not log 1440 lines a day. */
|
|
5864
|
+
enclaveListWarned = /* @__PURE__ */ new Set();
|
|
5865
|
+
static ENCLAVE_ENUM_TTL_MS = 5 * 6e4;
|
|
5866
|
+
enclaveEnumWarned = false;
|
|
5867
|
+
/**
|
|
5868
|
+
* C1 (split-brain Task 3): every enclave this agent must COVER — for the
|
|
5869
|
+
* self-heal sweep and for channel-meta refresh alike. ONE helper on purpose:
|
|
5870
|
+
* these two call sites had drifted (refreshChannelMeta looped home+learned,
|
|
5871
|
+
* selfHealSweep looped nothing), and the drift is exactly what left both
|
|
5872
|
+
* production agents with zero recovery coverage in their second enclave.
|
|
5873
|
+
*
|
|
5874
|
+
* The set is SERVER enumeration ∪ home ∪ store-learned:
|
|
5875
|
+
*
|
|
5876
|
+
* - SERVER (`GET /api/v1/enclaves`, membership-scoped, federated included)
|
|
5877
|
+
* is the authoritative source. The store CANNOT be: channel_meta's only
|
|
5878
|
+
* writers are listChannels itself (circular — you learn enclave B by
|
|
5879
|
+
* having already listed B) and the receive pump's successfully-decrypted
|
|
5880
|
+
* loop, and a FORKED agent decrypts nothing, so the agent that most needs
|
|
5881
|
+
* sweeping in enclave B is precisely the one whose store never learned B.
|
|
5882
|
+
* - home + learned stay in the union as the degraded-mode floor: if the
|
|
5883
|
+
* enumeration endpoint is down, we sweep what we can rather than nothing,
|
|
5884
|
+
* and we warn (once) so the gap is diagnosable.
|
|
5885
|
+
*
|
|
5886
|
+
* The server result is cached for ENCLAVE_ENUM_TTL_MS so per-ingest-cycle
|
|
5887
|
+
* callers (refreshChannelMeta) do not add an HTTP round-trip per cycle;
|
|
5888
|
+
* membership changes are picked up within one TTL, which matches the sweep
|
|
5889
|
+
* cadence this exists to serve.
|
|
5890
|
+
*
|
|
5891
|
+
* ⚠️ Residual gap, stated: the enumeration RETURNS cross-tenant partner
|
|
5892
|
+
* enclaves, but `list_channels_in_enclave` is tenant-scoped and 404s them
|
|
5893
|
+
* under this agent's bearer — so a partner enclave in this set still yields
|
|
5894
|
+
* no channels. Callers must treat a per-enclave listing failure as skip-one,
|
|
5895
|
+
* never abort-all. Closing that needs backend authz work, not daemon work.
|
|
5896
|
+
*/
|
|
5897
|
+
async enclavesToCover() {
|
|
5898
|
+
const now = Date.now();
|
|
5899
|
+
if (!this.enclaveEnumCache || now - this.enclaveEnumCache.at > _BridgeAgent.ENCLAVE_ENUM_TTL_MS) {
|
|
5900
|
+
try {
|
|
5901
|
+
const url = `${this.cfg.apiBase}/api/v1/enclaves?caller_identity_id=${encodeURIComponent(this.cfg.identityId)}`;
|
|
5902
|
+
const res = await this.fetchImpl(url, {
|
|
5903
|
+
headers: { authorization: `Bearer ${this.cfg.apiKey}` }
|
|
5904
|
+
});
|
|
5905
|
+
if (!res.ok) throw new Error(`list_enclaves failed: ${res.status}`);
|
|
5906
|
+
const rows = await res.json();
|
|
5907
|
+
this.enclaveEnumCache = { ids: rows.map((r) => r.enclave_id), at: Date.now() };
|
|
5908
|
+
this.enclaveEnumWarned = false;
|
|
5909
|
+
} catch (e) {
|
|
5910
|
+
if (!this.enclaveEnumWarned) {
|
|
5911
|
+
this.enclaveEnumWarned = true;
|
|
5912
|
+
console.warn(
|
|
5913
|
+
"[bridge] enclave enumeration failed; sweep coverage degraded to home+learned enclaves:",
|
|
5914
|
+
e
|
|
5915
|
+
);
|
|
5916
|
+
}
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5919
|
+
const fresh = this.enclaveEnumCache && Date.now() - this.enclaveEnumCache.at <= _BridgeAgent.ENCLAVE_ENUM_TTL_MS;
|
|
5920
|
+
if (fresh) {
|
|
5921
|
+
return /* @__PURE__ */ new Set([this.cfg.enclaveId, ...this.enclaveEnumCache.ids]);
|
|
5922
|
+
}
|
|
5923
|
+
return /* @__PURE__ */ new Set([
|
|
5924
|
+
this.cfg.enclaveId,
|
|
5925
|
+
...this.store.distinctEnclaveIds(),
|
|
5926
|
+
...this.enclaveEnumCache?.ids ?? []
|
|
5927
|
+
]);
|
|
5928
|
+
}
|
|
5579
5929
|
/** A live roster for a channel, or undefined if missing OR expired past the TTL. */
|
|
5580
5930
|
freshRoster(channelId) {
|
|
5581
5931
|
const hit = this.channelRoster.get(channelId);
|
|
@@ -6172,6 +6522,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
6172
6522
|
const stalePeriodMs = _BridgeAgent.SELF_HEAL_SWEEP_INTERVAL_MS + _BridgeAgent.SELF_HEAL_SWEEP_JITTER_MS;
|
|
6173
6523
|
const observationsStale = this.providerCapabilityMissing || channels.length === 0 || Date.now() - newest > stalePeriodMs;
|
|
6174
6524
|
const divergedChannels = channels.filter((o) => o.diverged === true).length;
|
|
6525
|
+
const sweepCoverageLost = this.sweepEnclavesFailed.some(
|
|
6526
|
+
(id) => this.enclaveListEverSucceeded.has(id)
|
|
6527
|
+
);
|
|
6175
6528
|
const liveChannels = channels.map((o) => ({
|
|
6176
6529
|
...o,
|
|
6177
6530
|
last_decrypt_at: this.provider.lastDecryptAtFor?.(o.channel_id) ?? o.last_decrypt_at
|
|
@@ -6186,8 +6539,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
6186
6539
|
observations_stale: observationsStale,
|
|
6187
6540
|
diverged_channels: divergedChannels,
|
|
6188
6541
|
// A2: the truthful roll-up gates read instead of `status` (D-3).
|
|
6189
|
-
degraded: divergedChannels > 0 || observationsStale,
|
|
6542
|
+
degraded: divergedChannels > 0 || observationsStale || sweepCoverageLost,
|
|
6190
6543
|
channels: liveChannels,
|
|
6544
|
+
sweep_enclaves_failed: [...this.sweepEnclavesFailed],
|
|
6191
6545
|
identity_id: this.cfg.identityId,
|
|
6192
6546
|
joined_channels: joined,
|
|
6193
6547
|
buffered: this.buffer.length,
|
|
@@ -6257,11 +6611,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
6257
6611
|
|
|
6258
6612
|
// src/auth.ts
|
|
6259
6613
|
import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
|
|
6260
|
-
import { existsSync, readFileSync as
|
|
6614
|
+
import { existsSync, readFileSync as readFileSync6, writeFileSync } from "node:fs";
|
|
6261
6615
|
function resolveToken(opts) {
|
|
6262
6616
|
if (opts.token) return opts.token;
|
|
6263
6617
|
const f = opts.tokenFile;
|
|
6264
|
-
if (f && existsSync(f)) return
|
|
6618
|
+
if (f && existsSync(f)) return readFileSync6(f, "utf8").trim();
|
|
6265
6619
|
const tok = randomBytes3(32).toString("base64url");
|
|
6266
6620
|
if (f) writeFileSync(f, tok, { mode: 384 });
|
|
6267
6621
|
return tok;
|
|
@@ -6832,8 +7186,8 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
6832
7186
|
}
|
|
6833
7187
|
|
|
6834
7188
|
// src/bin.ts
|
|
6835
|
-
import { readFileSync as
|
|
6836
|
-
import { join as
|
|
7189
|
+
import { readFileSync as readFileSync8, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, rmSync, existsSync as existsSync2, realpathSync, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
|
|
7190
|
+
import { join as join12, dirname as dirname7, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
|
|
6837
7191
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
6838
7192
|
import { homedir as homedir2, userInfo } from "node:os";
|
|
6839
7193
|
import { spawnSync } from "node:child_process";
|
|
@@ -7042,10 +7396,10 @@ function uninstallService(input, deps) {
|
|
|
7042
7396
|
}
|
|
7043
7397
|
|
|
7044
7398
|
// src/service/install-prep.ts
|
|
7045
|
-
import { join as
|
|
7399
|
+
import { join as join10 } from "node:path";
|
|
7046
7400
|
|
|
7047
7401
|
// src/service/wire.ts
|
|
7048
|
-
import { join as
|
|
7402
|
+
import { join as join9, dirname as dirname5 } from "node:path";
|
|
7049
7403
|
import { fileURLToPath } from "node:url";
|
|
7050
7404
|
|
|
7051
7405
|
// src/service/hermes-config.ts
|
|
@@ -7211,7 +7565,7 @@ function connectorTargetDir(home, harness) {
|
|
|
7211
7565
|
}
|
|
7212
7566
|
function connectorsSrcDir(importMetaUrl, harness) {
|
|
7213
7567
|
const pkgRoot = dirname5(dirname5(fileURLToPath(importMetaUrl)));
|
|
7214
|
-
return
|
|
7568
|
+
return join9(pkgRoot, "connectors", harness);
|
|
7215
7569
|
}
|
|
7216
7570
|
function hermesConfigPath(home) {
|
|
7217
7571
|
return `${home}/.hermes/config.yaml`;
|
|
@@ -7430,10 +7784,10 @@ function assertReadableCredential(absPath, deps) {
|
|
|
7430
7784
|
return { deviceId: json2.device_id, identityId: json2.identity_id, issuedAt };
|
|
7431
7785
|
}
|
|
7432
7786
|
function durableDir(home, slug) {
|
|
7433
|
-
return
|
|
7787
|
+
return join10(home, `securecoms-agent-${slug}`);
|
|
7434
7788
|
}
|
|
7435
7789
|
function durableCredentialPath(home, slug) {
|
|
7436
|
-
return
|
|
7790
|
+
return join10(durableDir(home, slug), `${slug}.json`);
|
|
7437
7791
|
}
|
|
7438
7792
|
function takeCustody(args2, deps) {
|
|
7439
7793
|
const dir = durableDir(args2.home, args2.slug);
|
|
@@ -7480,7 +7834,7 @@ function readSiblingDefs(dir, prefix, deps) {
|
|
|
7480
7834
|
for (const n of names) {
|
|
7481
7835
|
if (!n.startsWith(prefix)) continue;
|
|
7482
7836
|
try {
|
|
7483
|
-
out.push(deps.readFile(
|
|
7837
|
+
out.push(deps.readFile(join10(dir, n)));
|
|
7484
7838
|
} catch {
|
|
7485
7839
|
}
|
|
7486
7840
|
}
|
|
@@ -7676,29 +8030,29 @@ function renderStatusTable(rows) {
|
|
|
7676
8030
|
import {
|
|
7677
8031
|
chmodSync,
|
|
7678
8032
|
mkdirSync,
|
|
7679
|
-
readFileSync as
|
|
8033
|
+
readFileSync as readFileSync7,
|
|
7680
8034
|
renameSync,
|
|
7681
8035
|
unlinkSync,
|
|
7682
8036
|
writeFileSync as writeFileSync2
|
|
7683
8037
|
} from "node:fs";
|
|
7684
8038
|
import { randomUUID } from "node:crypto";
|
|
7685
|
-
import { join as
|
|
8039
|
+
import { join as join11 } from "node:path";
|
|
7686
8040
|
var REAL_FS = {
|
|
7687
8041
|
mkdir: (p, mode) => mkdirSync(p, { recursive: true, mode }),
|
|
7688
8042
|
chmod: (p, mode) => chmodSync(p, mode),
|
|
7689
8043
|
writeFile: (p, data, mode) => writeFileSync2(p, data, { mode }),
|
|
7690
8044
|
rename: (a, b) => renameSync(a, b),
|
|
7691
|
-
readFile: (p) =>
|
|
8045
|
+
readFile: (p) => readFileSync7(p, "utf8"),
|
|
7692
8046
|
unlink: (p) => unlinkSync(p)
|
|
7693
8047
|
};
|
|
7694
8048
|
function securecomsDir(home) {
|
|
7695
|
-
return
|
|
8049
|
+
return join11(home, ".securecoms");
|
|
7696
8050
|
}
|
|
7697
8051
|
function connectionsDir(home) {
|
|
7698
|
-
return
|
|
8052
|
+
return join11(securecomsDir(home), "connections");
|
|
7699
8053
|
}
|
|
7700
8054
|
function connectionFilePath(home, identityId) {
|
|
7701
|
-
return
|
|
8055
|
+
return join11(connectionsDir(home), `${identityId}.json`);
|
|
7702
8056
|
}
|
|
7703
8057
|
function newInstanceId() {
|
|
7704
8058
|
return randomUUID();
|
|
@@ -7813,7 +8167,7 @@ function realResolveDeps() {
|
|
|
7813
8167
|
}
|
|
7814
8168
|
function realPrepDeps() {
|
|
7815
8169
|
return {
|
|
7816
|
-
readFile: (p) =>
|
|
8170
|
+
readFile: (p) => readFileSync8(p, "utf8"),
|
|
7817
8171
|
writeFileMode: (p, data, mode) => writeFileSync3(p, data, { mode }),
|
|
7818
8172
|
mkdirp: (p, mode) => {
|
|
7819
8173
|
mkdirSync2(p, { recursive: true, mode: mode ?? 493 });
|
|
@@ -7822,7 +8176,7 @@ function realPrepDeps() {
|
|
|
7822
8176
|
realpath: (p) => realpathSync(p),
|
|
7823
8177
|
listDir: (p) => {
|
|
7824
8178
|
try {
|
|
7825
|
-
return
|
|
8179
|
+
return readdirSync2(p);
|
|
7826
8180
|
} catch {
|
|
7827
8181
|
return [];
|
|
7828
8182
|
}
|
|
@@ -7864,12 +8218,12 @@ function realReadFleet() {
|
|
|
7864
8218
|
home: homedir2(),
|
|
7865
8219
|
listDir: (p) => {
|
|
7866
8220
|
try {
|
|
7867
|
-
return
|
|
8221
|
+
return readdirSync2(p);
|
|
7868
8222
|
} catch {
|
|
7869
8223
|
return [];
|
|
7870
8224
|
}
|
|
7871
8225
|
},
|
|
7872
|
-
readFile: (p) =>
|
|
8226
|
+
readFile: (p) => readFileSync8(p, "utf8")
|
|
7873
8227
|
});
|
|
7874
8228
|
}
|
|
7875
8229
|
function runStatus() {
|
|
@@ -7892,43 +8246,43 @@ function runStatus() {
|
|
|
7892
8246
|
function realCopyConnector(srcDir, dstDir, harness) {
|
|
7893
8247
|
const allowedExt = CONNECTOR_FILE_EXTENSIONS[harness];
|
|
7894
8248
|
mkdirSync2(dstDir, { recursive: true });
|
|
7895
|
-
for (const name of
|
|
7896
|
-
const srcPath =
|
|
7897
|
-
if (
|
|
8249
|
+
for (const name of readdirSync2(srcDir)) {
|
|
8250
|
+
const srcPath = join12(srcDir, name);
|
|
8251
|
+
if (statSync3(srcPath).isDirectory()) {
|
|
7898
8252
|
if (CONNECTOR_SKIP_DIRS.has(name)) continue;
|
|
7899
|
-
realCopyConnector(srcPath,
|
|
8253
|
+
realCopyConnector(srcPath, join12(dstDir, name), harness);
|
|
7900
8254
|
continue;
|
|
7901
8255
|
}
|
|
7902
8256
|
if (name.includes(".test.")) continue;
|
|
7903
8257
|
if (!allowedExt.some((ext) => name.endsWith(ext))) continue;
|
|
7904
|
-
writeFileSync3(
|
|
8258
|
+
writeFileSync3(join12(dstDir, name), readFileSync8(srcPath));
|
|
7905
8259
|
}
|
|
7906
8260
|
}
|
|
7907
8261
|
function realWriteBinding(dstDir, identityId) {
|
|
7908
8262
|
mkdirSync2(dstDir, { recursive: true });
|
|
7909
|
-
writeFileSync3(
|
|
8263
|
+
writeFileSync3(join12(dstDir, "securecoms.binding.json"), JSON.stringify({ identity_id: identityId }) + "\n", { mode: 384 });
|
|
7910
8264
|
}
|
|
7911
8265
|
function realReadBinding(dstDir) {
|
|
7912
8266
|
try {
|
|
7913
|
-
const parsed = JSON.parse(
|
|
8267
|
+
const parsed = JSON.parse(readFileSync8(join12(dstDir, "securecoms.binding.json"), "utf8"));
|
|
7914
8268
|
return parsed.identity_id ?? null;
|
|
7915
8269
|
} catch {
|
|
7916
8270
|
return null;
|
|
7917
8271
|
}
|
|
7918
8272
|
}
|
|
7919
8273
|
function realRemoveBinding(dstDir) {
|
|
7920
|
-
rmSync(
|
|
8274
|
+
rmSync(join12(dstDir, "securecoms.binding.json"), { force: true });
|
|
7921
8275
|
}
|
|
7922
8276
|
function realStatDir(p) {
|
|
7923
8277
|
try {
|
|
7924
|
-
return
|
|
8278
|
+
return statSync3(p).isDirectory();
|
|
7925
8279
|
} catch {
|
|
7926
8280
|
return false;
|
|
7927
8281
|
}
|
|
7928
8282
|
}
|
|
7929
8283
|
function realReadHermesConfig(p) {
|
|
7930
8284
|
try {
|
|
7931
|
-
return
|
|
8285
|
+
return readFileSync8(p, "utf8");
|
|
7932
8286
|
} catch {
|
|
7933
8287
|
return null;
|
|
7934
8288
|
}
|
|
@@ -8087,11 +8441,11 @@ async function main() {
|
|
|
8087
8441
|
`);
|
|
8088
8442
|
process.exit(1);
|
|
8089
8443
|
}
|
|
8090
|
-
const tokenFile = daemon.tokenFile ??
|
|
8444
|
+
const tokenFile = daemon.tokenFile ?? join12(dirname7(credentialPath), "bridge-token");
|
|
8091
8445
|
const token = resolveToken({ tokenFile, token: daemon.token });
|
|
8092
8446
|
const agent = new BridgeAgent(cfg);
|
|
8093
8447
|
await agent.start();
|
|
8094
|
-
const tls = daemon.tls ? { cert:
|
|
8448
|
+
const tls = daemon.tls ? { cert: readFileSync8(daemon.tlsCert, "utf8"), key: readFileSync8(daemon.tlsKey, "utf8") } : void 0;
|
|
8095
8449
|
const { server, closeAllSseStreams } = createHttpServer(agent, { token, allowOrigins: daemon.allowOrigins, tls, websocket: true });
|
|
8096
8450
|
const agentWs = attachAgentWebSocket(server, agent, { token, allowOrigins: daemon.allowOrigins });
|
|
8097
8451
|
let down = false;
|