@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/index.js
CHANGED
|
@@ -1284,6 +1284,18 @@ var LeafRosterMismatchError = class extends Error {
|
|
|
1284
1284
|
this.name = "LeafRosterMismatchError";
|
|
1285
1285
|
}
|
|
1286
1286
|
};
|
|
1287
|
+
var AmbiguousOwnLeafError = class extends Error {
|
|
1288
|
+
deviceId;
|
|
1289
|
+
totalLeaves;
|
|
1290
|
+
candidateLeaves;
|
|
1291
|
+
constructor(deviceId, totalLeaves, candidateLeaves) {
|
|
1292
|
+
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.`);
|
|
1293
|
+
this.deviceId = deviceId;
|
|
1294
|
+
this.totalLeaves = totalLeaves;
|
|
1295
|
+
this.candidateLeaves = candidateLeaves;
|
|
1296
|
+
this.name = "AmbiguousOwnLeafError";
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1287
1299
|
var HealAddAfterRemoveError = class extends Error {
|
|
1288
1300
|
constructor(cause) {
|
|
1289
1301
|
super("healDivergedMember: Add failed after Remove already landed \u2014 member detached, re-run heal to recover");
|
|
@@ -1303,7 +1315,7 @@ var ForgedCredentialError = class extends Error {
|
|
|
1303
1315
|
this.name = "ForgedCredentialError";
|
|
1304
1316
|
}
|
|
1305
1317
|
};
|
|
1306
|
-
var MlsGroupKeyProvider = class {
|
|
1318
|
+
var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
|
|
1307
1319
|
o;
|
|
1308
1320
|
suite;
|
|
1309
1321
|
managers = /* @__PURE__ */ new Map();
|
|
@@ -1453,9 +1465,25 @@ var MlsGroupKeyProvider = class {
|
|
|
1453
1465
|
* Snapshot of per-channel decrypt-error / stuck state (ADR-0002 observability).
|
|
1454
1466
|
* Surfaces a wedged channel that would otherwise "look idle": the daemon serves
|
|
1455
1467
|
* it on GET /v1/metrics and the web shell reads it via a selector. A channel is
|
|
1456
|
-
* `stuck` once it has had `stuckThreshold`
|
|
1457
|
-
* intervening successful decrypt. Returns a plain object keyed by
|
|
1458
|
-
* absence of a key means zero errors observed for that channel.
|
|
1468
|
+
* `stuck` once it has had `stuckThreshold` GENUINELY-WEDGED decrypt failures
|
|
1469
|
+
* with no intervening successful decrypt. Returns a plain object keyed by
|
|
1470
|
+
* channelId; absence of a key means zero errors observed for that channel.
|
|
1471
|
+
*
|
|
1472
|
+
* `stuck` gates on `wedgedRun`, NOT `consecutiveErrors` — the same correction
|
|
1473
|
+
* the quarantine gate already got (P0a). `consecutiveErrors` is bumped at the
|
|
1474
|
+
* top of the catch for EVERY failure, INCLUDING the benign past-epoch
|
|
1475
|
+
* duplicate-commit churn that the `permanentlyDead` path immediately and
|
|
1476
|
+
* safely auto-drains. Deriving the user-visible health signal from it meant
|
|
1477
|
+
* ordinary at-least-once / committer-race traffic painted a perfectly healthy
|
|
1478
|
+
* channel red.
|
|
1479
|
+
*
|
|
1480
|
+
* That is the prod symptom of 2026-07-31: a freshly-welcomed federated partner
|
|
1481
|
+
* was queued the very Add commit its Welcome already contained. It joined the
|
|
1482
|
+
* correct group at the correct epoch, correctly identified the replayed commit
|
|
1483
|
+
* as dead and drained it — and still showed "This channel is stuck", because
|
|
1484
|
+
* the drain happened AFTER the error had been counted. `wedgedRun` counts only
|
|
1485
|
+
* rows left UN-acked (actually stuck), and a clean decrypt clears it, so a
|
|
1486
|
+
* genuine wedge still reports exactly as before.
|
|
1459
1487
|
*/
|
|
1460
1488
|
errorStats() {
|
|
1461
1489
|
const out = {};
|
|
@@ -1464,7 +1492,7 @@ var MlsGroupKeyProvider = class {
|
|
|
1464
1492
|
decryptErrors: e.decryptErrors,
|
|
1465
1493
|
consecutiveErrors: e.consecutiveErrors,
|
|
1466
1494
|
lastErrorAt: e.lastErrorAt,
|
|
1467
|
-
stuck: e.
|
|
1495
|
+
stuck: e.wedgedRun >= this.stuckThreshold,
|
|
1468
1496
|
quarantined: e.quarantined
|
|
1469
1497
|
};
|
|
1470
1498
|
}
|
|
@@ -2129,6 +2157,25 @@ var MlsGroupKeyProvider = class {
|
|
|
2129
2157
|
* `removed_device_id` (instead of removed_identity_id); the server resolves the
|
|
2130
2158
|
* device's identity, asserts it matches, and purges that ONE device.
|
|
2131
2159
|
*/
|
|
2160
|
+
/**
|
|
2161
|
+
* This committer's OWN leaf index in the given group's ratchet tree, or null
|
|
2162
|
+
* when it cannot be read.
|
|
2163
|
+
*
|
|
2164
|
+
* `MLSGroupManager.state` is marked `@internal` but is a declared (non-private)
|
|
2165
|
+
* field, and `state.privatePath.leafIndex` is the same value ts-mls itself uses
|
|
2166
|
+
* as the sender leaf when protecting application data. Reading it here avoids a
|
|
2167
|
+
* change to `@agentvault/crypto`; if a public accessor is added later this
|
|
2168
|
+
* should delegate to it.
|
|
2169
|
+
*
|
|
2170
|
+
* Returns null rather than throwing so every caller must decide explicitly what
|
|
2171
|
+
* an unknown own-leaf means. The one caller today FAILS CLOSED on null by
|
|
2172
|
+
* falling back to the positional path only for FOREIGN identities — it never
|
|
2173
|
+
* uses null to justify guessing among an own identity's leaves.
|
|
2174
|
+
*/
|
|
2175
|
+
ownLeafIndex(mgr) {
|
|
2176
|
+
const idx = mgr.state?.privatePath?.leafIndex;
|
|
2177
|
+
return typeof idx === "number" ? idx : null;
|
|
2178
|
+
}
|
|
2132
2179
|
async removeDeviceFromLiveGroup(channelId, deviceId, opts) {
|
|
2133
2180
|
return this.locks.run(channelId, () => this.evictOnThrow(channelId, () => this.removeDeviceFromLiveGroupUnlocked(channelId, deviceId, opts, 0)));
|
|
2134
2181
|
}
|
|
@@ -2141,7 +2188,17 @@ var MlsGroupKeyProvider = class {
|
|
|
2141
2188
|
if (leaves.length !== opts.orderedDeviceIds.length) {
|
|
2142
2189
|
throw new LeafRosterMismatchError();
|
|
2143
2190
|
}
|
|
2144
|
-
|
|
2191
|
+
let leafIndex;
|
|
2192
|
+
const ownLeaf = this.ownLeafIndex(mgr);
|
|
2193
|
+
if (ownLeaf !== null && leaves.includes(ownLeaf)) {
|
|
2194
|
+
const others = leaves.filter((i) => i !== ownLeaf);
|
|
2195
|
+
if (others.length !== 1) {
|
|
2196
|
+
throw new AmbiguousOwnLeafError(deviceId, leaves.length, others.length);
|
|
2197
|
+
}
|
|
2198
|
+
leafIndex = others[0];
|
|
2199
|
+
} else {
|
|
2200
|
+
leafIndex = leaves[pos];
|
|
2201
|
+
}
|
|
2145
2202
|
const init = await this.o.backend.initConnectChannelGroup(channelId, [this.o.deviceId]);
|
|
2146
2203
|
const priorEpoch = Number(mgr.epoch);
|
|
2147
2204
|
const { commit } = await mgr.removeMembers([leafIndex]);
|
|
@@ -2394,7 +2451,7 @@ var MlsGroupKeyProvider = class {
|
|
|
2394
2451
|
});
|
|
2395
2452
|
await this.o.store.save(channelId, mgr.exportState());
|
|
2396
2453
|
this.managers.set(channelId, mgr);
|
|
2397
|
-
|
|
2454
|
+
return { staleMemberDeviceIds: reinit.stale_member_device_ids ?? [] };
|
|
2398
2455
|
});
|
|
2399
2456
|
}
|
|
2400
2457
|
/** Returns true when an in-memory MLS manager is live for the channel.
|
|
@@ -2470,7 +2527,60 @@ var MlsGroupKeyProvider = class {
|
|
|
2470
2527
|
* ⇒ refuse, never call verify), then structural signer check, then attestation
|
|
2471
2528
|
* verify against the NON-EMPTY pinned key, then post-join group-id assertion.
|
|
2472
2529
|
*/
|
|
2530
|
+
/** C4 (split-brain Task 4): back-off for REFUSED supersedes, keyed
|
|
2531
|
+
* `${channelId}:${groupId}`. A refused supersede leaves its row un-acked by
|
|
2532
|
+
* design (it is not a stray), but pre-back-off that meant a full re-verify —
|
|
2533
|
+
* including a fetchResetAttestation HTTP round-trip — on EVERY receive poll,
|
|
2534
|
+
* forever, for a request that can never succeed (the nine production split
|
|
2535
|
+
* channels all carry NULL-attestation legacy groups).
|
|
2536
|
+
*
|
|
2537
|
+
* ⚠️ ONLY POST-FETCH refusals enter the back-off. The step-0 pin guard and
|
|
2538
|
+
* the retired-group check run BEFORE fetchResetAttestation, so they
|
|
2539
|
+
* contribute zero HTTP to the storm — and the pin guard's precondition is
|
|
2540
|
+
* TRANSIENT (cold owner cache / momentarily anomalous roster; the bridge
|
|
2541
|
+
* warms the cache before receiveOnce for exactly this reason). Backing a
|
|
2542
|
+
* pre-fetch refusal off buys nothing and converts a one-cycle stall into a
|
|
2543
|
+
* ten-minute outage on a valid recovery.
|
|
2544
|
+
*
|
|
2545
|
+
* Of the backed-off refusals: no-attestation, unparseable content,
|
|
2546
|
+
* channel/group mismatch and post-join mismatch are DETERMINISTIC per
|
|
2547
|
+
* (channel, group) — the attestation columns are written at group creation
|
|
2548
|
+
* and never change. Signer-mismatch, signature-verify and no-bundle are
|
|
2549
|
+
* ASSUMED stable for the window; they are pin- or bundle-dependent, but by
|
|
2550
|
+
* the time they run the fetch has already been paid, so suppressing the
|
|
2551
|
+
* window bounds the storm at the cost of a ≤10-minute delay if the pin or
|
|
2552
|
+
* bundle changes. Note the suppression means the reason is not RE-OBSERVED
|
|
2553
|
+
* during the window — the underlying precondition can change freely; the
|
|
2554
|
+
* window is a cost bound, not a claim of immutability.
|
|
2555
|
+
*
|
|
2556
|
+
* The reason lives in the VALUE for observability; the clearing rule lives
|
|
2557
|
+
* in the KEY: the owner minting a FIXED replacement group arrives with a
|
|
2558
|
+
* NEW group id — a different key, tried immediately. The un-openable path
|
|
2559
|
+
* (verified Welcome, KP mismatch) is EXEMPT: onSupersedeUnopenable's own
|
|
2560
|
+
* cooldown governs it. In-memory only: a reload/restart costs one extra
|
|
2561
|
+
* fetch per stranded row — reload-bounded, not poll-bounded. */
|
|
2562
|
+
supersedeRefusals = /* @__PURE__ */ new Map();
|
|
2563
|
+
static SUPERSEDE_REFUSAL_BACKOFF_MS = 10 * 6e4;
|
|
2564
|
+
/** True while (channelId, groupId) is inside its refusal back-off window.
|
|
2565
|
+
* @internal exposed for diagnostics only — not part of the stable surface. */
|
|
2566
|
+
supersedeBackoffActive(channelId, groupId) {
|
|
2567
|
+
const rec = this.supersedeRefusals.get(`${channelId}:${groupId}`);
|
|
2568
|
+
return rec !== void 0 && Date.now() < rec.until;
|
|
2569
|
+
}
|
|
2473
2570
|
async trySupersede(channelId, row, heldGroupId) {
|
|
2571
|
+
const refuse = (msg) => {
|
|
2572
|
+
this.recordDecryptError(row, new Error(msg));
|
|
2573
|
+
const now = Date.now();
|
|
2574
|
+
for (const [k, v] of this.supersedeRefusals) {
|
|
2575
|
+
if (now >= v.until)
|
|
2576
|
+
this.supersedeRefusals.delete(k);
|
|
2577
|
+
}
|
|
2578
|
+
this.supersedeRefusals.set(`${channelId}:${row.group_id}`, {
|
|
2579
|
+
reason: msg,
|
|
2580
|
+
until: now + _MlsGroupKeyProvider.SUPERSEDE_REFUSAL_BACKOFF_MS
|
|
2581
|
+
});
|
|
2582
|
+
return false;
|
|
2583
|
+
};
|
|
2474
2584
|
const owner = this.o.onResolvePinnedOwner ? await this.o.onResolvePinnedOwner(channelId) : null;
|
|
2475
2585
|
if (!owner || !owner.publicKeyHex) {
|
|
2476
2586
|
this.recordDecryptError(row, new Error("supersede refused: no pinned owner key (step-0 guard)"));
|
|
@@ -2483,33 +2593,27 @@ var MlsGroupKeyProvider = class {
|
|
|
2483
2593
|
}
|
|
2484
2594
|
const att = await this.o.backend.fetchResetAttestation(row.group_id);
|
|
2485
2595
|
if (!att) {
|
|
2486
|
-
|
|
2487
|
-
return false;
|
|
2596
|
+
return refuse(`supersede refused: no attestation for group ${row.group_id}`);
|
|
2488
2597
|
}
|
|
2489
2598
|
if (att.signerIdentityId !== owner.identityId) {
|
|
2490
|
-
|
|
2491
|
-
return false;
|
|
2599
|
+
return refuse("supersede refused: signer is not the pinned owner");
|
|
2492
2600
|
}
|
|
2493
2601
|
let content;
|
|
2494
2602
|
try {
|
|
2495
2603
|
content = JSON.parse(att.signedContent);
|
|
2496
2604
|
} catch {
|
|
2497
|
-
|
|
2498
|
-
return false;
|
|
2605
|
+
return refuse("supersede refused: unparseable signed_content");
|
|
2499
2606
|
}
|
|
2500
2607
|
if (content.channel_id !== row.connect_channel_id || content.new_group_id !== row.group_id) {
|
|
2501
|
-
|
|
2502
|
-
return false;
|
|
2608
|
+
return refuse("supersede refused: attestation channel/group mismatch");
|
|
2503
2609
|
}
|
|
2504
2610
|
const ok = await verifyApprovalArtifact({ content, signature: att.signatureHex, signerPublicKey: owner.publicKeyHex, createdAt: "", version: 1 }, owner.publicKeyHex);
|
|
2505
2611
|
if (!ok) {
|
|
2506
|
-
|
|
2507
|
-
return false;
|
|
2612
|
+
return refuse("supersede refused: signature does not verify against pinned owner key");
|
|
2508
2613
|
}
|
|
2509
2614
|
const bundle = await this.o.store.loadPendingBundle(this.o.deviceId);
|
|
2510
2615
|
if (!bundle) {
|
|
2511
|
-
|
|
2512
|
-
return false;
|
|
2616
|
+
return refuse("supersede refused: no pending KeyPackage bundle to open the new Welcome");
|
|
2513
2617
|
}
|
|
2514
2618
|
return await this.locks.run(channelId, async () => {
|
|
2515
2619
|
const mgr = new MLSGroupManager();
|
|
@@ -2534,14 +2638,14 @@ var MlsGroupKeyProvider = class {
|
|
|
2534
2638
|
return false;
|
|
2535
2639
|
}
|
|
2536
2640
|
if (new TextDecoder().decode(mgr.groupId) !== row.group_id) {
|
|
2537
|
-
|
|
2538
|
-
return false;
|
|
2641
|
+
return refuse("supersede refused: post-join group id != attested new_group_id");
|
|
2539
2642
|
}
|
|
2540
2643
|
this.evictChannelState(channelId);
|
|
2541
2644
|
await this.o.store.delete(channelId);
|
|
2542
2645
|
await this.o.store.discardStaging(channelId);
|
|
2543
2646
|
await this.o.store.addRetiredGroupId(channelId, heldGroupId);
|
|
2544
2647
|
this.errors.delete(channelId);
|
|
2648
|
+
this.supersedeRefusals.delete(`${channelId}:${row.group_id}`);
|
|
2545
2649
|
this.managers.set(channelId, mgr);
|
|
2546
2650
|
await this.o.store.save(channelId, mgr.exportState());
|
|
2547
2651
|
await this.o.backend.ackDelivery(this.o.deviceId, [row.queue_id]);
|
|
@@ -2578,9 +2682,15 @@ var MlsGroupKeyProvider = class {
|
|
|
2578
2682
|
if (heldGroupId !== null && heldGroupId === row.group_id) {
|
|
2579
2683
|
handshakeAcked.push(row.queue_id);
|
|
2580
2684
|
} else if (heldGroupId !== null && heldGroupId !== row.group_id) {
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2685
|
+
if (this.supersedeBackoffActive(wcid, row.group_id)) {
|
|
2686
|
+
if (typeof process !== "undefined" && process.env?.SECURECOMS_TRACE_DELIVERY) {
|
|
2687
|
+
console.warn(`[trace] welcome ch=${wcid.slice(0, 8)} g=${row.group_id.slice(0, 8)} -> SKIP(refusal back-off)`);
|
|
2688
|
+
}
|
|
2689
|
+
} else {
|
|
2690
|
+
const ok = await this.trySupersede(wcid, row, heldGroupId);
|
|
2691
|
+
if (ok)
|
|
2692
|
+
supersededChannelIds.push(wcid);
|
|
2693
|
+
}
|
|
2584
2694
|
}
|
|
2585
2695
|
} else if (typeof process !== "undefined" && process.env?.SECURECOMS_TRACE_DELIVERY) {
|
|
2586
2696
|
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)`);
|
|
@@ -3283,19 +3393,8 @@ var RestMlsBackend = class {
|
|
|
3283
3393
|
created: j.created === void 0 ? true : Boolean(j.created)
|
|
3284
3394
|
};
|
|
3285
3395
|
}
|
|
3286
|
-
async reinitConnectChannelGroup(
|
|
3287
|
-
|
|
3288
|
-
if (!committer)
|
|
3289
|
-
throw new Error("reinitConnectChannelGroup: empty member set");
|
|
3290
|
-
const j = await this.post(`/api/v1/mls/connect-channels/${channelId}/reinit`, {
|
|
3291
|
-
member_device_ids: memberDeviceIds,
|
|
3292
|
-
committer_device_id: committer
|
|
3293
|
-
});
|
|
3294
|
-
return {
|
|
3295
|
-
group_id: String(j.group_id),
|
|
3296
|
-
current_epoch: Number(j.current_epoch),
|
|
3297
|
-
member_device_ids: Array.isArray(j.member_device_ids) ? j.member_device_ids.map(String) : []
|
|
3298
|
-
};
|
|
3396
|
+
async reinitConnectChannelGroup() {
|
|
3397
|
+
throw new Error("reinitConnectChannelGroup was removed (it minted unattested MLS groups that orphaned channel members permanently). Use resetReinitConnectChannelGroup with an owner attestation.");
|
|
3299
3398
|
}
|
|
3300
3399
|
async resetReinitConnectChannelGroup(channelId, memberDeviceIds, att) {
|
|
3301
3400
|
const committer = memberDeviceIds[0];
|
|
@@ -3313,7 +3412,11 @@ var RestMlsBackend = class {
|
|
|
3313
3412
|
return {
|
|
3314
3413
|
group_id: String(j.group_id),
|
|
3315
3414
|
current_epoch: Number(j.current_epoch),
|
|
3316
|
-
member_device_ids: Array.isArray(j.member_device_ids) ? j.member_device_ids.map(String) : []
|
|
3415
|
+
member_device_ids: Array.isArray(j.member_device_ids) ? j.member_device_ids.map(String) : [],
|
|
3416
|
+
// Absent on a backend that predates the dropped-member report; an empty
|
|
3417
|
+
// list is the correct read of "the server told us nothing", since the old
|
|
3418
|
+
// server also never dropped anyone for an expired KeyPackage.
|
|
3419
|
+
stale_member_device_ids: Array.isArray(j.stale_member_device_ids) ? j.stale_member_device_ids.map(String) : []
|
|
3317
3420
|
};
|
|
3318
3421
|
}
|
|
3319
3422
|
async fetchKeyPackages(deviceIds) {
|
|
@@ -3811,7 +3914,7 @@ function loadPackageMeta(importMetaUrl) {
|
|
|
3811
3914
|
|
|
3812
3915
|
// src/bridge-agent.ts
|
|
3813
3916
|
import { promises as fsp } from "node:fs";
|
|
3814
|
-
import { join as
|
|
3917
|
+
import { join as join6, dirname } from "node:path";
|
|
3815
3918
|
|
|
3816
3919
|
// src/latency.ts
|
|
3817
3920
|
var DEFAULT_CAPACITY = 1024;
|
|
@@ -4324,29 +4427,153 @@ function readRecentTurns(baseDir, agent, sessionId, k) {
|
|
|
4324
4427
|
return k > 0 ? turns.slice(-k) : [];
|
|
4325
4428
|
}
|
|
4326
4429
|
|
|
4430
|
+
// src/memory/openclaw-workspace-facts.ts
|
|
4431
|
+
import { readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
4432
|
+
import { join as join5 } from "node:path";
|
|
4433
|
+
var DEFAULT_FACTS_BUDGET = 8e3;
|
|
4434
|
+
var MEMORY_MD_CAP = 6e3;
|
|
4435
|
+
var MIN_FRAGMENT = 200;
|
|
4436
|
+
var MAX_SOURCE_BYTES = 1024 * 1024;
|
|
4437
|
+
var DAILY_RE = /^\d{4}-\d{2}-\d{2}\.md$/;
|
|
4438
|
+
function resolveWorkspaceDir(baseDir, agent, override) {
|
|
4439
|
+
if (override) return expandHome(override);
|
|
4440
|
+
const base = expandHome(baseDir);
|
|
4441
|
+
const isMain = agent === "" || agent === "main";
|
|
4442
|
+
try {
|
|
4443
|
+
const cfg = JSON.parse(readFileSync4(join5(base, "openclaw.json"), "utf8"));
|
|
4444
|
+
const agents = cfg?.agents;
|
|
4445
|
+
if (agents && typeof agents === "object") {
|
|
4446
|
+
const list = Array.isArray(agents.list) ? agents.list : [];
|
|
4447
|
+
const wanted = isMain ? "main" : agent;
|
|
4448
|
+
const entry = list.find((e) => e && typeof e === "object" && e.id === wanted);
|
|
4449
|
+
const ws = entry?.workspace;
|
|
4450
|
+
if (typeof ws === "string" && ws) return expandHome(ws);
|
|
4451
|
+
const def = agents.defaults?.workspace;
|
|
4452
|
+
if (typeof def === "string" && def) return expandHome(def);
|
|
4453
|
+
}
|
|
4454
|
+
} catch {
|
|
4455
|
+
}
|
|
4456
|
+
return isMain ? join5(base, "workspace") : join5(base, `workspace-${agent}`);
|
|
4457
|
+
}
|
|
4458
|
+
var TRUNC = "\u2026[truncated]";
|
|
4459
|
+
function headSlice(s, n) {
|
|
4460
|
+
if (s.length <= n) return { text: s, clipped: false };
|
|
4461
|
+
let out = s.slice(0, n);
|
|
4462
|
+
const last = out.charCodeAt(out.length - 1);
|
|
4463
|
+
if (last >= 55296 && last <= 56319) out = out.slice(0, -1);
|
|
4464
|
+
return { text: out, clipped: true };
|
|
4465
|
+
}
|
|
4466
|
+
function tailSlice(s, n) {
|
|
4467
|
+
if (s.length <= n) return { text: s, clipped: false };
|
|
4468
|
+
let out = s.slice(s.length - n);
|
|
4469
|
+
const first = out.charCodeAt(0);
|
|
4470
|
+
if (first >= 56320 && first <= 57343) out = out.slice(1);
|
|
4471
|
+
return { text: out, clipped: true };
|
|
4472
|
+
}
|
|
4473
|
+
function readBounded(path) {
|
|
4474
|
+
if (statSync(path).size > MAX_SOURCE_BYTES) return { text: null, oversize: true };
|
|
4475
|
+
return { text: readFileSync4(path, "utf8"), oversize: false };
|
|
4476
|
+
}
|
|
4477
|
+
function readWorkspaceFacts(baseDir, agent, opts) {
|
|
4478
|
+
const raw = opts?.charBudget;
|
|
4479
|
+
const budget = typeof raw === "number" && Number.isInteger(raw) && raw >= 0 ? raw : DEFAULT_FACTS_BUDGET;
|
|
4480
|
+
if (budget === 0) return { facts: [], truncated: false, missing: false };
|
|
4481
|
+
const dir = resolveWorkspaceDir(baseDir, agent, opts?.workspaceDir);
|
|
4482
|
+
const facts = [];
|
|
4483
|
+
let truncated = false;
|
|
4484
|
+
let remaining = budget;
|
|
4485
|
+
try {
|
|
4486
|
+
const r = readBounded(join5(dir, "MEMORY.md"));
|
|
4487
|
+
if (r.oversize) truncated = true;
|
|
4488
|
+
else if (r.text && r.text.trim()) {
|
|
4489
|
+
const { text, clipped } = headSlice(r.text, Math.min(MEMORY_MD_CAP, budget));
|
|
4490
|
+
truncated ||= clipped;
|
|
4491
|
+
facts.push(`workspace MEMORY.md:
|
|
4492
|
+
${text}${clipped ? `
|
|
4493
|
+
${TRUNC}` : ""}`);
|
|
4494
|
+
remaining -= text.length;
|
|
4495
|
+
}
|
|
4496
|
+
} catch {
|
|
4497
|
+
}
|
|
4498
|
+
try {
|
|
4499
|
+
const dailies = readdirSync(join5(dir, "memory")).filter((f) => DAILY_RE.test(f)).sort().reverse().slice(0, 2);
|
|
4500
|
+
for (const f of dailies) {
|
|
4501
|
+
if (remaining < MIN_FRAGMENT) {
|
|
4502
|
+
truncated = true;
|
|
4503
|
+
break;
|
|
4504
|
+
}
|
|
4505
|
+
let r;
|
|
4506
|
+
try {
|
|
4507
|
+
r = readBounded(join5(dir, "memory", f));
|
|
4508
|
+
} catch {
|
|
4509
|
+
continue;
|
|
4510
|
+
}
|
|
4511
|
+
if (r.oversize) {
|
|
4512
|
+
truncated = true;
|
|
4513
|
+
continue;
|
|
4514
|
+
}
|
|
4515
|
+
if (!r.text || !r.text.trim()) continue;
|
|
4516
|
+
const { text, clipped } = tailSlice(r.text, remaining);
|
|
4517
|
+
truncated ||= clipped;
|
|
4518
|
+
facts.push(`workspace memory/${f} (daily):
|
|
4519
|
+
${clipped ? `${TRUNC}
|
|
4520
|
+
` : ""}${text}`);
|
|
4521
|
+
remaining -= text.length;
|
|
4522
|
+
}
|
|
4523
|
+
} catch {
|
|
4524
|
+
}
|
|
4525
|
+
return { facts, truncated, missing: facts.length === 0 };
|
|
4526
|
+
}
|
|
4527
|
+
|
|
4327
4528
|
// src/memory/openclaw-provider.ts
|
|
4328
4529
|
var DEFAULT_K2 = 200;
|
|
4329
4530
|
var OpenClawMemoryProvider = class {
|
|
4330
|
-
|
|
4531
|
+
// gauge: last successful returned total, including prefixes/markers
|
|
4532
|
+
constructor(store, baseDir, agent, factsOpts) {
|
|
4331
4533
|
this.store = store;
|
|
4332
4534
|
this.baseDir = baseDir;
|
|
4333
4535
|
this.agent = agent;
|
|
4536
|
+
this.factsOpts = factsOpts;
|
|
4334
4537
|
}
|
|
4335
4538
|
noSession = 0;
|
|
4336
4539
|
readFailed = 0;
|
|
4540
|
+
factsMissing = 0;
|
|
4541
|
+
// absence-shaped: resolved path missing or zero sources (per-call)
|
|
4542
|
+
factsReadFailed = 0;
|
|
4543
|
+
// anything THROWN out of the facts computation
|
|
4544
|
+
factsTruncated = 0;
|
|
4545
|
+
// budget clipped/dropped a source (incl. the 1MB skip)
|
|
4546
|
+
factsChars = 0;
|
|
4337
4547
|
async record(input) {
|
|
4338
4548
|
if (input.modality !== "voice") return { seq: 0, deduped: true };
|
|
4339
4549
|
return this.store.recordMemory({ ...input, text: `[voice] ${input.text}` });
|
|
4340
4550
|
}
|
|
4341
4551
|
async context(conversationId, limit) {
|
|
4342
4552
|
const k = limit ?? DEFAULT_K2;
|
|
4553
|
+
let wsFacts = [];
|
|
4554
|
+
try {
|
|
4555
|
+
const r = readWorkspaceFacts(this.baseDir, this.agent, {
|
|
4556
|
+
workspaceDir: this.factsOpts?.workspaceDir,
|
|
4557
|
+
charBudget: this.factsOpts?.factsCharBudget
|
|
4558
|
+
});
|
|
4559
|
+
wsFacts = r.facts;
|
|
4560
|
+
if (r.missing) this.factsMissing++;
|
|
4561
|
+
if (r.truncated) this.factsTruncated++;
|
|
4562
|
+
this.factsChars = r.facts.reduce((n, f) => n + f.length, 0);
|
|
4563
|
+
} catch {
|
|
4564
|
+
this.factsReadFailed++;
|
|
4565
|
+
}
|
|
4566
|
+
const withFacts = (res) => ({
|
|
4567
|
+
recent: res.recent,
|
|
4568
|
+
facts: [...wsFacts, ...res.facts]
|
|
4569
|
+
});
|
|
4343
4570
|
const voice = await this.store.memoryContext(conversationId, k);
|
|
4344
4571
|
let textTurns = [];
|
|
4345
4572
|
try {
|
|
4346
4573
|
const sid = resolveOwnerDmSession(this.baseDir, this.agent);
|
|
4347
4574
|
if (!sid) {
|
|
4348
4575
|
this.noSession++;
|
|
4349
|
-
return voice;
|
|
4576
|
+
return withFacts(voice);
|
|
4350
4577
|
}
|
|
4351
4578
|
textTurns = readRecentTurns(this.baseDir, this.agent, sid, k).map((t) => ({
|
|
4352
4579
|
seq: 0,
|
|
@@ -4356,7 +4583,7 @@ var OpenClawMemoryProvider = class {
|
|
|
4356
4583
|
}));
|
|
4357
4584
|
} catch {
|
|
4358
4585
|
this.readFailed++;
|
|
4359
|
-
return voice;
|
|
4586
|
+
return withFacts(voice);
|
|
4360
4587
|
}
|
|
4361
4588
|
const tagged = [
|
|
4362
4589
|
...textTurns.map((t, i) => ({ ...t, _src: 0, _i: i })),
|
|
@@ -4375,10 +4602,17 @@ var OpenClawMemoryProvider = class {
|
|
|
4375
4602
|
text: t.text,
|
|
4376
4603
|
ts: t.ts
|
|
4377
4604
|
}));
|
|
4378
|
-
return { recent, facts: voice.facts };
|
|
4605
|
+
return withFacts({ recent, facts: voice.facts });
|
|
4379
4606
|
}
|
|
4380
4607
|
stats() {
|
|
4381
|
-
return {
|
|
4608
|
+
return {
|
|
4609
|
+
openclaw_no_session: this.noSession,
|
|
4610
|
+
openclaw_read_failed: this.readFailed,
|
|
4611
|
+
openclaw_facts_missing: this.factsMissing,
|
|
4612
|
+
openclaw_facts_read_failed: this.factsReadFailed,
|
|
4613
|
+
openclaw_facts_truncated: this.factsTruncated,
|
|
4614
|
+
openclaw_facts_chars: this.factsChars
|
|
4615
|
+
};
|
|
4382
4616
|
}
|
|
4383
4617
|
};
|
|
4384
4618
|
|
|
@@ -4402,10 +4636,10 @@ var NullMemory = class {
|
|
|
4402
4636
|
return { recent: [], facts: [] };
|
|
4403
4637
|
}
|
|
4404
4638
|
};
|
|
4405
|
-
function createMemoryProvider(provider, store, baseDir, agent) {
|
|
4639
|
+
function createMemoryProvider(provider, store, baseDir, agent, openclawFacts) {
|
|
4406
4640
|
if (provider === "none") return new NullMemory();
|
|
4407
4641
|
if (provider === "hermes") return new HermesMemoryProvider(baseDir ?? "~/.hermes");
|
|
4408
|
-
if (provider === "openclaw") return new OpenClawMemoryProvider(store, baseDir ?? "~/.openclaw", agent ?? "");
|
|
4642
|
+
if (provider === "openclaw") return new OpenClawMemoryProvider(store, baseDir ?? "~/.openclaw", agent ?? "", openclawFacts);
|
|
4409
4643
|
return new SecureComsLocalMemory(store);
|
|
4410
4644
|
}
|
|
4411
4645
|
|
|
@@ -4513,7 +4747,10 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4513
4747
|
identityId: cfg.identityId,
|
|
4514
4748
|
cipherFor: () => new KeyedInboxCipher(deriveMlsStoreKey(seed))
|
|
4515
4749
|
});
|
|
4516
|
-
this.memory = createMemoryProvider(this.cfg.memoryProvider, this.store, this.cfg.memoryBaseDir, this.cfg.memoryAgent
|
|
4750
|
+
this.memory = createMemoryProvider(this.cfg.memoryProvider, this.store, this.cfg.memoryBaseDir, this.cfg.memoryAgent, {
|
|
4751
|
+
workspaceDir: this.cfg.memoryWorkspaceDir,
|
|
4752
|
+
factsCharBudget: this.cfg.memoryFactsCharBudget
|
|
4753
|
+
});
|
|
4517
4754
|
this.provider = new MlsGroupKeyProvider({
|
|
4518
4755
|
store: this.store,
|
|
4519
4756
|
backend: this.backend,
|
|
@@ -4593,7 +4830,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4593
4830
|
const j = await res.json();
|
|
4594
4831
|
return String(j.attachment_id);
|
|
4595
4832
|
};
|
|
4596
|
-
this.attachmentsDirImpl = deps.attachmentsDir ??
|
|
4833
|
+
this.attachmentsDirImpl = deps.attachmentsDir ?? join6(dirname(cfg.storePath), "attachments");
|
|
4597
4834
|
this.action = {
|
|
4598
4835
|
begin: (channelId) => this.actionBegin(channelId),
|
|
4599
4836
|
start: (opts) => this.actionStart(opts),
|
|
@@ -5077,11 +5314,33 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
5077
5314
|
return;
|
|
5078
5315
|
}
|
|
5079
5316
|
this.providerCapabilityMissing = false;
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5317
|
+
const enclaves = await this.enclavesToCover();
|
|
5318
|
+
this.enclaveListEverSucceeded.add(this.cfg.enclaveId);
|
|
5319
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5320
|
+
const chans = [];
|
|
5321
|
+
const failed = [];
|
|
5322
|
+
let listedAny = false;
|
|
5323
|
+
for (const encId of enclaves) {
|
|
5324
|
+
try {
|
|
5325
|
+
for (const c of await this.listChannels(encId)) {
|
|
5326
|
+
if (seen.has(c.channel_id)) continue;
|
|
5327
|
+
seen.add(c.channel_id);
|
|
5328
|
+
chans.push(c);
|
|
5329
|
+
}
|
|
5330
|
+
listedAny = true;
|
|
5331
|
+
this.enclaveListEverSucceeded.add(encId);
|
|
5332
|
+
this.enclaveListWarned.delete(encId);
|
|
5333
|
+
} catch (e) {
|
|
5334
|
+
failed.push(encId);
|
|
5335
|
+
if (!this.enclaveListWarned.has(encId)) {
|
|
5336
|
+
this.enclaveListWarned.add(encId);
|
|
5337
|
+
console.warn(`[bridge] selfHealSweep: listChannels failed for enclave ${encId}; skipping (warn-once per streak):`, e);
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5341
|
+
this.sweepEnclavesFailed = failed;
|
|
5342
|
+
if (!listedAny) {
|
|
5343
|
+
console.warn("[bridge] selfHealSweep aborted: every enclave listing failed");
|
|
5085
5344
|
return;
|
|
5086
5345
|
}
|
|
5087
5346
|
for (const c of chans) {
|
|
@@ -5427,7 +5686,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
5427
5686
|
* the other enclaves in the set.
|
|
5428
5687
|
*/
|
|
5429
5688
|
async refreshChannelMeta() {
|
|
5430
|
-
const enclaves =
|
|
5689
|
+
const enclaves = await this.enclavesToCover();
|
|
5431
5690
|
const now = Date.now();
|
|
5432
5691
|
for (const encId of enclaves) {
|
|
5433
5692
|
try {
|
|
@@ -5440,6 +5699,89 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
5440
5699
|
}
|
|
5441
5700
|
}
|
|
5442
5701
|
}
|
|
5702
|
+
/** Cache for the server-side enclave enumeration (see enclavesToCover). */
|
|
5703
|
+
enclaveEnumCache = null;
|
|
5704
|
+
/** Enclaves whose channel listing failed on the NEWEST sweep (audit HIGH:
|
|
5705
|
+
* skip-one deleted the sweep's partial-coverage signal — observations_stale
|
|
5706
|
+
* is a global max, so one healthy enclave masked a failed one). Surfaced on
|
|
5707
|
+
* health() so partial coverage is never silent. */
|
|
5708
|
+
sweepEnclavesFailed = [];
|
|
5709
|
+
/** Enclave ids whose channel listing has EVER succeeded this process, seeded
|
|
5710
|
+
* with HOME (home is always expected to list; without the seed, a home
|
|
5711
|
+
* failure right after a restart would read as "never succeeded" and slip
|
|
5712
|
+
* through the expected-gap branch — weaker than the old abort-on-failure).
|
|
5713
|
+
* A failure of a member of this set is a REGRESSION and degrades health; a
|
|
5714
|
+
* failure of a never-succeeded enclave (partner: tenant-scoped 404, permanent
|
|
5715
|
+
* pending backend authz) is an EXPECTED gap — reported, not degrading, or
|
|
5716
|
+
* every federated agent would peg degraded forever. */
|
|
5717
|
+
enclaveListEverSucceeded = /* @__PURE__ */ new Set();
|
|
5718
|
+
/** Warn-once-per-failure-streak, keyed by enclave id (cleared on success):
|
|
5719
|
+
* a permanently-404ing partner enclave must not log 1440 lines a day. */
|
|
5720
|
+
enclaveListWarned = /* @__PURE__ */ new Set();
|
|
5721
|
+
static ENCLAVE_ENUM_TTL_MS = 5 * 6e4;
|
|
5722
|
+
enclaveEnumWarned = false;
|
|
5723
|
+
/**
|
|
5724
|
+
* C1 (split-brain Task 3): every enclave this agent must COVER — for the
|
|
5725
|
+
* self-heal sweep and for channel-meta refresh alike. ONE helper on purpose:
|
|
5726
|
+
* these two call sites had drifted (refreshChannelMeta looped home+learned,
|
|
5727
|
+
* selfHealSweep looped nothing), and the drift is exactly what left both
|
|
5728
|
+
* production agents with zero recovery coverage in their second enclave.
|
|
5729
|
+
*
|
|
5730
|
+
* The set is SERVER enumeration ∪ home ∪ store-learned:
|
|
5731
|
+
*
|
|
5732
|
+
* - SERVER (`GET /api/v1/enclaves`, membership-scoped, federated included)
|
|
5733
|
+
* is the authoritative source. The store CANNOT be: channel_meta's only
|
|
5734
|
+
* writers are listChannels itself (circular — you learn enclave B by
|
|
5735
|
+
* having already listed B) and the receive pump's successfully-decrypted
|
|
5736
|
+
* loop, and a FORKED agent decrypts nothing, so the agent that most needs
|
|
5737
|
+
* sweeping in enclave B is precisely the one whose store never learned B.
|
|
5738
|
+
* - home + learned stay in the union as the degraded-mode floor: if the
|
|
5739
|
+
* enumeration endpoint is down, we sweep what we can rather than nothing,
|
|
5740
|
+
* and we warn (once) so the gap is diagnosable.
|
|
5741
|
+
*
|
|
5742
|
+
* The server result is cached for ENCLAVE_ENUM_TTL_MS so per-ingest-cycle
|
|
5743
|
+
* callers (refreshChannelMeta) do not add an HTTP round-trip per cycle;
|
|
5744
|
+
* membership changes are picked up within one TTL, which matches the sweep
|
|
5745
|
+
* cadence this exists to serve.
|
|
5746
|
+
*
|
|
5747
|
+
* ⚠️ Residual gap, stated: the enumeration RETURNS cross-tenant partner
|
|
5748
|
+
* enclaves, but `list_channels_in_enclave` is tenant-scoped and 404s them
|
|
5749
|
+
* under this agent's bearer — so a partner enclave in this set still yields
|
|
5750
|
+
* no channels. Callers must treat a per-enclave listing failure as skip-one,
|
|
5751
|
+
* never abort-all. Closing that needs backend authz work, not daemon work.
|
|
5752
|
+
*/
|
|
5753
|
+
async enclavesToCover() {
|
|
5754
|
+
const now = Date.now();
|
|
5755
|
+
if (!this.enclaveEnumCache || now - this.enclaveEnumCache.at > _BridgeAgent.ENCLAVE_ENUM_TTL_MS) {
|
|
5756
|
+
try {
|
|
5757
|
+
const url = `${this.cfg.apiBase}/api/v1/enclaves?caller_identity_id=${encodeURIComponent(this.cfg.identityId)}`;
|
|
5758
|
+
const res = await this.fetchImpl(url, {
|
|
5759
|
+
headers: { authorization: `Bearer ${this.cfg.apiKey}` }
|
|
5760
|
+
});
|
|
5761
|
+
if (!res.ok) throw new Error(`list_enclaves failed: ${res.status}`);
|
|
5762
|
+
const rows = await res.json();
|
|
5763
|
+
this.enclaveEnumCache = { ids: rows.map((r) => r.enclave_id), at: Date.now() };
|
|
5764
|
+
this.enclaveEnumWarned = false;
|
|
5765
|
+
} catch (e) {
|
|
5766
|
+
if (!this.enclaveEnumWarned) {
|
|
5767
|
+
this.enclaveEnumWarned = true;
|
|
5768
|
+
console.warn(
|
|
5769
|
+
"[bridge] enclave enumeration failed; sweep coverage degraded to home+learned enclaves:",
|
|
5770
|
+
e
|
|
5771
|
+
);
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5775
|
+
const fresh = this.enclaveEnumCache && Date.now() - this.enclaveEnumCache.at <= _BridgeAgent.ENCLAVE_ENUM_TTL_MS;
|
|
5776
|
+
if (fresh) {
|
|
5777
|
+
return /* @__PURE__ */ new Set([this.cfg.enclaveId, ...this.enclaveEnumCache.ids]);
|
|
5778
|
+
}
|
|
5779
|
+
return /* @__PURE__ */ new Set([
|
|
5780
|
+
this.cfg.enclaveId,
|
|
5781
|
+
...this.store.distinctEnclaveIds(),
|
|
5782
|
+
...this.enclaveEnumCache?.ids ?? []
|
|
5783
|
+
]);
|
|
5784
|
+
}
|
|
5443
5785
|
/** A live roster for a channel, or undefined if missing OR expired past the TTL. */
|
|
5444
5786
|
freshRoster(channelId) {
|
|
5445
5787
|
const hit = this.channelRoster.get(channelId);
|
|
@@ -6036,6 +6378,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
6036
6378
|
const stalePeriodMs = _BridgeAgent.SELF_HEAL_SWEEP_INTERVAL_MS + _BridgeAgent.SELF_HEAL_SWEEP_JITTER_MS;
|
|
6037
6379
|
const observationsStale = this.providerCapabilityMissing || channels.length === 0 || Date.now() - newest > stalePeriodMs;
|
|
6038
6380
|
const divergedChannels = channels.filter((o) => o.diverged === true).length;
|
|
6381
|
+
const sweepCoverageLost = this.sweepEnclavesFailed.some(
|
|
6382
|
+
(id) => this.enclaveListEverSucceeded.has(id)
|
|
6383
|
+
);
|
|
6039
6384
|
const liveChannels = channels.map((o) => ({
|
|
6040
6385
|
...o,
|
|
6041
6386
|
last_decrypt_at: this.provider.lastDecryptAtFor?.(o.channel_id) ?? o.last_decrypt_at
|
|
@@ -6050,8 +6395,9 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
6050
6395
|
observations_stale: observationsStale,
|
|
6051
6396
|
diverged_channels: divergedChannels,
|
|
6052
6397
|
// A2: the truthful roll-up gates read instead of `status` (D-3).
|
|
6053
|
-
degraded: divergedChannels > 0 || observationsStale,
|
|
6398
|
+
degraded: divergedChannels > 0 || observationsStale || sweepCoverageLost,
|
|
6054
6399
|
channels: liveChannels,
|
|
6400
|
+
sweep_enclaves_failed: [...this.sweepEnclavesFailed],
|
|
6055
6401
|
identity_id: this.cfg.identityId,
|
|
6056
6402
|
joined_channels: joined,
|
|
6057
6403
|
buffered: this.buffer.length,
|
|
@@ -6120,13 +6466,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
6120
6466
|
};
|
|
6121
6467
|
|
|
6122
6468
|
// src/config.ts
|
|
6123
|
-
import { readFileSync as
|
|
6124
|
-
import { dirname as dirname2, join as
|
|
6469
|
+
import { readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
|
|
6470
|
+
import { dirname as dirname2, join as join7 } from "node:path";
|
|
6125
6471
|
var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
|
|
6126
6472
|
function loadConfig(opts) {
|
|
6127
|
-
const raw =
|
|
6473
|
+
const raw = readFileSync5(opts.credentialPath, "utf8");
|
|
6128
6474
|
try {
|
|
6129
|
-
const mode =
|
|
6475
|
+
const mode = statSync2(opts.credentialPath).mode & 511;
|
|
6130
6476
|
if (mode & 63) process.stderr.write(`[bridge] WARNING: credential file ${opts.credentialPath} permission ${mode.toString(8)} is not 0600 \u2014 tighten it (chmod 600).
|
|
6131
6477
|
`);
|
|
6132
6478
|
} catch {
|
|
@@ -6138,7 +6484,7 @@ function loadConfig(opts) {
|
|
|
6138
6484
|
throw new Error("credential file is not valid JSON");
|
|
6139
6485
|
}
|
|
6140
6486
|
for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new Error(`credential file missing required field: ${k}`);
|
|
6141
|
-
const storePath = opts.storePath ??
|
|
6487
|
+
const storePath = opts.storePath ?? join7(dirname2(opts.credentialPath), "bridge-state.db");
|
|
6142
6488
|
const agentName = typeof json2.agent_name === "string" && json2.agent_name ? json2.agent_name : typeof json2.display_name === "string" && json2.display_name ? json2.display_name : void 0;
|
|
6143
6489
|
const rateLimitCapacity = typeof json2.rate_limit_capacity === "number" ? json2.rate_limit_capacity : 20;
|
|
6144
6490
|
const rateLimitRefillPerMinute = typeof json2.rate_limit_refill_per_minute === "number" ? json2.rate_limit_refill_per_minute : 20;
|
|
@@ -6146,6 +6492,8 @@ function loadConfig(opts) {
|
|
|
6146
6492
|
let memoryProvider;
|
|
6147
6493
|
let memoryBaseDir;
|
|
6148
6494
|
let memoryAgent;
|
|
6495
|
+
let memoryWorkspaceDir;
|
|
6496
|
+
let memoryFactsCharBudget;
|
|
6149
6497
|
if (memProvRaw === "none") {
|
|
6150
6498
|
memoryProvider = "none";
|
|
6151
6499
|
} else if (memProvRaw === "hermes") {
|
|
@@ -6159,6 +6507,10 @@ function loadConfig(opts) {
|
|
|
6159
6507
|
memoryBaseDir = typeof bd === "string" && bd ? bd : "~/.openclaw";
|
|
6160
6508
|
const ag = oc?.agent;
|
|
6161
6509
|
memoryAgent = typeof ag === "string" && ag ? ag : void 0;
|
|
6510
|
+
const wd = oc?.workspace_dir;
|
|
6511
|
+
memoryWorkspaceDir = typeof wd === "string" && wd ? wd : void 0;
|
|
6512
|
+
const fb = oc?.facts_char_budget;
|
|
6513
|
+
memoryFactsCharBudget = typeof fb === "number" && Number.isInteger(fb) && fb >= 0 ? fb : void 0;
|
|
6162
6514
|
} else {
|
|
6163
6515
|
memoryProvider = "securecoms";
|
|
6164
6516
|
}
|
|
@@ -6175,7 +6527,9 @@ function loadConfig(opts) {
|
|
|
6175
6527
|
rateLimitRefillPerMinute,
|
|
6176
6528
|
memoryProvider,
|
|
6177
6529
|
memoryBaseDir,
|
|
6178
|
-
memoryAgent
|
|
6530
|
+
memoryAgent,
|
|
6531
|
+
memoryWorkspaceDir,
|
|
6532
|
+
memoryFactsCharBudget
|
|
6179
6533
|
};
|
|
6180
6534
|
}
|
|
6181
6535
|
function arg(argv, flag) {
|
|
@@ -6219,11 +6573,11 @@ import { z } from "zod";
|
|
|
6219
6573
|
|
|
6220
6574
|
// src/auth.ts
|
|
6221
6575
|
import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
|
|
6222
|
-
import { existsSync, readFileSync as
|
|
6576
|
+
import { existsSync, readFileSync as readFileSync6, writeFileSync } from "node:fs";
|
|
6223
6577
|
function resolveToken(opts) {
|
|
6224
6578
|
if (opts.token) return opts.token;
|
|
6225
6579
|
const f = opts.tokenFile;
|
|
6226
|
-
if (f && existsSync(f)) return
|
|
6580
|
+
if (f && existsSync(f)) return readFileSync6(f, "utf8").trim();
|
|
6227
6581
|
const tok = randomBytes3(32).toString("base64url");
|
|
6228
6582
|
if (f) writeFileSync(f, tok, { mode: 384 });
|
|
6229
6583
|
return tok;
|