@securecoms/agent-bridge 0.3.2 → 0.5.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/README.md +21 -1
- package/dist/bin.js +1073 -90
- package/dist/bridge-agent.d.ts +68 -7
- package/dist/config.d.ts +7 -0
- package/dist/http-server.d.ts +42 -0
- package/dist/index.js +837 -63
- package/dist/memory/hermes-provider.d.ts +21 -0
- package/dist/memory/hermes-session-map.d.ts +8 -0
- package/dist/memory/hermes-store.d.ts +32 -0
- package/dist/memory/hermes-test-fixture.d.ts +5 -0
- package/dist/memory/openclaw-provider.d.ts +24 -0
- package/dist/memory/openclaw-session-log.d.ts +15 -0
- package/dist/memory/openclaw-session-map.d.ts +7 -0
- package/dist/memory/provider.d.ts +29 -0
- package/dist/memory-taps.d.ts +13 -0
- package/dist/preflight-modern.cjs +35 -0
- package/dist/preflight-modern.d.cts +1 -0
- package/dist/preflight-upgrade.d.cts +30 -0
- package/dist/preflight.cjs +277 -0
- package/dist/preflight.d.cts +14 -0
- package/dist/service/config.d.ts +1 -0
- package/dist/service/install-prep.d.ts +68 -0
- package/package.json +3 -5
- package/dist/attachments.js +0 -49
- package/dist/auth.js +0 -56
- package/dist/bin.js.pre-c1-f803007 +0 -5795
- package/dist/bin.js.pre-c3-096a9a3 +0 -5832
- package/dist/bin.js.pre-ffrebuild-b162d4a +0 -5639
- package/dist/bin.js.pre-kpheal-20260708-150924 +0 -5531
- package/dist/bin.js.pre-kppersist-20260708-152341 +0 -5543
- package/dist/bin.js.pre-p0-20260708-102415 +0 -5196
- package/dist/bin.js.pre-p1b-02f47e1 +0 -5639
- package/dist/bin.js.pre-warmfix-20260708-153705 +0 -5613
- package/dist/bin.js.pretrace-113205 +0 -5395
- package/dist/bridge-agent.js +0 -831
- package/dist/config.js +0 -74
- package/dist/coordination.js +0 -63
- package/dist/errors.js +0 -25
- package/dist/http-server.js +0 -220
- package/dist/index.js.pre-p0-20260708-102415 +0 -4879
- package/dist/latency.js +0 -87
- package/dist/rate-limit.js +0 -56
- package/dist/service/config.js +0 -26
- package/dist/service/install.js +0 -60
- package/dist/service/launchd.js +0 -30
- package/dist/service/systemd.js +0 -25
- package/dist/ws-server.js +0 -254
package/dist/index.js
CHANGED
|
@@ -568,6 +568,152 @@ var payloadsSchema = {
|
|
|
568
568
|
}
|
|
569
569
|
}
|
|
570
570
|
]
|
|
571
|
+
},
|
|
572
|
+
"call_control": {
|
|
573
|
+
"description": 'The call_control signaling body for 1:1 voice calls (docs/superpowers/specs/2026-07-12-voice-human-to-human-calling-design.md). NOT a message_type \u2014 it rides envelope message_type "system_event"; the "kind":"call_control" field is a body-level discriminator and "event" selects the variant. Compiled standalone via validate.ts validateCallControlBody. SERVER-BLIND KEY CONTRACT: NO variant ever carries media key material (the media key is derived independently from the MLS exporter, never transmitted). Every variant sets additionalProperties:false so a stray media_key/key/secret is rejected \u2014 the only key-adjacent field is `key_confirmation` on accept, which is HMAC(media_key, call_id), not the key itself.',
|
|
574
|
+
"oneOf": [
|
|
575
|
+
{
|
|
576
|
+
"type": "object",
|
|
577
|
+
"additionalProperties": false,
|
|
578
|
+
"required": [
|
|
579
|
+
"kind",
|
|
580
|
+
"event",
|
|
581
|
+
"call_id",
|
|
582
|
+
"room",
|
|
583
|
+
"caller_identity_id",
|
|
584
|
+
"epoch",
|
|
585
|
+
"ts"
|
|
586
|
+
],
|
|
587
|
+
"properties": {
|
|
588
|
+
"kind": {
|
|
589
|
+
"const": "call_control"
|
|
590
|
+
},
|
|
591
|
+
"event": {
|
|
592
|
+
"const": "invite"
|
|
593
|
+
},
|
|
594
|
+
"call_id": {
|
|
595
|
+
"type": "string",
|
|
596
|
+
"minLength": 1
|
|
597
|
+
},
|
|
598
|
+
"room": {
|
|
599
|
+
"type": "string",
|
|
600
|
+
"minLength": 1
|
|
601
|
+
},
|
|
602
|
+
"caller_identity_id": {
|
|
603
|
+
"type": "string",
|
|
604
|
+
"format": "uuid"
|
|
605
|
+
},
|
|
606
|
+
"epoch": {
|
|
607
|
+
"type": "integer",
|
|
608
|
+
"minimum": 0
|
|
609
|
+
},
|
|
610
|
+
"ts": {
|
|
611
|
+
"type": "number"
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
"type": "object",
|
|
617
|
+
"additionalProperties": false,
|
|
618
|
+
"required": [
|
|
619
|
+
"kind",
|
|
620
|
+
"event",
|
|
621
|
+
"call_id",
|
|
622
|
+
"accepting_device_id",
|
|
623
|
+
"key_confirmation"
|
|
624
|
+
],
|
|
625
|
+
"properties": {
|
|
626
|
+
"kind": {
|
|
627
|
+
"const": "call_control"
|
|
628
|
+
},
|
|
629
|
+
"event": {
|
|
630
|
+
"const": "accept"
|
|
631
|
+
},
|
|
632
|
+
"call_id": {
|
|
633
|
+
"type": "string",
|
|
634
|
+
"minLength": 1
|
|
635
|
+
},
|
|
636
|
+
"accepting_device_id": {
|
|
637
|
+
"type": "string",
|
|
638
|
+
"minLength": 1
|
|
639
|
+
},
|
|
640
|
+
"key_confirmation": {
|
|
641
|
+
"type": "string",
|
|
642
|
+
"minLength": 1
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
"type": "object",
|
|
648
|
+
"additionalProperties": false,
|
|
649
|
+
"required": [
|
|
650
|
+
"kind",
|
|
651
|
+
"event",
|
|
652
|
+
"call_id"
|
|
653
|
+
],
|
|
654
|
+
"properties": {
|
|
655
|
+
"kind": {
|
|
656
|
+
"const": "call_control"
|
|
657
|
+
},
|
|
658
|
+
"event": {
|
|
659
|
+
"const": "decline"
|
|
660
|
+
},
|
|
661
|
+
"call_id": {
|
|
662
|
+
"type": "string",
|
|
663
|
+
"minLength": 1
|
|
664
|
+
},
|
|
665
|
+
"reason": {
|
|
666
|
+
"type": "string",
|
|
667
|
+
"enum": [
|
|
668
|
+
"busy",
|
|
669
|
+
"declined"
|
|
670
|
+
]
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
"type": "object",
|
|
676
|
+
"additionalProperties": false,
|
|
677
|
+
"required": [
|
|
678
|
+
"kind",
|
|
679
|
+
"event",
|
|
680
|
+
"call_id"
|
|
681
|
+
],
|
|
682
|
+
"properties": {
|
|
683
|
+
"kind": {
|
|
684
|
+
"const": "call_control"
|
|
685
|
+
},
|
|
686
|
+
"event": {
|
|
687
|
+
"const": "cancel"
|
|
688
|
+
},
|
|
689
|
+
"call_id": {
|
|
690
|
+
"type": "string",
|
|
691
|
+
"minLength": 1
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
"type": "object",
|
|
697
|
+
"additionalProperties": false,
|
|
698
|
+
"required": [
|
|
699
|
+
"kind",
|
|
700
|
+
"event",
|
|
701
|
+
"call_id"
|
|
702
|
+
],
|
|
703
|
+
"properties": {
|
|
704
|
+
"kind": {
|
|
705
|
+
"const": "call_control"
|
|
706
|
+
},
|
|
707
|
+
"event": {
|
|
708
|
+
"const": "end"
|
|
709
|
+
},
|
|
710
|
+
"call_id": {
|
|
711
|
+
"type": "string",
|
|
712
|
+
"minLength": 1
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
]
|
|
571
717
|
}
|
|
572
718
|
}
|
|
573
719
|
};
|
|
@@ -592,6 +738,11 @@ if (!agentActionDef) {
|
|
|
592
738
|
throw new Error(`payloads.schema.json is missing the "agent_action" $defs schema`);
|
|
593
739
|
}
|
|
594
740
|
var agentActionValidator = ajv.compile(agentActionDef);
|
|
741
|
+
var callControlDef = payloadDefs["call_control"];
|
|
742
|
+
if (!callControlDef) {
|
|
743
|
+
throw new Error(`payloads.schema.json is missing the "call_control" $defs schema`);
|
|
744
|
+
}
|
|
745
|
+
var callControlValidator = ajv.compile(callControlDef);
|
|
595
746
|
|
|
596
747
|
// ../connect-core/dist/envelope/sign.js
|
|
597
748
|
import { canonicalize } from "@agentvault/crypto/canonical";
|
|
@@ -653,6 +804,7 @@ import { gcm } from "@noble/ciphers/aes";
|
|
|
653
804
|
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
654
805
|
import { randomBytes } from "@noble/hashes/utils";
|
|
655
806
|
import { canonicalize as canonicalize2 } from "@agentvault/crypto/canonical";
|
|
807
|
+
var VOICE_MEDIA_EXPORTER_LABEL = "securecoms-voice-media-v1";
|
|
656
808
|
var APPLICATION_KEY_BYTES = 32;
|
|
657
809
|
var REPLAY_NONCE_BYTES = 24;
|
|
658
810
|
var NONCE_BYTES = {
|
|
@@ -674,10 +826,20 @@ function epochToBytes(epoch) {
|
|
|
674
826
|
new DataView(b.buffer).setBigUint64(0, BigInt(epoch), false);
|
|
675
827
|
return b;
|
|
676
828
|
}
|
|
829
|
+
function u32ToBytes(n) {
|
|
830
|
+
const b = new Uint8Array(4);
|
|
831
|
+
new DataView(b.buffer).setUint32(0, n >>> 0, false);
|
|
832
|
+
return b;
|
|
833
|
+
}
|
|
677
834
|
function buildExporterContext(enclaveId, channelId, epoch) {
|
|
678
835
|
const enc = new TextEncoder();
|
|
679
836
|
return concatBytes(enc.encode(enclaveId), enc.encode(channelId), epochToBytes(epoch));
|
|
680
837
|
}
|
|
838
|
+
function buildVoiceExporterContext(enclaveId, channelId, epoch, callId) {
|
|
839
|
+
const enc = new TextEncoder();
|
|
840
|
+
const callIdBytes = enc.encode(callId);
|
|
841
|
+
return concatBytes(buildExporterContext(enclaveId, channelId, epoch), u32ToBytes(callIdBytes.length), callIdBytes);
|
|
842
|
+
}
|
|
681
843
|
function cipherFor(suite, key, nonce, aad) {
|
|
682
844
|
return suite === "aes256gcm" ? gcm(key, nonce, aad) : xchacha20poly1305(key, nonce, aad);
|
|
683
845
|
}
|
|
@@ -1045,8 +1207,13 @@ var ALWAYS_SAMPLE = /* @__PURE__ */ new Set([
|
|
|
1045
1207
|
// ../connect-sdk-ts/dist/spans/emitter.js
|
|
1046
1208
|
var KNOWN = new Set(Object.values(SPAN));
|
|
1047
1209
|
|
|
1210
|
+
// ../connect-sdk-ts/dist/voice-key.js
|
|
1211
|
+
import { hmac } from "@noble/hashes/hmac";
|
|
1212
|
+
import { sha256 as sha2562 } from "@noble/hashes/sha256";
|
|
1213
|
+
import { bytesToHex } from "@agentvault/crypto";
|
|
1214
|
+
|
|
1048
1215
|
// ../connect-sdk-ts/dist/groupkey/mls.js
|
|
1049
|
-
import { MLSGroupManager, bytesToHex, hexToBytes, verifyApprovalArtifact } from "@agentvault/crypto";
|
|
1216
|
+
import { MLSGroupManager, bytesToHex as bytesToHex2, hexToBytes, verifyApprovalArtifact } from "@agentvault/crypto";
|
|
1050
1217
|
|
|
1051
1218
|
// ../connect-sdk-ts/dist/groupkey/operation-queue.js
|
|
1052
1219
|
var MlsOperationQueue = class {
|
|
@@ -1426,6 +1593,28 @@ var MlsGroupKeyProvider = class {
|
|
|
1426
1593
|
const mgr = await this.manager(channelId);
|
|
1427
1594
|
return mgr.exportSecret(LABEL, buildExporterContext(enclaveId, channelId, epoch), 32);
|
|
1428
1595
|
}
|
|
1596
|
+
/**
|
|
1597
|
+
* The 1:1 voice media key: taken STRAIGHT from the MLS exporter under a
|
|
1598
|
+
* DEDICATED label ({@link VOICE_MEDIA_EXPORTER_LABEL}) and a context that
|
|
1599
|
+
* binds `callId` (voice-h2h spec, audit H-P1) — domain-separated from the
|
|
1600
|
+
* message key ({@link exporterKey}) and per-call, with NO HKDF layered on
|
|
1601
|
+
* top. Taken under the per-channel lock (as the receive-side message-key
|
|
1602
|
+
* read is, mls.ts) so the exporter read is epoch-stable against a concurrent
|
|
1603
|
+
* commit. Never transmitted or persisted; both call participants derive it
|
|
1604
|
+
* independently at the invite epoch.
|
|
1605
|
+
*
|
|
1606
|
+
* CONTRACT: unlike {@link exporterKey} (lock-free — its callers wrap it), this
|
|
1607
|
+
* takes the channel lock INTERNALLY. The lock queue is non-reentrant, so this
|
|
1608
|
+
* MUST NOT be called while already holding this channel's lock (doing so
|
|
1609
|
+
* self-deadlocks). Call it stand-alone from the call-setup path, never from
|
|
1610
|
+
* inside another `locks.run(channelId, ...)` for the same channel.
|
|
1611
|
+
*/
|
|
1612
|
+
async voiceMediaKey(enclaveId, channelId, epoch, callId) {
|
|
1613
|
+
return this.locks.run(channelId, async () => {
|
|
1614
|
+
const mgr = await this.manager(channelId);
|
|
1615
|
+
return mgr.exportSecret(VOICE_MEDIA_EXPORTER_LABEL, buildVoiceExporterContext(enclaveId, channelId, epoch, callId), 32);
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1429
1618
|
/**
|
|
1430
1619
|
* Join a channel from the host-committed Welcome, then drain any commits that
|
|
1431
1620
|
* landed after the Welcome so we converge on the true current epoch. Without
|
|
@@ -1554,7 +1743,7 @@ var MlsGroupKeyProvider = class {
|
|
|
1554
1743
|
const bundle = await this.o.store.loadPendingBundle(this.o.deviceId);
|
|
1555
1744
|
if (!bundle)
|
|
1556
1745
|
return false;
|
|
1557
|
-
const heldHex =
|
|
1746
|
+
const heldHex = bytesToHex2(MLSGroupManager.serializeKeyPackage(bundle.publicPackage));
|
|
1558
1747
|
const published = await this.o.backend.fetchKeyPackages([this.o.deviceId]);
|
|
1559
1748
|
const pubHex = published[this.o.deviceId];
|
|
1560
1749
|
return !!pubHex && pubHex === heldHex;
|
|
@@ -1570,7 +1759,7 @@ var MlsGroupKeyProvider = class {
|
|
|
1570
1759
|
console.warn(`publishKeyPackage: minting a fresh KeyPackage (rotation) for device ${this.o.deviceId.slice(0, 8)}`);
|
|
1571
1760
|
const mgr = new MLSGroupManager();
|
|
1572
1761
|
const bundle = await mgr.generateKeyPackage(this.o.identity);
|
|
1573
|
-
const hex =
|
|
1762
|
+
const hex = bytesToHex2(MLSGroupManager.serializeKeyPackage(bundle.publicPackage));
|
|
1574
1763
|
const id = await this.o.backend.publishKeyPackage(this.o.deviceId, hex);
|
|
1575
1764
|
await this.o.store.saveLastKpAndBundle(this.o.deviceId, id, bundle);
|
|
1576
1765
|
return id;
|
|
@@ -1610,11 +1799,11 @@ var MlsGroupKeyProvider = class {
|
|
|
1610
1799
|
const { commit, welcome } = await mgr.addMembers(pkgs);
|
|
1611
1800
|
await this.o.backend.distribute(reinit.group_id, {
|
|
1612
1801
|
device_id: this.o.deviceId,
|
|
1613
|
-
commit:
|
|
1802
|
+
commit: bytesToHex2(commit),
|
|
1614
1803
|
commit_epoch: Number(mgr.epoch),
|
|
1615
1804
|
expected_epoch: reinit.current_epoch,
|
|
1616
1805
|
member_device_ids: [this.o.deviceId, ...others],
|
|
1617
|
-
welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload:
|
|
1806
|
+
welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload: bytesToHex2(welcome) })) : []
|
|
1618
1807
|
});
|
|
1619
1808
|
await this.o.store.save(channelId, mgr.exportState());
|
|
1620
1809
|
this.managers.set(channelId, mgr);
|
|
@@ -1637,11 +1826,11 @@ var MlsGroupKeyProvider = class {
|
|
|
1637
1826
|
const { commit, welcome } = await mgr.addMembers(pkgs);
|
|
1638
1827
|
await this.o.backend.distribute(init.group_id, {
|
|
1639
1828
|
device_id: this.o.deviceId,
|
|
1640
|
-
commit:
|
|
1829
|
+
commit: bytesToHex2(commit),
|
|
1641
1830
|
commit_epoch: Number(mgr.epoch),
|
|
1642
1831
|
expected_epoch: init.current_epoch,
|
|
1643
1832
|
member_device_ids: [this.o.deviceId, ...others],
|
|
1644
|
-
welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload:
|
|
1833
|
+
welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload: bytesToHex2(welcome) })) : []
|
|
1645
1834
|
});
|
|
1646
1835
|
this.managers.set(channelId, mgr);
|
|
1647
1836
|
await this.o.store.save(channelId, mgr.exportState());
|
|
@@ -1701,12 +1890,12 @@ var MlsGroupKeyProvider = class {
|
|
|
1701
1890
|
try {
|
|
1702
1891
|
await this.o.backend.distribute(init.group_id, {
|
|
1703
1892
|
device_id: this.o.deviceId,
|
|
1704
|
-
commit:
|
|
1893
|
+
commit: bytesToHex2(commit),
|
|
1705
1894
|
commit_epoch: Number(mgr.epoch),
|
|
1706
1895
|
expected_epoch: priorEpoch,
|
|
1707
1896
|
member_device_ids: members,
|
|
1708
1897
|
op: "add",
|
|
1709
|
-
welcomes: welcome ? [{ target_device_id: newDeviceId, payload:
|
|
1898
|
+
welcomes: welcome ? [{ target_device_id: newDeviceId, payload: bytesToHex2(welcome) }] : []
|
|
1710
1899
|
});
|
|
1711
1900
|
} catch (e) {
|
|
1712
1901
|
this.managers.delete(channelId);
|
|
@@ -1764,7 +1953,7 @@ var MlsGroupKeyProvider = class {
|
|
|
1764
1953
|
try {
|
|
1765
1954
|
await this.o.backend.distribute(init.group_id, {
|
|
1766
1955
|
device_id: this.o.deviceId,
|
|
1767
|
-
commit:
|
|
1956
|
+
commit: bytesToHex2(commit),
|
|
1768
1957
|
commit_epoch: Number(mgr.epoch),
|
|
1769
1958
|
expected_epoch: priorEpoch,
|
|
1770
1959
|
member_device_ids: members,
|
|
@@ -1832,11 +2021,11 @@ var MlsGroupKeyProvider = class {
|
|
|
1832
2021
|
}
|
|
1833
2022
|
await this.o.store.saveStaging(channelId, mgr.exportState());
|
|
1834
2023
|
const members = init.member_device_ids ?? [this.o.deviceId];
|
|
1835
|
-
const removedIdentity = typeof opts.identity === "string" ? opts.identity :
|
|
2024
|
+
const removedIdentity = typeof opts.identity === "string" ? opts.identity : bytesToHex2(opts.identity);
|
|
1836
2025
|
try {
|
|
1837
2026
|
await this.o.backend.distribute(init.group_id, {
|
|
1838
2027
|
device_id: this.o.deviceId,
|
|
1839
|
-
commit:
|
|
2028
|
+
commit: bytesToHex2(commit),
|
|
1840
2029
|
commit_epoch: Number(mgr.epoch),
|
|
1841
2030
|
expected_epoch: priorEpoch,
|
|
1842
2031
|
member_device_ids: members,
|
|
@@ -2067,11 +2256,11 @@ var MlsGroupKeyProvider = class {
|
|
|
2067
2256
|
const { commit, welcome } = await mgr.addMembers(pkgs);
|
|
2068
2257
|
await this.o.backend.distribute(reinit.group_id, {
|
|
2069
2258
|
device_id: this.o.deviceId,
|
|
2070
|
-
commit:
|
|
2259
|
+
commit: bytesToHex2(commit),
|
|
2071
2260
|
commit_epoch: Number(mgr.epoch),
|
|
2072
2261
|
expected_epoch: reinit.current_epoch,
|
|
2073
2262
|
member_device_ids: [this.o.deviceId, ...others],
|
|
2074
|
-
welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload:
|
|
2263
|
+
welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload: bytesToHex2(welcome) })) : []
|
|
2075
2264
|
});
|
|
2076
2265
|
await this.o.store.save(channelId, mgr.exportState());
|
|
2077
2266
|
this.managers.set(channelId, mgr);
|
|
@@ -2441,6 +2630,15 @@ function parseServerFrame(frame) {
|
|
|
2441
2630
|
}
|
|
2442
2631
|
|
|
2443
2632
|
// ../connect-sdk-ts/dist/transport/ws-pings.js
|
|
2633
|
+
function base64UrlNoPad(s) {
|
|
2634
|
+
const bytes = new TextEncoder().encode(s);
|
|
2635
|
+
let bin = "";
|
|
2636
|
+
for (const b of bytes)
|
|
2637
|
+
bin += String.fromCharCode(b);
|
|
2638
|
+
const b64 = typeof btoa === "function" ? btoa(bin) : Buffer.from(bytes).toString("base64");
|
|
2639
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2640
|
+
}
|
|
2641
|
+
var BEARER_SUBPROTOCOL = "securecoms.bearer.v1";
|
|
2444
2642
|
function openDeliveryPings(o) {
|
|
2445
2643
|
const base = o.backoffBaseMs ?? 500;
|
|
2446
2644
|
const cap = o.backoffCapMs ?? 8e3;
|
|
@@ -2589,8 +2787,7 @@ function openDeliveryPings(o) {
|
|
|
2589
2787
|
}
|
|
2590
2788
|
if (closed || gen !== connectGen)
|
|
2591
2789
|
return;
|
|
2592
|
-
const
|
|
2593
|
-
const ws = new o.wsImpl(`${o.url}${sep}token=${encodeURIComponent(token)}`);
|
|
2790
|
+
const ws = new o.wsImpl(o.url, [BEARER_SUBPROTOCOL, base64UrlNoPad(token)]);
|
|
2594
2791
|
sock = ws;
|
|
2595
2792
|
ws.onopen = () => {
|
|
2596
2793
|
push({ reconnected: true });
|
|
@@ -3023,11 +3220,11 @@ var RestMlsBackend = class {
|
|
|
3023
3220
|
};
|
|
3024
3221
|
|
|
3025
3222
|
// ../connect-sdk-ts/dist/mls/fake-backend.js
|
|
3026
|
-
import { bytesToHex as
|
|
3223
|
+
import { bytesToHex as bytesToHex3, hexToBytes as hexToBytes2 } from "@agentvault/crypto";
|
|
3027
3224
|
|
|
3028
3225
|
// ../connect-sdk-ts/dist/receive/inbox-cipher.js
|
|
3029
3226
|
import { hkdf } from "@noble/hashes/hkdf";
|
|
3030
|
-
import { sha256 as
|
|
3227
|
+
import { sha256 as sha2563 } from "@noble/hashes/sha256";
|
|
3031
3228
|
var SUITE = "xchacha20poly1305";
|
|
3032
3229
|
var INFO = new TextEncoder().encode("agentvault-connect-inbox-v1");
|
|
3033
3230
|
var EMPTY_AAD = new Uint8Array(0);
|
|
@@ -3055,12 +3252,12 @@ var KeyedInboxCipher = class {
|
|
|
3055
3252
|
};
|
|
3056
3253
|
var MLS_STORE_INFO = new TextEncoder().encode("agentvault-connect-mls-store-v1");
|
|
3057
3254
|
function deriveMlsStoreKey(identityKeyMaterial) {
|
|
3058
|
-
return hkdf(
|
|
3255
|
+
return hkdf(sha2563, identityKeyMaterial, void 0, MLS_STORE_INFO, 32);
|
|
3059
3256
|
}
|
|
3060
3257
|
|
|
3061
3258
|
// ../connect-sdk-ts/dist/recovery/backup-crypto.js
|
|
3062
3259
|
import { hkdf as hkdf2 } from "@noble/hashes/hkdf";
|
|
3063
|
-
import { sha256 as
|
|
3260
|
+
import { sha256 as sha2564 } from "@noble/hashes/sha256";
|
|
3064
3261
|
import { randomBytes as randomBytes2 } from "@noble/hashes/utils";
|
|
3065
3262
|
var BACKUP_INFO = new TextEncoder().encode("connect-backup-v1");
|
|
3066
3263
|
var RECOVERY_INFO = new TextEncoder().encode("connect-recovery-v1");
|
|
@@ -3068,15 +3265,15 @@ var RECOVERY_INFO = new TextEncoder().encode("connect-recovery-v1");
|
|
|
3068
3265
|
// ../connect-sdk-ts/dist/recovery/guest-recovery-crypto.js
|
|
3069
3266
|
import { argon2idAsync } from "@noble/hashes/argon2";
|
|
3070
3267
|
import { hkdf as hkdf3 } from "@noble/hashes/hkdf";
|
|
3071
|
-
import { sha256 as
|
|
3268
|
+
import { sha256 as sha2565 } from "@noble/hashes/sha256";
|
|
3072
3269
|
var HKDF_INFO = new TextEncoder().encode("connect-guest-recovery-wrap-v1");
|
|
3073
3270
|
|
|
3074
3271
|
// ../connect-mls-store/dist/sqlite-store.js
|
|
3075
3272
|
import { DatabaseSync } from "node:sqlite";
|
|
3076
|
-
import { MLSGroupManager as MLSGroupManager2, bytesToHex as
|
|
3273
|
+
import { MLSGroupManager as MLSGroupManager2, bytesToHex as bytesToHex4, hexToBytes as hexToBytes3 } from "@agentvault/crypto";
|
|
3077
3274
|
|
|
3078
3275
|
// ../connect-mls-store/dist/schema.js
|
|
3079
|
-
var SCHEMA_VERSION =
|
|
3276
|
+
var SCHEMA_VERSION = 6;
|
|
3080
3277
|
function openSchema(db) {
|
|
3081
3278
|
db.exec("PRAGMA journal_mode=WAL");
|
|
3082
3279
|
const row = db.prepare("PRAGMA user_version").get();
|
|
@@ -3123,6 +3320,51 @@ function openSchema(db) {
|
|
|
3123
3320
|
);
|
|
3124
3321
|
`);
|
|
3125
3322
|
}
|
|
3323
|
+
if (row.user_version < 5) {
|
|
3324
|
+
db.exec(`
|
|
3325
|
+
CREATE TABLE IF NOT EXISTS channel_meta (
|
|
3326
|
+
identity_id TEXT NOT NULL,
|
|
3327
|
+
channel_id TEXT NOT NULL,
|
|
3328
|
+
enclave_id TEXT,
|
|
3329
|
+
is_dm INTEGER,
|
|
3330
|
+
updated_at INTEGER NOT NULL,
|
|
3331
|
+
PRIMARY KEY (identity_id, channel_id)
|
|
3332
|
+
);
|
|
3333
|
+
`);
|
|
3334
|
+
}
|
|
3335
|
+
if (row.user_version < 6) {
|
|
3336
|
+
db.exec(`
|
|
3337
|
+
CREATE TABLE IF NOT EXISTS agent_memory (
|
|
3338
|
+
identity_id TEXT NOT NULL,
|
|
3339
|
+
conversation_id TEXT NOT NULL,
|
|
3340
|
+
seq INTEGER NOT NULL,
|
|
3341
|
+
role TEXT NOT NULL,
|
|
3342
|
+
peer_id TEXT,
|
|
3343
|
+
sealed_text BLOB NOT NULL,
|
|
3344
|
+
modality TEXT NOT NULL,
|
|
3345
|
+
message_id TEXT,
|
|
3346
|
+
idempotency_key TEXT,
|
|
3347
|
+
dedup_key TEXT,
|
|
3348
|
+
recorded_at INTEGER NOT NULL,
|
|
3349
|
+
PRIMARY KEY (identity_id, conversation_id, seq)
|
|
3350
|
+
);
|
|
3351
|
+
`);
|
|
3352
|
+
db.exec(`
|
|
3353
|
+
CREATE UNIQUE INDEX IF NOT EXISTS agent_memory_dedup
|
|
3354
|
+
ON agent_memory (identity_id, conversation_id, dedup_key)
|
|
3355
|
+
WHERE dedup_key IS NOT NULL;
|
|
3356
|
+
`);
|
|
3357
|
+
db.exec(`
|
|
3358
|
+
CREATE TABLE IF NOT EXISTS agent_memory_fact (
|
|
3359
|
+
identity_id TEXT NOT NULL,
|
|
3360
|
+
conversation_id TEXT NOT NULL,
|
|
3361
|
+
fact_id INTEGER NOT NULL,
|
|
3362
|
+
sealed_fact BLOB NOT NULL,
|
|
3363
|
+
added_at INTEGER NOT NULL,
|
|
3364
|
+
PRIMARY KEY (identity_id, conversation_id, fact_id)
|
|
3365
|
+
);
|
|
3366
|
+
`);
|
|
3367
|
+
}
|
|
3126
3368
|
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
3127
3369
|
}
|
|
3128
3370
|
|
|
@@ -3144,10 +3386,10 @@ var MlsStoreSealError = class extends Error {
|
|
|
3144
3386
|
function encodeBundle(bundle) {
|
|
3145
3387
|
const pp = bundle.privatePackage;
|
|
3146
3388
|
const obj = {
|
|
3147
|
-
pub:
|
|
3148
|
-
initPriv:
|
|
3149
|
-
hpkePriv:
|
|
3150
|
-
sigPriv:
|
|
3389
|
+
pub: bytesToHex4(MLSGroupManager2.serializeKeyPackage(bundle.publicPackage)),
|
|
3390
|
+
initPriv: bytesToHex4(pp.initPrivateKey),
|
|
3391
|
+
hpkePriv: bytesToHex4(pp.hpkePrivateKey),
|
|
3392
|
+
sigPriv: bytesToHex4(pp.signaturePrivateKey)
|
|
3151
3393
|
};
|
|
3152
3394
|
return new TextEncoder().encode(JSON.stringify(obj));
|
|
3153
3395
|
}
|
|
@@ -3174,9 +3416,11 @@ var SqliteMlsStateStore = class {
|
|
|
3174
3416
|
// the source of truth across process restarts.
|
|
3175
3417
|
bundles = /* @__PURE__ */ new Map();
|
|
3176
3418
|
lastKpIds = /* @__PURE__ */ new Map();
|
|
3419
|
+
memoryRetentionK;
|
|
3177
3420
|
constructor(opts) {
|
|
3178
3421
|
this.id = opts.identityId;
|
|
3179
3422
|
this.cipher = opts.cipherFor(opts.identityId);
|
|
3423
|
+
this.memoryRetentionK = opts.memoryRetentionK ?? 500;
|
|
3180
3424
|
try {
|
|
3181
3425
|
this.db = new DatabaseSync(opts.dbPath);
|
|
3182
3426
|
} catch (e) {
|
|
@@ -3311,6 +3555,74 @@ var SqliteMlsStateStore = class {
|
|
|
3311
3555
|
const rows = this.db.prepare("SELECT group_id FROM mls_retired_group WHERE channel_id=?").all(channelId);
|
|
3312
3556
|
return rows.map((r) => r.group_id);
|
|
3313
3557
|
}
|
|
3558
|
+
recordChannelEnclave(channelId, enclaveId) {
|
|
3559
|
+
this.db.prepare(`INSERT INTO channel_meta (identity_id, channel_id, enclave_id, updated_at)
|
|
3560
|
+
VALUES (?,?,?,?)
|
|
3561
|
+
ON CONFLICT(identity_id, channel_id) DO UPDATE SET enclave_id=excluded.enclave_id, updated_at=excluded.updated_at`).run(this.id, channelId, enclaveId, Date.now());
|
|
3562
|
+
}
|
|
3563
|
+
recordChannelIsDm(channelId, isDm) {
|
|
3564
|
+
this.db.prepare(`INSERT INTO channel_meta (identity_id, channel_id, is_dm, updated_at)
|
|
3565
|
+
VALUES (?,?,?,?)
|
|
3566
|
+
ON CONFLICT(identity_id, channel_id) DO UPDATE SET is_dm=excluded.is_dm, updated_at=excluded.updated_at`).run(this.id, channelId, isDm ? 1 : 0, Date.now());
|
|
3567
|
+
}
|
|
3568
|
+
getChannelMeta(channelId) {
|
|
3569
|
+
const row = this.db.prepare("SELECT enclave_id, is_dm FROM channel_meta WHERE identity_id=? AND channel_id=?").get(this.id, channelId);
|
|
3570
|
+
if (!row)
|
|
3571
|
+
return void 0;
|
|
3572
|
+
return {
|
|
3573
|
+
enclaveId: row.enclave_id ?? void 0,
|
|
3574
|
+
isDm: row.is_dm == null ? void 0 : row.is_dm === 1
|
|
3575
|
+
};
|
|
3576
|
+
}
|
|
3577
|
+
/**
|
|
3578
|
+
* The set of enclave ids this identity has learned about (from inbound
|
|
3579
|
+
* envelopes or a prior listChannels discovery), beyond its own home
|
|
3580
|
+
* enclave. Task 3: refreshChannelMeta unions this with cfg.enclaveId so a
|
|
3581
|
+
* cross-enclave room's is_dm gets resolved instead of staying UNKNOWN.
|
|
3582
|
+
*/
|
|
3583
|
+
distinctEnclaveIds() {
|
|
3584
|
+
const rows = this.db.prepare("SELECT DISTINCT enclave_id FROM channel_meta WHERE identity_id=? AND enclave_id IS NOT NULL").all(this.id);
|
|
3585
|
+
return rows.map((r) => r.enclave_id);
|
|
3586
|
+
}
|
|
3587
|
+
async recordMemory(input) {
|
|
3588
|
+
const dedupKey = input.messageId ?? input.idempotencyKey ?? null;
|
|
3589
|
+
if (!dedupKey)
|
|
3590
|
+
throw new Error("recordMemory requires message_id or idempotency_key");
|
|
3591
|
+
const sealed = await this.cipher.seal(new TextEncoder().encode(input.text));
|
|
3592
|
+
const info = this.db.prepare(`INSERT OR IGNORE INTO agent_memory
|
|
3593
|
+
(identity_id, conversation_id, seq, role, peer_id, sealed_text, modality, message_id, idempotency_key, dedup_key, recorded_at)
|
|
3594
|
+
VALUES (?, ?,
|
|
3595
|
+
(SELECT COALESCE(MAX(seq),0)+1 FROM agent_memory WHERE identity_id=? AND conversation_id=?),
|
|
3596
|
+
?, ?, ?, ?, ?, ?, ?, ?)`).run(this.id, input.conversationId, this.id, input.conversationId, input.role, input.peerId ?? null, sealed, input.modality, input.messageId ?? null, input.idempotencyKey ?? null, dedupKey, Date.now());
|
|
3597
|
+
if (info.changes === 0) {
|
|
3598
|
+
const row2 = this.db.prepare("SELECT seq FROM agent_memory WHERE identity_id=? AND conversation_id=? AND dedup_key=?").get(this.id, input.conversationId, dedupKey);
|
|
3599
|
+
return { seq: row2.seq, deduped: true };
|
|
3600
|
+
}
|
|
3601
|
+
const row = this.db.prepare("SELECT MAX(seq) AS seq FROM agent_memory WHERE identity_id=? AND conversation_id=?").get(this.id, input.conversationId);
|
|
3602
|
+
if (this.memoryRetentionK > 0) {
|
|
3603
|
+
this.db.prepare("DELETE FROM agent_memory WHERE identity_id=? AND conversation_id=? AND seq <= ?").run(this.id, input.conversationId, row.seq - this.memoryRetentionK);
|
|
3604
|
+
}
|
|
3605
|
+
return { seq: row.seq, deduped: false };
|
|
3606
|
+
}
|
|
3607
|
+
async memoryContext(conversationId, limit = 50) {
|
|
3608
|
+
const rows = this.db.prepare(`SELECT seq, role, peer_id, sealed_text, recorded_at FROM agent_memory
|
|
3609
|
+
WHERE identity_id=? AND conversation_id=? ORDER BY seq DESC LIMIT ?`).all(this.id, conversationId, limit);
|
|
3610
|
+
const recent = [];
|
|
3611
|
+
for (const r of rows.reverse()) {
|
|
3612
|
+
const text = new TextDecoder().decode(await this.cipher.open(r.sealed_text));
|
|
3613
|
+
recent.push({ seq: r.seq, role: r.role, peerId: r.peer_id ?? void 0, text, ts: r.recorded_at });
|
|
3614
|
+
}
|
|
3615
|
+
const factRows = this.db.prepare("SELECT sealed_fact FROM agent_memory_fact WHERE identity_id=? AND conversation_id=? ORDER BY fact_id ASC").all(this.id, conversationId);
|
|
3616
|
+
const facts = [];
|
|
3617
|
+
for (const f of factRows)
|
|
3618
|
+
facts.push(new TextDecoder().decode(await this.cipher.open(f.sealed_fact)));
|
|
3619
|
+
return { recent, facts };
|
|
3620
|
+
}
|
|
3621
|
+
async recordMemoryFact(conversationId, fact) {
|
|
3622
|
+
const sealed = await this.cipher.seal(new TextEncoder().encode(fact));
|
|
3623
|
+
this.db.prepare(`INSERT INTO agent_memory_fact (identity_id, conversation_id, fact_id, sealed_fact, added_at)
|
|
3624
|
+
VALUES (?, ?, (SELECT COALESCE(MAX(fact_id),0)+1 FROM agent_memory_fact WHERE identity_id=? AND conversation_id=?), ?, ?)`).run(this.id, conversationId, this.id, conversationId, sealed, Date.now());
|
|
3625
|
+
}
|
|
3314
3626
|
/** @internal test-only */
|
|
3315
3627
|
_rawMainForTest(identityId, channelId) {
|
|
3316
3628
|
const row = this.db.prepare("SELECT sealed_main FROM mls_group_state WHERE identity_id=? AND channel_id=?").get(identityId, channelId);
|
|
@@ -3334,7 +3646,7 @@ function loadPackageMeta(importMetaUrl) {
|
|
|
3334
3646
|
|
|
3335
3647
|
// src/bridge-agent.ts
|
|
3336
3648
|
import { promises as fsp } from "node:fs";
|
|
3337
|
-
import { join, dirname } from "node:path";
|
|
3649
|
+
import { join as join5, dirname } from "node:path";
|
|
3338
3650
|
|
|
3339
3651
|
// src/latency.ts
|
|
3340
3652
|
var DEFAULT_CAPACITY = 1024;
|
|
@@ -3608,8 +3920,8 @@ async function prepareAttachment(filePath, caption, channelId, deps) {
|
|
|
3608
3920
|
}
|
|
3609
3921
|
async function materializeAttachment(body, ctx, deps) {
|
|
3610
3922
|
const ciphertext = await deps.downloadBlob(body.blobId);
|
|
3611
|
-
const
|
|
3612
|
-
if (
|
|
3923
|
+
const sha2566 = deps.computeFileDigest(ciphertext);
|
|
3924
|
+
if (sha2566 !== body.digest) {
|
|
3613
3925
|
throw new Error(`attachment digest mismatch for blob ${body.blobId}`);
|
|
3614
3926
|
}
|
|
3615
3927
|
const plain = deps.decryptFile(
|
|
@@ -3622,7 +3934,328 @@ async function materializeAttachment(body, ctx, deps) {
|
|
|
3622
3934
|
const path = `${dir}/${ctx.messageId}-${safeName}`;
|
|
3623
3935
|
await deps.mkdirp(dir);
|
|
3624
3936
|
await deps.writeFile(path, plain);
|
|
3625
|
-
return { filename: safeName, mime: body.mime, size: plain.length, path, sha256:
|
|
3937
|
+
return { filename: safeName, mime: body.mime, size: plain.length, path, sha256: sha2566 };
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
// src/memory/hermes-session-map.ts
|
|
3941
|
+
import { readFileSync } from "node:fs";
|
|
3942
|
+
import { homedir } from "node:os";
|
|
3943
|
+
import { join } from "node:path";
|
|
3944
|
+
function expandHome(p) {
|
|
3945
|
+
return p === "~" || p.startsWith("~/") ? join(homedir(), p.slice(1)) : p;
|
|
3946
|
+
}
|
|
3947
|
+
function resolveHermesSession(baseDir, conversationId) {
|
|
3948
|
+
try {
|
|
3949
|
+
const path = join(expandHome(baseDir), "sessions", "sessions.json");
|
|
3950
|
+
const map = JSON.parse(readFileSync(path, "utf8"));
|
|
3951
|
+
const entry = map[`agent:main:securecoms:dm:${conversationId}`];
|
|
3952
|
+
const sid = entry?.session_id;
|
|
3953
|
+
return typeof sid === "string" && sid ? sid : null;
|
|
3954
|
+
} catch {
|
|
3955
|
+
return null;
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
// src/memory/hermes-store.ts
|
|
3960
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
3961
|
+
import { join as join2 } from "node:path";
|
|
3962
|
+
var HermesStore = class {
|
|
3963
|
+
dbPath;
|
|
3964
|
+
constructor(baseDir) {
|
|
3965
|
+
this.dbPath = join2(expandHome(baseDir), "state.db");
|
|
3966
|
+
}
|
|
3967
|
+
/** Recent-K conversational turns for a session, chronological. Fail-safe → []. */
|
|
3968
|
+
readRecent(sessionId, limit) {
|
|
3969
|
+
let db;
|
|
3970
|
+
try {
|
|
3971
|
+
db = new DatabaseSync2(this.dbPath, { readOnly: true });
|
|
3972
|
+
const rows = db.prepare(
|
|
3973
|
+
`SELECT id, role, content, timestamp FROM messages
|
|
3974
|
+
WHERE session_id = ? AND active = 1 AND content IS NOT NULL AND role IN ('user','assistant')
|
|
3975
|
+
ORDER BY timestamp DESC LIMIT ?`
|
|
3976
|
+
).all(sessionId, limit);
|
|
3977
|
+
return rows.reverse().map((r) => ({ id: r.id, role: r.role, content: r.content, timestamp: r.timestamp }));
|
|
3978
|
+
} catch {
|
|
3979
|
+
return [];
|
|
3980
|
+
} finally {
|
|
3981
|
+
db?.close();
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
open(busyMs = 5e3) {
|
|
3985
|
+
const db = new DatabaseSync2(this.dbPath);
|
|
3986
|
+
db.exec(`PRAGMA busy_timeout = ${busyMs}`);
|
|
3987
|
+
return db;
|
|
3988
|
+
}
|
|
3989
|
+
/**
|
|
3990
|
+
* Append a voice turn to a session in one synchronous BEGIN IMMEDIATE block
|
|
3991
|
+
* (no await between statements → atomic against Hermes and our own retries).
|
|
3992
|
+
* Guards: skip when the session is absent/archived/ended; in-transaction dedup
|
|
3993
|
+
* on (session_id, platform_message_id). Writes only `messages`. Fail-safe.
|
|
3994
|
+
*/
|
|
3995
|
+
writeVoiceTurn(input) {
|
|
3996
|
+
let db;
|
|
3997
|
+
try {
|
|
3998
|
+
db = this.open();
|
|
3999
|
+
db.exec("BEGIN IMMEDIATE");
|
|
4000
|
+
try {
|
|
4001
|
+
const s = db.prepare("SELECT archived, ended_at FROM sessions WHERE id = ?").get(input.sessionId);
|
|
4002
|
+
if (!s || s.archived === 1 || s.ended_at != null) {
|
|
4003
|
+
db.exec("ROLLBACK");
|
|
4004
|
+
return "skipped";
|
|
4005
|
+
}
|
|
4006
|
+
if (input.dedupKey) {
|
|
4007
|
+
const dup = db.prepare("SELECT 1 FROM messages WHERE session_id = ? AND platform_message_id = ?").get(input.sessionId, input.dedupKey);
|
|
4008
|
+
if (dup) {
|
|
4009
|
+
db.exec("ROLLBACK");
|
|
4010
|
+
return "deduped";
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
db.prepare(
|
|
4014
|
+
`INSERT INTO messages (session_id, role, content, timestamp, active, platform_message_id) VALUES (?, ?, ?, ?, 1, ?)`
|
|
4015
|
+
).run(input.sessionId, input.role, input.content, input.timestamp, input.dedupKey);
|
|
4016
|
+
db.exec("COMMIT");
|
|
4017
|
+
return "written";
|
|
4018
|
+
} catch (e) {
|
|
4019
|
+
try {
|
|
4020
|
+
db.exec("ROLLBACK");
|
|
4021
|
+
} catch {
|
|
4022
|
+
}
|
|
4023
|
+
throw e;
|
|
4024
|
+
}
|
|
4025
|
+
} catch {
|
|
4026
|
+
return "failed";
|
|
4027
|
+
} finally {
|
|
4028
|
+
db?.close();
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
/**
|
|
4032
|
+
* Startup write-capability probe: insert a sentinel against any existing
|
|
4033
|
+
* session inside a rolled-back transaction. Throws if the write path is broken
|
|
4034
|
+
* (e.g. a schema regression). No-op when the db has no sessions.
|
|
4035
|
+
*/
|
|
4036
|
+
probeWrite() {
|
|
4037
|
+
let db;
|
|
4038
|
+
try {
|
|
4039
|
+
db = this.open(500);
|
|
4040
|
+
const s = db.prepare("SELECT id FROM sessions LIMIT 1").get();
|
|
4041
|
+
if (!s) return;
|
|
4042
|
+
db.exec("BEGIN IMMEDIATE");
|
|
4043
|
+
try {
|
|
4044
|
+
db.prepare(
|
|
4045
|
+
`INSERT INTO messages (session_id, role, content, timestamp, active, platform_message_id) VALUES (?, 'assistant', '[voice] __probe__', ?, 1, ?)`
|
|
4046
|
+
).run(s.id, Date.now() / 1e3, `__probe__${Date.now()}`);
|
|
4047
|
+
} finally {
|
|
4048
|
+
db.exec("ROLLBACK");
|
|
4049
|
+
}
|
|
4050
|
+
} finally {
|
|
4051
|
+
db?.close();
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
};
|
|
4055
|
+
|
|
4056
|
+
// src/memory/hermes-provider.ts
|
|
4057
|
+
var DEFAULT_K = 30;
|
|
4058
|
+
var HermesMemoryProvider = class {
|
|
4059
|
+
constructor(baseDir) {
|
|
4060
|
+
this.baseDir = baseDir;
|
|
4061
|
+
this.store = new HermesStore(baseDir);
|
|
4062
|
+
try {
|
|
4063
|
+
this.store.probeWrite();
|
|
4064
|
+
} catch (e) {
|
|
4065
|
+
process.stderr.write(`[bridge] HERMES MEMORY WRITE-BACK NON-FUNCTIONAL (schema probe failed): ${String(e)}
|
|
4066
|
+
`);
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
store;
|
|
4070
|
+
noSession = 0;
|
|
4071
|
+
skipped = 0;
|
|
4072
|
+
writeFailed = 0;
|
|
4073
|
+
async record(input) {
|
|
4074
|
+
if (input.modality !== "voice") return { seq: 0, deduped: true };
|
|
4075
|
+
const sessionId = resolveHermesSession(this.baseDir, input.conversationId);
|
|
4076
|
+
if (!sessionId) {
|
|
4077
|
+
this.noSession++;
|
|
4078
|
+
return { seq: 0, deduped: false };
|
|
4079
|
+
}
|
|
4080
|
+
const role = input.role === "peer" ? "user" : "assistant";
|
|
4081
|
+
const res = this.store.writeVoiceTurn({
|
|
4082
|
+
sessionId,
|
|
4083
|
+
role,
|
|
4084
|
+
content: `[voice] ${input.text}`,
|
|
4085
|
+
dedupKey: input.idempotencyKey ?? input.messageId ?? null,
|
|
4086
|
+
timestamp: Date.now() / 1e3
|
|
4087
|
+
});
|
|
4088
|
+
if (res === "failed") this.writeFailed++;
|
|
4089
|
+
else if (res === "skipped") this.skipped++;
|
|
4090
|
+
return { seq: 0, deduped: res === "deduped" };
|
|
4091
|
+
}
|
|
4092
|
+
async context(conversationId, limit) {
|
|
4093
|
+
const sessionId = resolveHermesSession(this.baseDir, conversationId);
|
|
4094
|
+
if (!sessionId) return { recent: [], facts: [] };
|
|
4095
|
+
const turns = this.store.readRecent(sessionId, limit ?? DEFAULT_K);
|
|
4096
|
+
return {
|
|
4097
|
+
recent: turns.map((t) => ({ seq: t.id, role: t.role === "user" ? "peer" : "agent", text: t.content })),
|
|
4098
|
+
facts: []
|
|
4099
|
+
};
|
|
4100
|
+
}
|
|
4101
|
+
stats() {
|
|
4102
|
+
return { hermes_no_session: this.noSession, hermes_skipped: this.skipped, hermes_write_failed: this.writeFailed };
|
|
4103
|
+
}
|
|
4104
|
+
};
|
|
4105
|
+
|
|
4106
|
+
// src/memory/openclaw-session-map.ts
|
|
4107
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
4108
|
+
import { join as join3 } from "node:path";
|
|
4109
|
+
function resolveOwnerDmSession(baseDir, agent) {
|
|
4110
|
+
try {
|
|
4111
|
+
const path = join3(expandHome(baseDir), "agents", agent, "sessions", "sessions.json");
|
|
4112
|
+
const map = JSON.parse(readFileSync2(path, "utf8"));
|
|
4113
|
+
let best = null;
|
|
4114
|
+
for (const [key, entry] of Object.entries(map)) {
|
|
4115
|
+
if (!key.includes(":direct:") || !key.endsWith(":owner")) continue;
|
|
4116
|
+
const sid = entry?.sessionId;
|
|
4117
|
+
if (typeof sid !== "string" || !sid) continue;
|
|
4118
|
+
const updatedAt = typeof entry?.updatedAt === "number" ? entry.updatedAt : 0;
|
|
4119
|
+
if (!best || updatedAt > best.updatedAt) best = { sid, updatedAt };
|
|
4120
|
+
}
|
|
4121
|
+
return best?.sid ?? null;
|
|
4122
|
+
} catch {
|
|
4123
|
+
return null;
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
// src/memory/openclaw-session-log.ts
|
|
4128
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
4129
|
+
import { join as join4 } from "node:path";
|
|
4130
|
+
function readRecentTurns(baseDir, agent, sessionId, k) {
|
|
4131
|
+
let raw;
|
|
4132
|
+
try {
|
|
4133
|
+
raw = readFileSync3(join4(expandHome(baseDir), "agents", agent, "sessions", `${sessionId}.jsonl`), "utf8");
|
|
4134
|
+
} catch {
|
|
4135
|
+
return [];
|
|
4136
|
+
}
|
|
4137
|
+
const turns = [];
|
|
4138
|
+
for (const line of raw.split("\n")) {
|
|
4139
|
+
if (!line.trim()) continue;
|
|
4140
|
+
let o;
|
|
4141
|
+
try {
|
|
4142
|
+
o = JSON.parse(line);
|
|
4143
|
+
} catch {
|
|
4144
|
+
continue;
|
|
4145
|
+
}
|
|
4146
|
+
if (o?.type !== "message" || !o.message) continue;
|
|
4147
|
+
const ts = typeof o.timestamp === "string" ? Date.parse(o.timestamp) : NaN;
|
|
4148
|
+
if (Number.isNaN(ts)) continue;
|
|
4149
|
+
const role = o.message.role;
|
|
4150
|
+
if (role === "user") {
|
|
4151
|
+
const text = typeof o.message.content === "string" ? o.message.content.trim() : "";
|
|
4152
|
+
if (text) turns.push({ role: "peer", text, ts });
|
|
4153
|
+
} else if (role === "assistant") {
|
|
4154
|
+
const blocks = Array.isArray(o.message.content) ? o.message.content : [];
|
|
4155
|
+
const text = blocks.filter((b) => !!b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
|
|
4156
|
+
if (text) turns.push({ role: "agent", text, ts });
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
return k > 0 ? turns.slice(-k) : [];
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
// src/memory/openclaw-provider.ts
|
|
4163
|
+
var DEFAULT_K2 = 200;
|
|
4164
|
+
var OpenClawMemoryProvider = class {
|
|
4165
|
+
constructor(store, baseDir, agent) {
|
|
4166
|
+
this.store = store;
|
|
4167
|
+
this.baseDir = baseDir;
|
|
4168
|
+
this.agent = agent;
|
|
4169
|
+
}
|
|
4170
|
+
noSession = 0;
|
|
4171
|
+
readFailed = 0;
|
|
4172
|
+
async record(input) {
|
|
4173
|
+
if (input.modality !== "voice") return { seq: 0, deduped: true };
|
|
4174
|
+
return this.store.recordMemory({ ...input, text: `[voice] ${input.text}` });
|
|
4175
|
+
}
|
|
4176
|
+
async context(conversationId, limit) {
|
|
4177
|
+
const k = limit ?? DEFAULT_K2;
|
|
4178
|
+
const voice = await this.store.memoryContext(conversationId, k);
|
|
4179
|
+
let textTurns = [];
|
|
4180
|
+
try {
|
|
4181
|
+
const sid = resolveOwnerDmSession(this.baseDir, this.agent);
|
|
4182
|
+
if (!sid) {
|
|
4183
|
+
this.noSession++;
|
|
4184
|
+
return voice;
|
|
4185
|
+
}
|
|
4186
|
+
textTurns = readRecentTurns(this.baseDir, this.agent, sid, k).map((t) => ({
|
|
4187
|
+
seq: 0,
|
|
4188
|
+
role: t.role,
|
|
4189
|
+
text: t.text,
|
|
4190
|
+
ts: t.ts
|
|
4191
|
+
}));
|
|
4192
|
+
} catch {
|
|
4193
|
+
this.readFailed++;
|
|
4194
|
+
return voice;
|
|
4195
|
+
}
|
|
4196
|
+
const tagged = [
|
|
4197
|
+
...textTurns.map((t, i) => ({ ...t, _src: 0, _i: i })),
|
|
4198
|
+
...voice.recent.map((t, i) => ({ ...t, _src: 1, _i: i }))
|
|
4199
|
+
];
|
|
4200
|
+
tagged.sort((a, b) => {
|
|
4201
|
+
const at = a.ts ?? 0, bt = b.ts ?? 0;
|
|
4202
|
+
if (at !== bt) return at - bt;
|
|
4203
|
+
if (a._src !== b._src) return a._src - b._src;
|
|
4204
|
+
return a._i - b._i;
|
|
4205
|
+
});
|
|
4206
|
+
const recent = (k > 0 ? tagged.slice(-k) : []).map((t, i) => ({
|
|
4207
|
+
seq: i + 1,
|
|
4208
|
+
role: t.role,
|
|
4209
|
+
peerId: t.peerId,
|
|
4210
|
+
text: t.text,
|
|
4211
|
+
ts: t.ts
|
|
4212
|
+
}));
|
|
4213
|
+
return { recent, facts: voice.facts };
|
|
4214
|
+
}
|
|
4215
|
+
stats() {
|
|
4216
|
+
return { openclaw_no_session: this.noSession, openclaw_read_failed: this.readFailed };
|
|
4217
|
+
}
|
|
4218
|
+
};
|
|
4219
|
+
|
|
4220
|
+
// src/memory/provider.ts
|
|
4221
|
+
var SecureComsLocalMemory = class {
|
|
4222
|
+
constructor(store) {
|
|
4223
|
+
this.store = store;
|
|
4224
|
+
}
|
|
4225
|
+
record(input) {
|
|
4226
|
+
return this.store.recordMemory(input);
|
|
4227
|
+
}
|
|
4228
|
+
context(conversationId, limit) {
|
|
4229
|
+
return this.store.memoryContext(conversationId, limit);
|
|
4230
|
+
}
|
|
4231
|
+
};
|
|
4232
|
+
var NullMemory = class {
|
|
4233
|
+
async record() {
|
|
4234
|
+
return { seq: 0, deduped: false };
|
|
4235
|
+
}
|
|
4236
|
+
async context() {
|
|
4237
|
+
return { recent: [], facts: [] };
|
|
4238
|
+
}
|
|
4239
|
+
};
|
|
4240
|
+
function createMemoryProvider(provider, store, baseDir, agent) {
|
|
4241
|
+
if (provider === "none") return new NullMemory();
|
|
4242
|
+
if (provider === "hermes") return new HermesMemoryProvider(baseDir ?? "~/.hermes");
|
|
4243
|
+
if (provider === "openclaw") return new OpenClawMemoryProvider(store, baseDir ?? "~/.openclaw", agent ?? "");
|
|
4244
|
+
return new SecureComsLocalMemory(store);
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
// src/memory-taps.ts
|
|
4248
|
+
async function recordInboundMemory(sink, channelId, senderId, text, messageId) {
|
|
4249
|
+
try {
|
|
4250
|
+
await sink.record({ conversationId: channelId, role: "peer", peerId: senderId, text, modality: "text", messageId });
|
|
4251
|
+
} catch {
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
async function recordOutboundMemory(sink, channelId, text, messageId) {
|
|
4255
|
+
try {
|
|
4256
|
+
await sink.record({ conversationId: channelId, role: "agent", text, modality: "text", messageId });
|
|
4257
|
+
} catch {
|
|
4258
|
+
}
|
|
3626
4259
|
}
|
|
3627
4260
|
|
|
3628
4261
|
// src/bridge-agent.ts
|
|
@@ -3699,6 +4332,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
3699
4332
|
identityId: cfg.identityId,
|
|
3700
4333
|
cipherFor: () => new KeyedInboxCipher(deriveMlsStoreKey(seed))
|
|
3701
4334
|
});
|
|
4335
|
+
this.memory = createMemoryProvider(this.cfg.memoryProvider, this.store, this.cfg.memoryBaseDir, this.cfg.memoryAgent);
|
|
3702
4336
|
this.provider = new MlsGroupKeyProvider({
|
|
3703
4337
|
store: this.store,
|
|
3704
4338
|
backend: this.backend,
|
|
@@ -3778,7 +4412,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
3778
4412
|
const j = await res.json();
|
|
3779
4413
|
return String(j.attachment_id);
|
|
3780
4414
|
};
|
|
3781
|
-
this.attachmentsDirImpl = deps.attachmentsDir ??
|
|
4415
|
+
this.attachmentsDirImpl = deps.attachmentsDir ?? join5(dirname(cfg.storePath), "attachments");
|
|
3782
4416
|
this.action = {
|
|
3783
4417
|
begin: (channelId) => this.actionBegin(channelId),
|
|
3784
4418
|
start: (opts) => this.actionStart(opts),
|
|
@@ -3950,6 +4584,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
3950
4584
|
listeners = /* @__PURE__ */ new Set();
|
|
3951
4585
|
startedAt = 0;
|
|
3952
4586
|
store;
|
|
4587
|
+
memory;
|
|
4588
|
+
/** Task 2: warn-once-per-channel guard for the enclave-map fallback (below). */
|
|
4589
|
+
enclaveFallbackWarned = /* @__PURE__ */ new Set();
|
|
4590
|
+
/** Task 2: dedupe guard so ingestOnce writes a channel's enclave at most once
|
|
4591
|
+
* per process — a channel's enclave is immutable, so no need to UPSERT on
|
|
4592
|
+
* every message on the hot receive path (audit M2). */
|
|
4593
|
+
channelEnclaveRecorded = /* @__PURE__ */ new Set();
|
|
3953
4594
|
latency;
|
|
3954
4595
|
/** Track 5: injectable blob download fn (default: fetch-based). */
|
|
3955
4596
|
downloadBlobImpl;
|
|
@@ -3989,6 +4630,26 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
3989
4630
|
sweepJitter() {
|
|
3990
4631
|
return Math.floor(this.rng() * _BridgeAgent.SELF_HEAL_SWEEP_JITTER_MS);
|
|
3991
4632
|
}
|
|
4633
|
+
/**
|
|
4634
|
+
* Task 2 (multi-enclave core fix): resolve the enclave to stamp on outbound
|
|
4635
|
+
* traffic for `channelId`. The daemon previously stamped `cfg.enclaveId`
|
|
4636
|
+
* (its "home" enclave) on EVERY channel, so an agent admitted to a second
|
|
4637
|
+
* enclave got a backend 400 on every send/reply there. The channel→enclave
|
|
4638
|
+
* map (`channel_meta`, Task 1) is fed by inbound envelopes (`ingestOnce`)
|
|
4639
|
+
* and by `listChannels`; a channel not yet in the map (never seen inbound,
|
|
4640
|
+
* never discovered) falls back to home and logs once so the fallback is
|
|
4641
|
+
* diagnosable (audit H3) rather than silently wrong.
|
|
4642
|
+
*/
|
|
4643
|
+
resolveEnclave(channelId) {
|
|
4644
|
+
const mapped = this.store.getChannelMeta(channelId)?.enclaveId;
|
|
4645
|
+
if (mapped) return mapped;
|
|
4646
|
+
if (!this.enclaveFallbackWarned.has(channelId)) {
|
|
4647
|
+
this.enclaveFallbackWarned.add(channelId);
|
|
4648
|
+
process.stderr.write(`[bridge] send.enclave_fallback channel=${channelId} stamped=home
|
|
4649
|
+
`);
|
|
4650
|
+
}
|
|
4651
|
+
return this.cfg.enclaveId;
|
|
4652
|
+
}
|
|
3992
4653
|
async reconcileJoin(channelId) {
|
|
3993
4654
|
if (this.joinState.get(channelId) === "JOINED") return true;
|
|
3994
4655
|
try {
|
|
@@ -4202,7 +4863,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4202
4863
|
groupKey: this.provider,
|
|
4203
4864
|
messageId: id,
|
|
4204
4865
|
channelId,
|
|
4205
|
-
enclaveId: this.
|
|
4866
|
+
enclaveId: this.resolveEnclave(channelId),
|
|
4206
4867
|
senderId: this.cfg.identityId,
|
|
4207
4868
|
senderType: "agent",
|
|
4208
4869
|
type: "text",
|
|
@@ -4220,6 +4881,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4220
4881
|
(envelope) => this.latency.time("send.publish", () => this.transport.publish(envelope))
|
|
4221
4882
|
);
|
|
4222
4883
|
this.latency.record("send.total", performance.now() - sendStart);
|
|
4884
|
+
void recordOutboundMemory(this.memory, channelId, text, id);
|
|
4223
4885
|
return { message_id: id, status: "sent" };
|
|
4224
4886
|
}
|
|
4225
4887
|
async sendA2A(channelId, a2a, messageId) {
|
|
@@ -4238,7 +4900,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4238
4900
|
groupKey: this.provider,
|
|
4239
4901
|
messageId: id,
|
|
4240
4902
|
channelId,
|
|
4241
|
-
enclaveId: this.
|
|
4903
|
+
enclaveId: this.resolveEnclave(channelId),
|
|
4242
4904
|
senderId: this.cfg.identityId,
|
|
4243
4905
|
senderType: "agent",
|
|
4244
4906
|
type: "a2a",
|
|
@@ -4291,7 +4953,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4291
4953
|
groupKey: this.provider,
|
|
4292
4954
|
messageId: id,
|
|
4293
4955
|
channelId,
|
|
4294
|
-
enclaveId: this.
|
|
4956
|
+
enclaveId: this.resolveEnclave(channelId),
|
|
4295
4957
|
senderId: this.cfg.identityId,
|
|
4296
4958
|
senderType: "agent",
|
|
4297
4959
|
type: "artifact_share",
|
|
@@ -4311,42 +4973,91 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4311
4973
|
this.latency.record("send.total", performance.now() - sendStart);
|
|
4312
4974
|
return { message_id: id, status: "sent" };
|
|
4313
4975
|
}
|
|
4314
|
-
|
|
4315
|
-
|
|
4976
|
+
/** Task 3: delegates to the configured MemoryProvider (SecureComsLocal | Null). */
|
|
4977
|
+
async recordMemory(input) {
|
|
4978
|
+
return this.memory.record(input);
|
|
4979
|
+
}
|
|
4980
|
+
async memoryContext(conversationId, limit) {
|
|
4981
|
+
return this.memory.context(conversationId, limit);
|
|
4982
|
+
}
|
|
4983
|
+
/**
|
|
4984
|
+
* List channels in one enclave (default: the daemon's own home enclave).
|
|
4985
|
+
* Task 3: called per-*known* enclave by refreshChannelMeta so a
|
|
4986
|
+
* cross-enclave room's is_dm gets resolved — see the scope caveat there
|
|
4987
|
+
* (same-tenant only; a cross-tenant enclave 404s under the agent's bearer).
|
|
4988
|
+
*/
|
|
4989
|
+
async listChannels(enclaveId = this.cfg.enclaveId) {
|
|
4990
|
+
const url = `${this.cfg.apiBase}/api/v1/enclaves/${enclaveId}/channels?caller_identity_id=${encodeURIComponent(this.cfg.identityId)}`;
|
|
4316
4991
|
const res = await this.fetchImpl(url, {
|
|
4317
4992
|
headers: { authorization: `Bearer ${this.cfg.apiKey}` }
|
|
4318
4993
|
});
|
|
4319
4994
|
if (!res.ok) throw new Error(`list_channels failed: ${res.status}`);
|
|
4320
4995
|
const rows = await res.json();
|
|
4996
|
+
for (const r of rows) this.store.recordChannelEnclave(r.channel_id, r.enclave_id);
|
|
4321
4997
|
return rows.map((r) => ({
|
|
4322
4998
|
channel_id: r.channel_id,
|
|
4323
4999
|
name: r.name ?? null,
|
|
4324
5000
|
is_dm: r.is_dm,
|
|
4325
|
-
joined: true
|
|
5001
|
+
joined: true,
|
|
5002
|
+
enclave_id: r.enclave_id
|
|
4326
5003
|
}));
|
|
4327
5004
|
}
|
|
4328
|
-
/**
|
|
5005
|
+
/**
|
|
5006
|
+
* A live channelMeta entry, or undefined if missing OR expired past the
|
|
5007
|
+
* TTL. A never-cached-this-process channel (no in-memory entry at all —
|
|
5008
|
+
* cold start, or every listChannels() call in refreshChannelMeta failed,
|
|
5009
|
+
* e.g. network down) falls back to the store's persisted is_dm (Task 3) so
|
|
5010
|
+
* a cross-enclave room stays classified across a restart instead of
|
|
5011
|
+
* reverting to UNKNOWN/fail-open. The store has no TTL (is_dm is immutable
|
|
5012
|
+
* per channel); `name` is not persisted so it comes back undefined on this
|
|
5013
|
+
* path and re-resolves once a listChannels() call succeeds.
|
|
5014
|
+
*
|
|
5015
|
+
* A TTL-*expired* in-memory entry deliberately does NOT use the store
|
|
5016
|
+
* fallback and is still treated as a miss — that miss is what drives
|
|
5017
|
+
* ingestOnce to retry a refresh each cycle (S-2: keeping `name` fresh after
|
|
5018
|
+
* a rename); serving the persisted is_dm there would silently suppress
|
|
5019
|
+
* that retry forever.
|
|
5020
|
+
*/
|
|
4329
5021
|
freshMeta(channelId) {
|
|
4330
5022
|
const hit = this.channelMeta.get(channelId);
|
|
4331
|
-
if (
|
|
4332
|
-
|
|
4333
|
-
|
|
5023
|
+
if (hit) {
|
|
5024
|
+
if (Date.now() - hit.at < CHANNEL_META_TTL_MS) return { is_dm: hit.is_dm, name: hit.name };
|
|
5025
|
+
return void 0;
|
|
5026
|
+
}
|
|
5027
|
+
const persisted = this.store.getChannelMeta(channelId);
|
|
5028
|
+
if (persisted?.isDm !== void 0) {
|
|
5029
|
+
return { is_dm: persisted.isDm, name: void 0 };
|
|
5030
|
+
}
|
|
5031
|
+
return void 0;
|
|
4334
5032
|
}
|
|
4335
5033
|
/**
|
|
4336
|
-
*
|
|
4337
|
-
*
|
|
4338
|
-
*
|
|
4339
|
-
*
|
|
4340
|
-
*
|
|
5034
|
+
* Whole-listing refresh of channelMeta, one listChannels() call per *known*
|
|
5035
|
+
* enclave — the daemon's own home enclave plus every enclave it has learned
|
|
5036
|
+
* about from inbound envelopes or a prior discovery (store.distinctEnclaveIds()).
|
|
5037
|
+
* Called AT MOST ONCE per ingest cycle (the caller guards with a
|
|
5038
|
+
* cycle-local "refreshed" flag). Task 3 (M1): without this, a cross-enclave
|
|
5039
|
+
* room is never listed under the home-enclave-only call, so its is_dm never
|
|
5040
|
+
* resolves and the @mention filter fails open for that room.
|
|
5041
|
+
*
|
|
5042
|
+
* Each enclave is wrapped in its own try/catch (scope caveat — audit
|
|
5043
|
+
* H2/tenant): list_channels_in_enclave filters on tenant_id, so a
|
|
5044
|
+
* same-tenant enclave the agent has learned about resolves fine, but a
|
|
5045
|
+
* cross-TENANT federated-partner enclave 404s under this agent's bearer;
|
|
5046
|
+
* that enclave's channels are left unresolved (fail open) without aborting
|
|
5047
|
+
* the other enclaves in the set.
|
|
4341
5048
|
*/
|
|
4342
5049
|
async refreshChannelMeta() {
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
5050
|
+
const enclaves = /* @__PURE__ */ new Set([this.cfg.enclaveId, ...this.store.distinctEnclaveIds()]);
|
|
5051
|
+
const now = Date.now();
|
|
5052
|
+
for (const encId of enclaves) {
|
|
5053
|
+
try {
|
|
5054
|
+
const channels = await this.listChannels(encId);
|
|
5055
|
+
for (const c of channels) {
|
|
5056
|
+
this.channelMeta.set(c.channel_id, { is_dm: c.is_dm, name: c.name, at: now });
|
|
5057
|
+
this.store.recordChannelIsDm(c.channel_id, c.is_dm);
|
|
5058
|
+
}
|
|
5059
|
+
} catch {
|
|
4348
5060
|
}
|
|
4349
|
-
} catch {
|
|
4350
5061
|
}
|
|
4351
5062
|
}
|
|
4352
5063
|
/** A live roster for a channel, or undefined if missing OR expired past the TTL. */
|
|
@@ -4452,6 +5163,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4452
5163
|
async reportStatus(channelId, state, detail) {
|
|
4453
5164
|
const joined = await this.reconcileJoin(channelId);
|
|
4454
5165
|
if (!joined) throw new ChannelPendingError(channelId);
|
|
5166
|
+
const enc = this.store.getChannelMeta(channelId)?.enclaveId;
|
|
5167
|
+
if (!enc) return;
|
|
4455
5168
|
await sendWithSelfHeal(
|
|
4456
5169
|
this.provider,
|
|
4457
5170
|
channelId,
|
|
@@ -4460,7 +5173,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4460
5173
|
groupKey: this.provider,
|
|
4461
5174
|
messageId: crypto.randomUUID(),
|
|
4462
5175
|
channelId,
|
|
4463
|
-
enclaveId:
|
|
5176
|
+
enclaveId: enc,
|
|
4464
5177
|
senderId: this.cfg.identityId,
|
|
4465
5178
|
senderType: "agent",
|
|
4466
5179
|
type: "system_event",
|
|
@@ -4507,7 +5220,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4507
5220
|
groupKey: this.provider,
|
|
4508
5221
|
messageId: crypto.randomUUID(),
|
|
4509
5222
|
channelId,
|
|
4510
|
-
enclaveId: this.
|
|
5223
|
+
enclaveId: this.resolveEnclave(channelId),
|
|
4511
5224
|
senderId: this.cfg.identityId,
|
|
4512
5225
|
senderType: "agent",
|
|
4513
5226
|
type: "system_event",
|
|
@@ -4751,6 +5464,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4751
5464
|
const acked = [];
|
|
4752
5465
|
for (const m of entries) {
|
|
4753
5466
|
try {
|
|
5467
|
+
const encId = m.envelope.enclave_id;
|
|
5468
|
+
if (encId && !this.channelEnclaveRecorded.has(m.channelId)) {
|
|
5469
|
+
this.channelEnclaveRecorded.add(m.channelId);
|
|
5470
|
+
this.store.recordChannelEnclave(m.channelId, encId);
|
|
5471
|
+
}
|
|
4754
5472
|
const parsed = JSON.parse(
|
|
4755
5473
|
new TextDecoder().decode(m.plaintext)
|
|
4756
5474
|
);
|
|
@@ -4822,6 +5540,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4822
5540
|
sender_is_agent: senderIsAgent
|
|
4823
5541
|
});
|
|
4824
5542
|
this.capBuffer();
|
|
5543
|
+
void recordInboundMemory(this.memory, m.channelId, m.envelope.sender_id, parsed.text, m.messageId);
|
|
4825
5544
|
}
|
|
4826
5545
|
}
|
|
4827
5546
|
acked.push(m.queueId);
|
|
@@ -4918,7 +5637,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4918
5637
|
failed_attachments: this.failedAttachments,
|
|
4919
5638
|
delivery_pings: this.pingsReceived,
|
|
4920
5639
|
uptime_s: this.startedAt ? Math.floor((Date.now() - this.startedAt) / 1e3) : 0,
|
|
4921
|
-
version: BRIDGE_VERSION
|
|
5640
|
+
version: BRIDGE_VERSION,
|
|
5641
|
+
...this.memory.stats?.() ?? {}
|
|
4922
5642
|
};
|
|
4923
5643
|
}
|
|
4924
5644
|
/**
|
|
@@ -4973,11 +5693,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4973
5693
|
};
|
|
4974
5694
|
|
|
4975
5695
|
// src/config.ts
|
|
4976
|
-
import { readFileSync, statSync } from "node:fs";
|
|
4977
|
-
import { dirname as dirname2, join as
|
|
5696
|
+
import { readFileSync as readFileSync4, statSync } from "node:fs";
|
|
5697
|
+
import { dirname as dirname2, join as join6 } from "node:path";
|
|
4978
5698
|
var REQUIRED = ["api_key", "identity_id", "device_id", "signing_private_key", "enclave_id", "api_base"];
|
|
4979
5699
|
function loadConfig(opts) {
|
|
4980
|
-
const raw =
|
|
5700
|
+
const raw = readFileSync4(opts.credentialPath, "utf8");
|
|
4981
5701
|
try {
|
|
4982
5702
|
const mode = statSync(opts.credentialPath).mode & 511;
|
|
4983
5703
|
if (mode & 63) process.stderr.write(`[bridge] WARNING: credential file ${opts.credentialPath} permission ${mode.toString(8)} is not 0600 \u2014 tighten it (chmod 600).
|
|
@@ -4991,10 +5711,30 @@ function loadConfig(opts) {
|
|
|
4991
5711
|
throw new Error("credential file is not valid JSON");
|
|
4992
5712
|
}
|
|
4993
5713
|
for (const k of REQUIRED) if (typeof json2[k] !== "string" || !json2[k]) throw new Error(`credential file missing required field: ${k}`);
|
|
4994
|
-
const storePath = opts.storePath ??
|
|
5714
|
+
const storePath = opts.storePath ?? join6(dirname2(opts.credentialPath), "bridge-state.db");
|
|
4995
5715
|
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;
|
|
4996
5716
|
const rateLimitCapacity = typeof json2.rate_limit_capacity === "number" ? json2.rate_limit_capacity : 20;
|
|
4997
5717
|
const rateLimitRefillPerMinute = typeof json2.rate_limit_refill_per_minute === "number" ? json2.rate_limit_refill_per_minute : 20;
|
|
5718
|
+
const memProvRaw = json2.memory?.provider;
|
|
5719
|
+
let memoryProvider;
|
|
5720
|
+
let memoryBaseDir;
|
|
5721
|
+
let memoryAgent;
|
|
5722
|
+
if (memProvRaw === "none") {
|
|
5723
|
+
memoryProvider = "none";
|
|
5724
|
+
} else if (memProvRaw === "hermes") {
|
|
5725
|
+
memoryProvider = "hermes";
|
|
5726
|
+
const bd = json2.memory?.base_dir;
|
|
5727
|
+
memoryBaseDir = typeof bd === "string" && bd ? bd : "~/.hermes";
|
|
5728
|
+
} else if (memProvRaw === "openclaw") {
|
|
5729
|
+
memoryProvider = "openclaw";
|
|
5730
|
+
const oc = json2.memory?.openclaw;
|
|
5731
|
+
const bd = oc?.base_dir;
|
|
5732
|
+
memoryBaseDir = typeof bd === "string" && bd ? bd : "~/.openclaw";
|
|
5733
|
+
const ag = oc?.agent;
|
|
5734
|
+
memoryAgent = typeof ag === "string" && ag ? ag : void 0;
|
|
5735
|
+
} else {
|
|
5736
|
+
memoryProvider = "securecoms";
|
|
5737
|
+
}
|
|
4998
5738
|
return {
|
|
4999
5739
|
apiKey: json2.api_key,
|
|
5000
5740
|
identityId: json2.identity_id,
|
|
@@ -5005,7 +5745,10 @@ function loadConfig(opts) {
|
|
|
5005
5745
|
storePath,
|
|
5006
5746
|
agentName,
|
|
5007
5747
|
rateLimitCapacity,
|
|
5008
|
-
rateLimitRefillPerMinute
|
|
5748
|
+
rateLimitRefillPerMinute,
|
|
5749
|
+
memoryProvider,
|
|
5750
|
+
memoryBaseDir,
|
|
5751
|
+
memoryAgent
|
|
5009
5752
|
};
|
|
5010
5753
|
}
|
|
5011
5754
|
function arg(argv, flag) {
|
|
@@ -5049,11 +5792,11 @@ import { z } from "zod";
|
|
|
5049
5792
|
|
|
5050
5793
|
// src/auth.ts
|
|
5051
5794
|
import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
|
|
5052
|
-
import { existsSync, readFileSync as
|
|
5795
|
+
import { existsSync, readFileSync as readFileSync5, writeFileSync } from "node:fs";
|
|
5053
5796
|
function resolveToken(opts) {
|
|
5054
5797
|
if (opts.token) return opts.token;
|
|
5055
5798
|
const f = opts.tokenFile;
|
|
5056
|
-
if (f && existsSync(f)) return
|
|
5799
|
+
if (f && existsSync(f)) return readFileSync5(f, "utf8").trim();
|
|
5057
5800
|
const tok = randomBytes3(32).toString("base64url");
|
|
5058
5801
|
if (f) writeFileSync(f, tok, { mode: 384 });
|
|
5059
5802
|
return tok;
|
|
@@ -5163,6 +5906,17 @@ var statusSchema = z.object({
|
|
|
5163
5906
|
state: z.enum(["idle", "working", "stalled", "error"]),
|
|
5164
5907
|
detail: z.string().optional()
|
|
5165
5908
|
});
|
|
5909
|
+
var memoryRecordSchema = z.object({
|
|
5910
|
+
conversation_id: z.string().min(1),
|
|
5911
|
+
role: z.enum(["peer", "agent"]),
|
|
5912
|
+
peer_id: z.string().optional(),
|
|
5913
|
+
text: z.string(),
|
|
5914
|
+
modality: z.enum(["text", "voice"]),
|
|
5915
|
+
message_id: z.string().optional(),
|
|
5916
|
+
idempotency_key: z.string().optional()
|
|
5917
|
+
}).refine((d) => Boolean(d.message_id || d.idempotency_key), {
|
|
5918
|
+
message: "memory record requires message_id or idempotency_key"
|
|
5919
|
+
});
|
|
5166
5920
|
function json(res, status, body) {
|
|
5167
5921
|
const s = JSON.stringify(body);
|
|
5168
5922
|
res.writeHead(status, { "content-type": "application/json" });
|
|
@@ -5256,6 +6010,26 @@ function createHttpServer(agent, opts) {
|
|
|
5256
6010
|
await agent.reportStatus(parsed.data.channel_id, parsed.data.state, parsed.data.detail);
|
|
5257
6011
|
return json(res, 200, { ok: true });
|
|
5258
6012
|
}
|
|
6013
|
+
if (method === "POST" && path === "/v1/memory/record") {
|
|
6014
|
+
const parsed = memoryRecordSchema.safeParse(JSON.parse(await readBody(req, OTHER_CAP)));
|
|
6015
|
+
if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
|
|
6016
|
+
const r = await agent.recordMemory({
|
|
6017
|
+
conversationId: parsed.data.conversation_id,
|
|
6018
|
+
role: parsed.data.role,
|
|
6019
|
+
peerId: parsed.data.peer_id,
|
|
6020
|
+
text: parsed.data.text,
|
|
6021
|
+
modality: parsed.data.modality,
|
|
6022
|
+
messageId: parsed.data.message_id,
|
|
6023
|
+
idempotencyKey: parsed.data.idempotency_key
|
|
6024
|
+
});
|
|
6025
|
+
return json(res, 200, r);
|
|
6026
|
+
}
|
|
6027
|
+
if (method === "GET" && path === "/v1/memory/context") {
|
|
6028
|
+
const conv = url.searchParams.get("conversation_id");
|
|
6029
|
+
if (!conv) return jsonError(res, "invalid_request", "conversation_id required");
|
|
6030
|
+
const limit = Math.max(1, Math.min(200, Number(url.searchParams.get("limit") ?? 50)));
|
|
6031
|
+
return json(res, 200, await agent.memoryContext(conv, limit));
|
|
6032
|
+
}
|
|
5259
6033
|
if (method === "GET" && path === "/v1/messages") {
|
|
5260
6034
|
const channel = url.searchParams.get("channel_id") ?? void 0;
|
|
5261
6035
|
const wait = Math.max(0, Math.min(50, Number(url.searchParams.get("wait") ?? 0)));
|