@securecoms/agent-bridge 0.3.2 → 0.4.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 +11 -1
- package/dist/bin.js +829 -63
- package/dist/bridge-agent.d.ts +68 -7
- package/dist/config.d.ts +6 -0
- package/dist/http-server.d.ts +42 -0
- package/dist/index.js +827 -61
- 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/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/bin.js
CHANGED
|
@@ -23,6 +23,26 @@ function loadConfig(opts) {
|
|
|
23
23
|
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;
|
|
24
24
|
const rateLimitCapacity = typeof json2.rate_limit_capacity === "number" ? json2.rate_limit_capacity : 20;
|
|
25
25
|
const rateLimitRefillPerMinute = typeof json2.rate_limit_refill_per_minute === "number" ? json2.rate_limit_refill_per_minute : 20;
|
|
26
|
+
const memProvRaw = json2.memory?.provider;
|
|
27
|
+
let memoryProvider;
|
|
28
|
+
let memoryBaseDir;
|
|
29
|
+
let memoryAgent;
|
|
30
|
+
if (memProvRaw === "none") {
|
|
31
|
+
memoryProvider = "none";
|
|
32
|
+
} else if (memProvRaw === "hermes") {
|
|
33
|
+
memoryProvider = "hermes";
|
|
34
|
+
const bd = json2.memory?.base_dir;
|
|
35
|
+
memoryBaseDir = typeof bd === "string" && bd ? bd : "~/.hermes";
|
|
36
|
+
} else if (memProvRaw === "openclaw") {
|
|
37
|
+
memoryProvider = "openclaw";
|
|
38
|
+
const oc = json2.memory?.openclaw;
|
|
39
|
+
const bd = oc?.base_dir;
|
|
40
|
+
memoryBaseDir = typeof bd === "string" && bd ? bd : "~/.openclaw";
|
|
41
|
+
const ag = oc?.agent;
|
|
42
|
+
memoryAgent = typeof ag === "string" && ag ? ag : void 0;
|
|
43
|
+
} else {
|
|
44
|
+
memoryProvider = "securecoms";
|
|
45
|
+
}
|
|
26
46
|
return {
|
|
27
47
|
apiKey: json2.api_key,
|
|
28
48
|
identityId: json2.identity_id,
|
|
@@ -33,7 +53,10 @@ function loadConfig(opts) {
|
|
|
33
53
|
storePath,
|
|
34
54
|
agentName,
|
|
35
55
|
rateLimitCapacity,
|
|
36
|
-
rateLimitRefillPerMinute
|
|
56
|
+
rateLimitRefillPerMinute,
|
|
57
|
+
memoryProvider,
|
|
58
|
+
memoryBaseDir,
|
|
59
|
+
memoryAgent
|
|
37
60
|
};
|
|
38
61
|
}
|
|
39
62
|
function arg(argv, flag) {
|
|
@@ -640,6 +663,152 @@ var payloadsSchema = {
|
|
|
640
663
|
}
|
|
641
664
|
}
|
|
642
665
|
]
|
|
666
|
+
},
|
|
667
|
+
"call_control": {
|
|
668
|
+
"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.',
|
|
669
|
+
"oneOf": [
|
|
670
|
+
{
|
|
671
|
+
"type": "object",
|
|
672
|
+
"additionalProperties": false,
|
|
673
|
+
"required": [
|
|
674
|
+
"kind",
|
|
675
|
+
"event",
|
|
676
|
+
"call_id",
|
|
677
|
+
"room",
|
|
678
|
+
"caller_identity_id",
|
|
679
|
+
"epoch",
|
|
680
|
+
"ts"
|
|
681
|
+
],
|
|
682
|
+
"properties": {
|
|
683
|
+
"kind": {
|
|
684
|
+
"const": "call_control"
|
|
685
|
+
},
|
|
686
|
+
"event": {
|
|
687
|
+
"const": "invite"
|
|
688
|
+
},
|
|
689
|
+
"call_id": {
|
|
690
|
+
"type": "string",
|
|
691
|
+
"minLength": 1
|
|
692
|
+
},
|
|
693
|
+
"room": {
|
|
694
|
+
"type": "string",
|
|
695
|
+
"minLength": 1
|
|
696
|
+
},
|
|
697
|
+
"caller_identity_id": {
|
|
698
|
+
"type": "string",
|
|
699
|
+
"format": "uuid"
|
|
700
|
+
},
|
|
701
|
+
"epoch": {
|
|
702
|
+
"type": "integer",
|
|
703
|
+
"minimum": 0
|
|
704
|
+
},
|
|
705
|
+
"ts": {
|
|
706
|
+
"type": "number"
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
"type": "object",
|
|
712
|
+
"additionalProperties": false,
|
|
713
|
+
"required": [
|
|
714
|
+
"kind",
|
|
715
|
+
"event",
|
|
716
|
+
"call_id",
|
|
717
|
+
"accepting_device_id",
|
|
718
|
+
"key_confirmation"
|
|
719
|
+
],
|
|
720
|
+
"properties": {
|
|
721
|
+
"kind": {
|
|
722
|
+
"const": "call_control"
|
|
723
|
+
},
|
|
724
|
+
"event": {
|
|
725
|
+
"const": "accept"
|
|
726
|
+
},
|
|
727
|
+
"call_id": {
|
|
728
|
+
"type": "string",
|
|
729
|
+
"minLength": 1
|
|
730
|
+
},
|
|
731
|
+
"accepting_device_id": {
|
|
732
|
+
"type": "string",
|
|
733
|
+
"minLength": 1
|
|
734
|
+
},
|
|
735
|
+
"key_confirmation": {
|
|
736
|
+
"type": "string",
|
|
737
|
+
"minLength": 1
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
"type": "object",
|
|
743
|
+
"additionalProperties": false,
|
|
744
|
+
"required": [
|
|
745
|
+
"kind",
|
|
746
|
+
"event",
|
|
747
|
+
"call_id"
|
|
748
|
+
],
|
|
749
|
+
"properties": {
|
|
750
|
+
"kind": {
|
|
751
|
+
"const": "call_control"
|
|
752
|
+
},
|
|
753
|
+
"event": {
|
|
754
|
+
"const": "decline"
|
|
755
|
+
},
|
|
756
|
+
"call_id": {
|
|
757
|
+
"type": "string",
|
|
758
|
+
"minLength": 1
|
|
759
|
+
},
|
|
760
|
+
"reason": {
|
|
761
|
+
"type": "string",
|
|
762
|
+
"enum": [
|
|
763
|
+
"busy",
|
|
764
|
+
"declined"
|
|
765
|
+
]
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
"type": "object",
|
|
771
|
+
"additionalProperties": false,
|
|
772
|
+
"required": [
|
|
773
|
+
"kind",
|
|
774
|
+
"event",
|
|
775
|
+
"call_id"
|
|
776
|
+
],
|
|
777
|
+
"properties": {
|
|
778
|
+
"kind": {
|
|
779
|
+
"const": "call_control"
|
|
780
|
+
},
|
|
781
|
+
"event": {
|
|
782
|
+
"const": "cancel"
|
|
783
|
+
},
|
|
784
|
+
"call_id": {
|
|
785
|
+
"type": "string",
|
|
786
|
+
"minLength": 1
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
"type": "object",
|
|
792
|
+
"additionalProperties": false,
|
|
793
|
+
"required": [
|
|
794
|
+
"kind",
|
|
795
|
+
"event",
|
|
796
|
+
"call_id"
|
|
797
|
+
],
|
|
798
|
+
"properties": {
|
|
799
|
+
"kind": {
|
|
800
|
+
"const": "call_control"
|
|
801
|
+
},
|
|
802
|
+
"event": {
|
|
803
|
+
"const": "end"
|
|
804
|
+
},
|
|
805
|
+
"call_id": {
|
|
806
|
+
"type": "string",
|
|
807
|
+
"minLength": 1
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
]
|
|
643
812
|
}
|
|
644
813
|
}
|
|
645
814
|
};
|
|
@@ -664,6 +833,11 @@ if (!agentActionDef) {
|
|
|
664
833
|
throw new Error(`payloads.schema.json is missing the "agent_action" $defs schema`);
|
|
665
834
|
}
|
|
666
835
|
var agentActionValidator = ajv.compile(agentActionDef);
|
|
836
|
+
var callControlDef = payloadDefs["call_control"];
|
|
837
|
+
if (!callControlDef) {
|
|
838
|
+
throw new Error(`payloads.schema.json is missing the "call_control" $defs schema`);
|
|
839
|
+
}
|
|
840
|
+
var callControlValidator = ajv.compile(callControlDef);
|
|
667
841
|
|
|
668
842
|
// ../connect-core/dist/envelope/sign.js
|
|
669
843
|
import { canonicalize } from "@agentvault/crypto/canonical";
|
|
@@ -725,6 +899,7 @@ import { gcm } from "@noble/ciphers/aes";
|
|
|
725
899
|
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
726
900
|
import { randomBytes } from "@noble/hashes/utils";
|
|
727
901
|
import { canonicalize as canonicalize2 } from "@agentvault/crypto/canonical";
|
|
902
|
+
var VOICE_MEDIA_EXPORTER_LABEL = "securecoms-voice-media-v1";
|
|
728
903
|
var APPLICATION_KEY_BYTES = 32;
|
|
729
904
|
var REPLAY_NONCE_BYTES = 24;
|
|
730
905
|
var NONCE_BYTES = {
|
|
@@ -746,10 +921,20 @@ function epochToBytes(epoch) {
|
|
|
746
921
|
new DataView(b.buffer).setBigUint64(0, BigInt(epoch), false);
|
|
747
922
|
return b;
|
|
748
923
|
}
|
|
924
|
+
function u32ToBytes(n) {
|
|
925
|
+
const b = new Uint8Array(4);
|
|
926
|
+
new DataView(b.buffer).setUint32(0, n >>> 0, false);
|
|
927
|
+
return b;
|
|
928
|
+
}
|
|
749
929
|
function buildExporterContext(enclaveId, channelId, epoch) {
|
|
750
930
|
const enc = new TextEncoder();
|
|
751
931
|
return concatBytes(enc.encode(enclaveId), enc.encode(channelId), epochToBytes(epoch));
|
|
752
932
|
}
|
|
933
|
+
function buildVoiceExporterContext(enclaveId, channelId, epoch, callId) {
|
|
934
|
+
const enc = new TextEncoder();
|
|
935
|
+
const callIdBytes = enc.encode(callId);
|
|
936
|
+
return concatBytes(buildExporterContext(enclaveId, channelId, epoch), u32ToBytes(callIdBytes.length), callIdBytes);
|
|
937
|
+
}
|
|
753
938
|
function cipherFor(suite, key, nonce, aad) {
|
|
754
939
|
return suite === "aes256gcm" ? gcm(key, nonce, aad) : xchacha20poly1305(key, nonce, aad);
|
|
755
940
|
}
|
|
@@ -1117,8 +1302,13 @@ var ALWAYS_SAMPLE = /* @__PURE__ */ new Set([
|
|
|
1117
1302
|
// ../connect-sdk-ts/dist/spans/emitter.js
|
|
1118
1303
|
var KNOWN = new Set(Object.values(SPAN));
|
|
1119
1304
|
|
|
1305
|
+
// ../connect-sdk-ts/dist/voice-key.js
|
|
1306
|
+
import { hmac } from "@noble/hashes/hmac";
|
|
1307
|
+
import { sha256 as sha2562 } from "@noble/hashes/sha256";
|
|
1308
|
+
import { bytesToHex } from "@agentvault/crypto";
|
|
1309
|
+
|
|
1120
1310
|
// ../connect-sdk-ts/dist/groupkey/mls.js
|
|
1121
|
-
import { MLSGroupManager, bytesToHex, hexToBytes, verifyApprovalArtifact } from "@agentvault/crypto";
|
|
1311
|
+
import { MLSGroupManager, bytesToHex as bytesToHex2, hexToBytes, verifyApprovalArtifact } from "@agentvault/crypto";
|
|
1122
1312
|
|
|
1123
1313
|
// ../connect-sdk-ts/dist/groupkey/operation-queue.js
|
|
1124
1314
|
var MlsOperationQueue = class {
|
|
@@ -1498,6 +1688,28 @@ var MlsGroupKeyProvider = class {
|
|
|
1498
1688
|
const mgr = await this.manager(channelId);
|
|
1499
1689
|
return mgr.exportSecret(LABEL, buildExporterContext(enclaveId, channelId, epoch), 32);
|
|
1500
1690
|
}
|
|
1691
|
+
/**
|
|
1692
|
+
* The 1:1 voice media key: taken STRAIGHT from the MLS exporter under a
|
|
1693
|
+
* DEDICATED label ({@link VOICE_MEDIA_EXPORTER_LABEL}) and a context that
|
|
1694
|
+
* binds `callId` (voice-h2h spec, audit H-P1) — domain-separated from the
|
|
1695
|
+
* message key ({@link exporterKey}) and per-call, with NO HKDF layered on
|
|
1696
|
+
* top. Taken under the per-channel lock (as the receive-side message-key
|
|
1697
|
+
* read is, mls.ts) so the exporter read is epoch-stable against a concurrent
|
|
1698
|
+
* commit. Never transmitted or persisted; both call participants derive it
|
|
1699
|
+
* independently at the invite epoch.
|
|
1700
|
+
*
|
|
1701
|
+
* CONTRACT: unlike {@link exporterKey} (lock-free — its callers wrap it), this
|
|
1702
|
+
* takes the channel lock INTERNALLY. The lock queue is non-reentrant, so this
|
|
1703
|
+
* MUST NOT be called while already holding this channel's lock (doing so
|
|
1704
|
+
* self-deadlocks). Call it stand-alone from the call-setup path, never from
|
|
1705
|
+
* inside another `locks.run(channelId, ...)` for the same channel.
|
|
1706
|
+
*/
|
|
1707
|
+
async voiceMediaKey(enclaveId, channelId, epoch, callId) {
|
|
1708
|
+
return this.locks.run(channelId, async () => {
|
|
1709
|
+
const mgr = await this.manager(channelId);
|
|
1710
|
+
return mgr.exportSecret(VOICE_MEDIA_EXPORTER_LABEL, buildVoiceExporterContext(enclaveId, channelId, epoch, callId), 32);
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1501
1713
|
/**
|
|
1502
1714
|
* Join a channel from the host-committed Welcome, then drain any commits that
|
|
1503
1715
|
* landed after the Welcome so we converge on the true current epoch. Without
|
|
@@ -1626,7 +1838,7 @@ var MlsGroupKeyProvider = class {
|
|
|
1626
1838
|
const bundle = await this.o.store.loadPendingBundle(this.o.deviceId);
|
|
1627
1839
|
if (!bundle)
|
|
1628
1840
|
return false;
|
|
1629
|
-
const heldHex =
|
|
1841
|
+
const heldHex = bytesToHex2(MLSGroupManager.serializeKeyPackage(bundle.publicPackage));
|
|
1630
1842
|
const published = await this.o.backend.fetchKeyPackages([this.o.deviceId]);
|
|
1631
1843
|
const pubHex = published[this.o.deviceId];
|
|
1632
1844
|
return !!pubHex && pubHex === heldHex;
|
|
@@ -1642,7 +1854,7 @@ var MlsGroupKeyProvider = class {
|
|
|
1642
1854
|
console.warn(`publishKeyPackage: minting a fresh KeyPackage (rotation) for device ${this.o.deviceId.slice(0, 8)}`);
|
|
1643
1855
|
const mgr = new MLSGroupManager();
|
|
1644
1856
|
const bundle = await mgr.generateKeyPackage(this.o.identity);
|
|
1645
|
-
const hex =
|
|
1857
|
+
const hex = bytesToHex2(MLSGroupManager.serializeKeyPackage(bundle.publicPackage));
|
|
1646
1858
|
const id = await this.o.backend.publishKeyPackage(this.o.deviceId, hex);
|
|
1647
1859
|
await this.o.store.saveLastKpAndBundle(this.o.deviceId, id, bundle);
|
|
1648
1860
|
return id;
|
|
@@ -1682,11 +1894,11 @@ var MlsGroupKeyProvider = class {
|
|
|
1682
1894
|
const { commit, welcome } = await mgr.addMembers(pkgs);
|
|
1683
1895
|
await this.o.backend.distribute(reinit.group_id, {
|
|
1684
1896
|
device_id: this.o.deviceId,
|
|
1685
|
-
commit:
|
|
1897
|
+
commit: bytesToHex2(commit),
|
|
1686
1898
|
commit_epoch: Number(mgr.epoch),
|
|
1687
1899
|
expected_epoch: reinit.current_epoch,
|
|
1688
1900
|
member_device_ids: [this.o.deviceId, ...others],
|
|
1689
|
-
welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload:
|
|
1901
|
+
welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload: bytesToHex2(welcome) })) : []
|
|
1690
1902
|
});
|
|
1691
1903
|
await this.o.store.save(channelId, mgr.exportState());
|
|
1692
1904
|
this.managers.set(channelId, mgr);
|
|
@@ -1709,11 +1921,11 @@ var MlsGroupKeyProvider = class {
|
|
|
1709
1921
|
const { commit, welcome } = await mgr.addMembers(pkgs);
|
|
1710
1922
|
await this.o.backend.distribute(init.group_id, {
|
|
1711
1923
|
device_id: this.o.deviceId,
|
|
1712
|
-
commit:
|
|
1924
|
+
commit: bytesToHex2(commit),
|
|
1713
1925
|
commit_epoch: Number(mgr.epoch),
|
|
1714
1926
|
expected_epoch: init.current_epoch,
|
|
1715
1927
|
member_device_ids: [this.o.deviceId, ...others],
|
|
1716
|
-
welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload:
|
|
1928
|
+
welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload: bytesToHex2(welcome) })) : []
|
|
1717
1929
|
});
|
|
1718
1930
|
this.managers.set(channelId, mgr);
|
|
1719
1931
|
await this.o.store.save(channelId, mgr.exportState());
|
|
@@ -1773,12 +1985,12 @@ var MlsGroupKeyProvider = class {
|
|
|
1773
1985
|
try {
|
|
1774
1986
|
await this.o.backend.distribute(init.group_id, {
|
|
1775
1987
|
device_id: this.o.deviceId,
|
|
1776
|
-
commit:
|
|
1988
|
+
commit: bytesToHex2(commit),
|
|
1777
1989
|
commit_epoch: Number(mgr.epoch),
|
|
1778
1990
|
expected_epoch: priorEpoch,
|
|
1779
1991
|
member_device_ids: members,
|
|
1780
1992
|
op: "add",
|
|
1781
|
-
welcomes: welcome ? [{ target_device_id: newDeviceId, payload:
|
|
1993
|
+
welcomes: welcome ? [{ target_device_id: newDeviceId, payload: bytesToHex2(welcome) }] : []
|
|
1782
1994
|
});
|
|
1783
1995
|
} catch (e) {
|
|
1784
1996
|
this.managers.delete(channelId);
|
|
@@ -1836,7 +2048,7 @@ var MlsGroupKeyProvider = class {
|
|
|
1836
2048
|
try {
|
|
1837
2049
|
await this.o.backend.distribute(init.group_id, {
|
|
1838
2050
|
device_id: this.o.deviceId,
|
|
1839
|
-
commit:
|
|
2051
|
+
commit: bytesToHex2(commit),
|
|
1840
2052
|
commit_epoch: Number(mgr.epoch),
|
|
1841
2053
|
expected_epoch: priorEpoch,
|
|
1842
2054
|
member_device_ids: members,
|
|
@@ -1904,11 +2116,11 @@ var MlsGroupKeyProvider = class {
|
|
|
1904
2116
|
}
|
|
1905
2117
|
await this.o.store.saveStaging(channelId, mgr.exportState());
|
|
1906
2118
|
const members = init.member_device_ids ?? [this.o.deviceId];
|
|
1907
|
-
const removedIdentity = typeof opts.identity === "string" ? opts.identity :
|
|
2119
|
+
const removedIdentity = typeof opts.identity === "string" ? opts.identity : bytesToHex2(opts.identity);
|
|
1908
2120
|
try {
|
|
1909
2121
|
await this.o.backend.distribute(init.group_id, {
|
|
1910
2122
|
device_id: this.o.deviceId,
|
|
1911
|
-
commit:
|
|
2123
|
+
commit: bytesToHex2(commit),
|
|
1912
2124
|
commit_epoch: Number(mgr.epoch),
|
|
1913
2125
|
expected_epoch: priorEpoch,
|
|
1914
2126
|
member_device_ids: members,
|
|
@@ -2139,11 +2351,11 @@ var MlsGroupKeyProvider = class {
|
|
|
2139
2351
|
const { commit, welcome } = await mgr.addMembers(pkgs);
|
|
2140
2352
|
await this.o.backend.distribute(reinit.group_id, {
|
|
2141
2353
|
device_id: this.o.deviceId,
|
|
2142
|
-
commit:
|
|
2354
|
+
commit: bytesToHex2(commit),
|
|
2143
2355
|
commit_epoch: Number(mgr.epoch),
|
|
2144
2356
|
expected_epoch: reinit.current_epoch,
|
|
2145
2357
|
member_device_ids: [this.o.deviceId, ...others],
|
|
2146
|
-
welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload:
|
|
2358
|
+
welcomes: welcome ? others.map((d) => ({ target_device_id: d, payload: bytesToHex2(welcome) })) : []
|
|
2147
2359
|
});
|
|
2148
2360
|
await this.o.store.save(channelId, mgr.exportState());
|
|
2149
2361
|
this.managers.set(channelId, mgr);
|
|
@@ -3095,11 +3307,11 @@ var RestMlsBackend = class {
|
|
|
3095
3307
|
};
|
|
3096
3308
|
|
|
3097
3309
|
// ../connect-sdk-ts/dist/mls/fake-backend.js
|
|
3098
|
-
import { bytesToHex as
|
|
3310
|
+
import { bytesToHex as bytesToHex3, hexToBytes as hexToBytes2 } from "@agentvault/crypto";
|
|
3099
3311
|
|
|
3100
3312
|
// ../connect-sdk-ts/dist/receive/inbox-cipher.js
|
|
3101
3313
|
import { hkdf } from "@noble/hashes/hkdf";
|
|
3102
|
-
import { sha256 as
|
|
3314
|
+
import { sha256 as sha2563 } from "@noble/hashes/sha256";
|
|
3103
3315
|
var SUITE = "xchacha20poly1305";
|
|
3104
3316
|
var INFO = new TextEncoder().encode("agentvault-connect-inbox-v1");
|
|
3105
3317
|
var EMPTY_AAD = new Uint8Array(0);
|
|
@@ -3127,12 +3339,12 @@ var KeyedInboxCipher = class {
|
|
|
3127
3339
|
};
|
|
3128
3340
|
var MLS_STORE_INFO = new TextEncoder().encode("agentvault-connect-mls-store-v1");
|
|
3129
3341
|
function deriveMlsStoreKey(identityKeyMaterial) {
|
|
3130
|
-
return hkdf(
|
|
3342
|
+
return hkdf(sha2563, identityKeyMaterial, void 0, MLS_STORE_INFO, 32);
|
|
3131
3343
|
}
|
|
3132
3344
|
|
|
3133
3345
|
// ../connect-sdk-ts/dist/recovery/backup-crypto.js
|
|
3134
3346
|
import { hkdf as hkdf2 } from "@noble/hashes/hkdf";
|
|
3135
|
-
import { sha256 as
|
|
3347
|
+
import { sha256 as sha2564 } from "@noble/hashes/sha256";
|
|
3136
3348
|
import { randomBytes as randomBytes2 } from "@noble/hashes/utils";
|
|
3137
3349
|
var BACKUP_INFO = new TextEncoder().encode("connect-backup-v1");
|
|
3138
3350
|
var RECOVERY_INFO = new TextEncoder().encode("connect-recovery-v1");
|
|
@@ -3140,15 +3352,15 @@ var RECOVERY_INFO = new TextEncoder().encode("connect-recovery-v1");
|
|
|
3140
3352
|
// ../connect-sdk-ts/dist/recovery/guest-recovery-crypto.js
|
|
3141
3353
|
import { argon2idAsync } from "@noble/hashes/argon2";
|
|
3142
3354
|
import { hkdf as hkdf3 } from "@noble/hashes/hkdf";
|
|
3143
|
-
import { sha256 as
|
|
3355
|
+
import { sha256 as sha2565 } from "@noble/hashes/sha256";
|
|
3144
3356
|
var HKDF_INFO = new TextEncoder().encode("connect-guest-recovery-wrap-v1");
|
|
3145
3357
|
|
|
3146
3358
|
// ../connect-mls-store/dist/sqlite-store.js
|
|
3147
3359
|
import { DatabaseSync } from "node:sqlite";
|
|
3148
|
-
import { MLSGroupManager as MLSGroupManager2, bytesToHex as
|
|
3360
|
+
import { MLSGroupManager as MLSGroupManager2, bytesToHex as bytesToHex4, hexToBytes as hexToBytes3 } from "@agentvault/crypto";
|
|
3149
3361
|
|
|
3150
3362
|
// ../connect-mls-store/dist/schema.js
|
|
3151
|
-
var SCHEMA_VERSION =
|
|
3363
|
+
var SCHEMA_VERSION = 6;
|
|
3152
3364
|
function openSchema(db) {
|
|
3153
3365
|
db.exec("PRAGMA journal_mode=WAL");
|
|
3154
3366
|
const row = db.prepare("PRAGMA user_version").get();
|
|
@@ -3195,6 +3407,51 @@ function openSchema(db) {
|
|
|
3195
3407
|
);
|
|
3196
3408
|
`);
|
|
3197
3409
|
}
|
|
3410
|
+
if (row.user_version < 5) {
|
|
3411
|
+
db.exec(`
|
|
3412
|
+
CREATE TABLE IF NOT EXISTS channel_meta (
|
|
3413
|
+
identity_id TEXT NOT NULL,
|
|
3414
|
+
channel_id TEXT NOT NULL,
|
|
3415
|
+
enclave_id TEXT,
|
|
3416
|
+
is_dm INTEGER,
|
|
3417
|
+
updated_at INTEGER NOT NULL,
|
|
3418
|
+
PRIMARY KEY (identity_id, channel_id)
|
|
3419
|
+
);
|
|
3420
|
+
`);
|
|
3421
|
+
}
|
|
3422
|
+
if (row.user_version < 6) {
|
|
3423
|
+
db.exec(`
|
|
3424
|
+
CREATE TABLE IF NOT EXISTS agent_memory (
|
|
3425
|
+
identity_id TEXT NOT NULL,
|
|
3426
|
+
conversation_id TEXT NOT NULL,
|
|
3427
|
+
seq INTEGER NOT NULL,
|
|
3428
|
+
role TEXT NOT NULL,
|
|
3429
|
+
peer_id TEXT,
|
|
3430
|
+
sealed_text BLOB NOT NULL,
|
|
3431
|
+
modality TEXT NOT NULL,
|
|
3432
|
+
message_id TEXT,
|
|
3433
|
+
idempotency_key TEXT,
|
|
3434
|
+
dedup_key TEXT,
|
|
3435
|
+
recorded_at INTEGER NOT NULL,
|
|
3436
|
+
PRIMARY KEY (identity_id, conversation_id, seq)
|
|
3437
|
+
);
|
|
3438
|
+
`);
|
|
3439
|
+
db.exec(`
|
|
3440
|
+
CREATE UNIQUE INDEX IF NOT EXISTS agent_memory_dedup
|
|
3441
|
+
ON agent_memory (identity_id, conversation_id, dedup_key)
|
|
3442
|
+
WHERE dedup_key IS NOT NULL;
|
|
3443
|
+
`);
|
|
3444
|
+
db.exec(`
|
|
3445
|
+
CREATE TABLE IF NOT EXISTS agent_memory_fact (
|
|
3446
|
+
identity_id TEXT NOT NULL,
|
|
3447
|
+
conversation_id TEXT NOT NULL,
|
|
3448
|
+
fact_id INTEGER NOT NULL,
|
|
3449
|
+
sealed_fact BLOB NOT NULL,
|
|
3450
|
+
added_at INTEGER NOT NULL,
|
|
3451
|
+
PRIMARY KEY (identity_id, conversation_id, fact_id)
|
|
3452
|
+
);
|
|
3453
|
+
`);
|
|
3454
|
+
}
|
|
3198
3455
|
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
3199
3456
|
}
|
|
3200
3457
|
|
|
@@ -3216,10 +3473,10 @@ var MlsStoreSealError = class extends Error {
|
|
|
3216
3473
|
function encodeBundle(bundle) {
|
|
3217
3474
|
const pp = bundle.privatePackage;
|
|
3218
3475
|
const obj = {
|
|
3219
|
-
pub:
|
|
3220
|
-
initPriv:
|
|
3221
|
-
hpkePriv:
|
|
3222
|
-
sigPriv:
|
|
3476
|
+
pub: bytesToHex4(MLSGroupManager2.serializeKeyPackage(bundle.publicPackage)),
|
|
3477
|
+
initPriv: bytesToHex4(pp.initPrivateKey),
|
|
3478
|
+
hpkePriv: bytesToHex4(pp.hpkePrivateKey),
|
|
3479
|
+
sigPriv: bytesToHex4(pp.signaturePrivateKey)
|
|
3223
3480
|
};
|
|
3224
3481
|
return new TextEncoder().encode(JSON.stringify(obj));
|
|
3225
3482
|
}
|
|
@@ -3246,9 +3503,11 @@ var SqliteMlsStateStore = class {
|
|
|
3246
3503
|
// the source of truth across process restarts.
|
|
3247
3504
|
bundles = /* @__PURE__ */ new Map();
|
|
3248
3505
|
lastKpIds = /* @__PURE__ */ new Map();
|
|
3506
|
+
memoryRetentionK;
|
|
3249
3507
|
constructor(opts) {
|
|
3250
3508
|
this.id = opts.identityId;
|
|
3251
3509
|
this.cipher = opts.cipherFor(opts.identityId);
|
|
3510
|
+
this.memoryRetentionK = opts.memoryRetentionK ?? 500;
|
|
3252
3511
|
try {
|
|
3253
3512
|
this.db = new DatabaseSync(opts.dbPath);
|
|
3254
3513
|
} catch (e) {
|
|
@@ -3383,6 +3642,74 @@ var SqliteMlsStateStore = class {
|
|
|
3383
3642
|
const rows = this.db.prepare("SELECT group_id FROM mls_retired_group WHERE channel_id=?").all(channelId);
|
|
3384
3643
|
return rows.map((r) => r.group_id);
|
|
3385
3644
|
}
|
|
3645
|
+
recordChannelEnclave(channelId, enclaveId) {
|
|
3646
|
+
this.db.prepare(`INSERT INTO channel_meta (identity_id, channel_id, enclave_id, updated_at)
|
|
3647
|
+
VALUES (?,?,?,?)
|
|
3648
|
+
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());
|
|
3649
|
+
}
|
|
3650
|
+
recordChannelIsDm(channelId, isDm) {
|
|
3651
|
+
this.db.prepare(`INSERT INTO channel_meta (identity_id, channel_id, is_dm, updated_at)
|
|
3652
|
+
VALUES (?,?,?,?)
|
|
3653
|
+
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());
|
|
3654
|
+
}
|
|
3655
|
+
getChannelMeta(channelId) {
|
|
3656
|
+
const row = this.db.prepare("SELECT enclave_id, is_dm FROM channel_meta WHERE identity_id=? AND channel_id=?").get(this.id, channelId);
|
|
3657
|
+
if (!row)
|
|
3658
|
+
return void 0;
|
|
3659
|
+
return {
|
|
3660
|
+
enclaveId: row.enclave_id ?? void 0,
|
|
3661
|
+
isDm: row.is_dm == null ? void 0 : row.is_dm === 1
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
/**
|
|
3665
|
+
* The set of enclave ids this identity has learned about (from inbound
|
|
3666
|
+
* envelopes or a prior listChannels discovery), beyond its own home
|
|
3667
|
+
* enclave. Task 3: refreshChannelMeta unions this with cfg.enclaveId so a
|
|
3668
|
+
* cross-enclave room's is_dm gets resolved instead of staying UNKNOWN.
|
|
3669
|
+
*/
|
|
3670
|
+
distinctEnclaveIds() {
|
|
3671
|
+
const rows = this.db.prepare("SELECT DISTINCT enclave_id FROM channel_meta WHERE identity_id=? AND enclave_id IS NOT NULL").all(this.id);
|
|
3672
|
+
return rows.map((r) => r.enclave_id);
|
|
3673
|
+
}
|
|
3674
|
+
async recordMemory(input) {
|
|
3675
|
+
const dedupKey = input.messageId ?? input.idempotencyKey ?? null;
|
|
3676
|
+
if (!dedupKey)
|
|
3677
|
+
throw new Error("recordMemory requires message_id or idempotency_key");
|
|
3678
|
+
const sealed = await this.cipher.seal(new TextEncoder().encode(input.text));
|
|
3679
|
+
const info = this.db.prepare(`INSERT OR IGNORE INTO agent_memory
|
|
3680
|
+
(identity_id, conversation_id, seq, role, peer_id, sealed_text, modality, message_id, idempotency_key, dedup_key, recorded_at)
|
|
3681
|
+
VALUES (?, ?,
|
|
3682
|
+
(SELECT COALESCE(MAX(seq),0)+1 FROM agent_memory WHERE identity_id=? AND conversation_id=?),
|
|
3683
|
+
?, ?, ?, ?, ?, ?, ?, ?)`).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());
|
|
3684
|
+
if (info.changes === 0) {
|
|
3685
|
+
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);
|
|
3686
|
+
return { seq: row2.seq, deduped: true };
|
|
3687
|
+
}
|
|
3688
|
+
const row = this.db.prepare("SELECT MAX(seq) AS seq FROM agent_memory WHERE identity_id=? AND conversation_id=?").get(this.id, input.conversationId);
|
|
3689
|
+
if (this.memoryRetentionK > 0) {
|
|
3690
|
+
this.db.prepare("DELETE FROM agent_memory WHERE identity_id=? AND conversation_id=? AND seq <= ?").run(this.id, input.conversationId, row.seq - this.memoryRetentionK);
|
|
3691
|
+
}
|
|
3692
|
+
return { seq: row.seq, deduped: false };
|
|
3693
|
+
}
|
|
3694
|
+
async memoryContext(conversationId, limit = 50) {
|
|
3695
|
+
const rows = this.db.prepare(`SELECT seq, role, peer_id, sealed_text, recorded_at FROM agent_memory
|
|
3696
|
+
WHERE identity_id=? AND conversation_id=? ORDER BY seq DESC LIMIT ?`).all(this.id, conversationId, limit);
|
|
3697
|
+
const recent = [];
|
|
3698
|
+
for (const r of rows.reverse()) {
|
|
3699
|
+
const text = new TextDecoder().decode(await this.cipher.open(r.sealed_text));
|
|
3700
|
+
recent.push({ seq: r.seq, role: r.role, peerId: r.peer_id ?? void 0, text, ts: r.recorded_at });
|
|
3701
|
+
}
|
|
3702
|
+
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);
|
|
3703
|
+
const facts = [];
|
|
3704
|
+
for (const f of factRows)
|
|
3705
|
+
facts.push(new TextDecoder().decode(await this.cipher.open(f.sealed_fact)));
|
|
3706
|
+
return { recent, facts };
|
|
3707
|
+
}
|
|
3708
|
+
async recordMemoryFact(conversationId, fact) {
|
|
3709
|
+
const sealed = await this.cipher.seal(new TextEncoder().encode(fact));
|
|
3710
|
+
this.db.prepare(`INSERT INTO agent_memory_fact (identity_id, conversation_id, fact_id, sealed_fact, added_at)
|
|
3711
|
+
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());
|
|
3712
|
+
}
|
|
3386
3713
|
/** @internal test-only */
|
|
3387
3714
|
_rawMainForTest(identityId, channelId) {
|
|
3388
3715
|
const row = this.db.prepare("SELECT sealed_main FROM mls_group_state WHERE identity_id=? AND channel_id=?").get(identityId, channelId);
|
|
@@ -3447,7 +3774,7 @@ function loadPackageMeta(importMetaUrl) {
|
|
|
3447
3774
|
|
|
3448
3775
|
// src/bridge-agent.ts
|
|
3449
3776
|
import { promises as fsp } from "node:fs";
|
|
3450
|
-
import { join as
|
|
3777
|
+
import { join as join7, dirname as dirname3 } from "node:path";
|
|
3451
3778
|
|
|
3452
3779
|
// src/latency.ts
|
|
3453
3780
|
var DEFAULT_CAPACITY = 1024;
|
|
@@ -3721,8 +4048,8 @@ async function prepareAttachment(filePath, caption, channelId, deps) {
|
|
|
3721
4048
|
}
|
|
3722
4049
|
async function materializeAttachment(body, ctx, deps) {
|
|
3723
4050
|
const ciphertext = await deps.downloadBlob(body.blobId);
|
|
3724
|
-
const
|
|
3725
|
-
if (
|
|
4051
|
+
const sha2566 = deps.computeFileDigest(ciphertext);
|
|
4052
|
+
if (sha2566 !== body.digest) {
|
|
3726
4053
|
throw new Error(`attachment digest mismatch for blob ${body.blobId}`);
|
|
3727
4054
|
}
|
|
3728
4055
|
const plain = deps.decryptFile(
|
|
@@ -3735,7 +4062,328 @@ async function materializeAttachment(body, ctx, deps) {
|
|
|
3735
4062
|
const path = `${dir}/${ctx.messageId}-${safeName}`;
|
|
3736
4063
|
await deps.mkdirp(dir);
|
|
3737
4064
|
await deps.writeFile(path, plain);
|
|
3738
|
-
return { filename: safeName, mime: body.mime, size: plain.length, path, sha256:
|
|
4065
|
+
return { filename: safeName, mime: body.mime, size: plain.length, path, sha256: sha2566 };
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
// src/memory/hermes-session-map.ts
|
|
4069
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
4070
|
+
import { homedir } from "node:os";
|
|
4071
|
+
import { join as join3 } from "node:path";
|
|
4072
|
+
function expandHome(p) {
|
|
4073
|
+
return p === "~" || p.startsWith("~/") ? join3(homedir(), p.slice(1)) : p;
|
|
4074
|
+
}
|
|
4075
|
+
function resolveHermesSession(baseDir, conversationId) {
|
|
4076
|
+
try {
|
|
4077
|
+
const path = join3(expandHome(baseDir), "sessions", "sessions.json");
|
|
4078
|
+
const map = JSON.parse(readFileSync2(path, "utf8"));
|
|
4079
|
+
const entry = map[`agent:main:securecoms:dm:${conversationId}`];
|
|
4080
|
+
const sid = entry?.session_id;
|
|
4081
|
+
return typeof sid === "string" && sid ? sid : null;
|
|
4082
|
+
} catch {
|
|
4083
|
+
return null;
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
// src/memory/hermes-store.ts
|
|
4088
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
4089
|
+
import { join as join4 } from "node:path";
|
|
4090
|
+
var HermesStore = class {
|
|
4091
|
+
dbPath;
|
|
4092
|
+
constructor(baseDir) {
|
|
4093
|
+
this.dbPath = join4(expandHome(baseDir), "state.db");
|
|
4094
|
+
}
|
|
4095
|
+
/** Recent-K conversational turns for a session, chronological. Fail-safe → []. */
|
|
4096
|
+
readRecent(sessionId, limit) {
|
|
4097
|
+
let db;
|
|
4098
|
+
try {
|
|
4099
|
+
db = new DatabaseSync2(this.dbPath, { readOnly: true });
|
|
4100
|
+
const rows = db.prepare(
|
|
4101
|
+
`SELECT id, role, content, timestamp FROM messages
|
|
4102
|
+
WHERE session_id = ? AND active = 1 AND content IS NOT NULL AND role IN ('user','assistant')
|
|
4103
|
+
ORDER BY timestamp DESC LIMIT ?`
|
|
4104
|
+
).all(sessionId, limit);
|
|
4105
|
+
return rows.reverse().map((r) => ({ id: r.id, role: r.role, content: r.content, timestamp: r.timestamp }));
|
|
4106
|
+
} catch {
|
|
4107
|
+
return [];
|
|
4108
|
+
} finally {
|
|
4109
|
+
db?.close();
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
open(busyMs = 5e3) {
|
|
4113
|
+
const db = new DatabaseSync2(this.dbPath);
|
|
4114
|
+
db.exec(`PRAGMA busy_timeout = ${busyMs}`);
|
|
4115
|
+
return db;
|
|
4116
|
+
}
|
|
4117
|
+
/**
|
|
4118
|
+
* Append a voice turn to a session in one synchronous BEGIN IMMEDIATE block
|
|
4119
|
+
* (no await between statements → atomic against Hermes and our own retries).
|
|
4120
|
+
* Guards: skip when the session is absent/archived/ended; in-transaction dedup
|
|
4121
|
+
* on (session_id, platform_message_id). Writes only `messages`. Fail-safe.
|
|
4122
|
+
*/
|
|
4123
|
+
writeVoiceTurn(input) {
|
|
4124
|
+
let db;
|
|
4125
|
+
try {
|
|
4126
|
+
db = this.open();
|
|
4127
|
+
db.exec("BEGIN IMMEDIATE");
|
|
4128
|
+
try {
|
|
4129
|
+
const s = db.prepare("SELECT archived, ended_at FROM sessions WHERE id = ?").get(input.sessionId);
|
|
4130
|
+
if (!s || s.archived === 1 || s.ended_at != null) {
|
|
4131
|
+
db.exec("ROLLBACK");
|
|
4132
|
+
return "skipped";
|
|
4133
|
+
}
|
|
4134
|
+
if (input.dedupKey) {
|
|
4135
|
+
const dup = db.prepare("SELECT 1 FROM messages WHERE session_id = ? AND platform_message_id = ?").get(input.sessionId, input.dedupKey);
|
|
4136
|
+
if (dup) {
|
|
4137
|
+
db.exec("ROLLBACK");
|
|
4138
|
+
return "deduped";
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
db.prepare(
|
|
4142
|
+
`INSERT INTO messages (session_id, role, content, timestamp, active, platform_message_id) VALUES (?, ?, ?, ?, 1, ?)`
|
|
4143
|
+
).run(input.sessionId, input.role, input.content, input.timestamp, input.dedupKey);
|
|
4144
|
+
db.exec("COMMIT");
|
|
4145
|
+
return "written";
|
|
4146
|
+
} catch (e) {
|
|
4147
|
+
try {
|
|
4148
|
+
db.exec("ROLLBACK");
|
|
4149
|
+
} catch {
|
|
4150
|
+
}
|
|
4151
|
+
throw e;
|
|
4152
|
+
}
|
|
4153
|
+
} catch {
|
|
4154
|
+
return "failed";
|
|
4155
|
+
} finally {
|
|
4156
|
+
db?.close();
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
/**
|
|
4160
|
+
* Startup write-capability probe: insert a sentinel against any existing
|
|
4161
|
+
* session inside a rolled-back transaction. Throws if the write path is broken
|
|
4162
|
+
* (e.g. a schema regression). No-op when the db has no sessions.
|
|
4163
|
+
*/
|
|
4164
|
+
probeWrite() {
|
|
4165
|
+
let db;
|
|
4166
|
+
try {
|
|
4167
|
+
db = this.open(500);
|
|
4168
|
+
const s = db.prepare("SELECT id FROM sessions LIMIT 1").get();
|
|
4169
|
+
if (!s) return;
|
|
4170
|
+
db.exec("BEGIN IMMEDIATE");
|
|
4171
|
+
try {
|
|
4172
|
+
db.prepare(
|
|
4173
|
+
`INSERT INTO messages (session_id, role, content, timestamp, active, platform_message_id) VALUES (?, 'assistant', '[voice] __probe__', ?, 1, ?)`
|
|
4174
|
+
).run(s.id, Date.now() / 1e3, `__probe__${Date.now()}`);
|
|
4175
|
+
} finally {
|
|
4176
|
+
db.exec("ROLLBACK");
|
|
4177
|
+
}
|
|
4178
|
+
} finally {
|
|
4179
|
+
db?.close();
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
};
|
|
4183
|
+
|
|
4184
|
+
// src/memory/hermes-provider.ts
|
|
4185
|
+
var DEFAULT_K = 30;
|
|
4186
|
+
var HermesMemoryProvider = class {
|
|
4187
|
+
constructor(baseDir) {
|
|
4188
|
+
this.baseDir = baseDir;
|
|
4189
|
+
this.store = new HermesStore(baseDir);
|
|
4190
|
+
try {
|
|
4191
|
+
this.store.probeWrite();
|
|
4192
|
+
} catch (e) {
|
|
4193
|
+
process.stderr.write(`[bridge] HERMES MEMORY WRITE-BACK NON-FUNCTIONAL (schema probe failed): ${String(e)}
|
|
4194
|
+
`);
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
store;
|
|
4198
|
+
noSession = 0;
|
|
4199
|
+
skipped = 0;
|
|
4200
|
+
writeFailed = 0;
|
|
4201
|
+
async record(input) {
|
|
4202
|
+
if (input.modality !== "voice") return { seq: 0, deduped: true };
|
|
4203
|
+
const sessionId = resolveHermesSession(this.baseDir, input.conversationId);
|
|
4204
|
+
if (!sessionId) {
|
|
4205
|
+
this.noSession++;
|
|
4206
|
+
return { seq: 0, deduped: false };
|
|
4207
|
+
}
|
|
4208
|
+
const role = input.role === "peer" ? "user" : "assistant";
|
|
4209
|
+
const res = this.store.writeVoiceTurn({
|
|
4210
|
+
sessionId,
|
|
4211
|
+
role,
|
|
4212
|
+
content: `[voice] ${input.text}`,
|
|
4213
|
+
dedupKey: input.idempotencyKey ?? input.messageId ?? null,
|
|
4214
|
+
timestamp: Date.now() / 1e3
|
|
4215
|
+
});
|
|
4216
|
+
if (res === "failed") this.writeFailed++;
|
|
4217
|
+
else if (res === "skipped") this.skipped++;
|
|
4218
|
+
return { seq: 0, deduped: res === "deduped" };
|
|
4219
|
+
}
|
|
4220
|
+
async context(conversationId, limit) {
|
|
4221
|
+
const sessionId = resolveHermesSession(this.baseDir, conversationId);
|
|
4222
|
+
if (!sessionId) return { recent: [], facts: [] };
|
|
4223
|
+
const turns = this.store.readRecent(sessionId, limit ?? DEFAULT_K);
|
|
4224
|
+
return {
|
|
4225
|
+
recent: turns.map((t) => ({ seq: t.id, role: t.role === "user" ? "peer" : "agent", text: t.content })),
|
|
4226
|
+
facts: []
|
|
4227
|
+
};
|
|
4228
|
+
}
|
|
4229
|
+
stats() {
|
|
4230
|
+
return { hermes_no_session: this.noSession, hermes_skipped: this.skipped, hermes_write_failed: this.writeFailed };
|
|
4231
|
+
}
|
|
4232
|
+
};
|
|
4233
|
+
|
|
4234
|
+
// src/memory/openclaw-session-map.ts
|
|
4235
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
4236
|
+
import { join as join5 } from "node:path";
|
|
4237
|
+
function resolveOwnerDmSession(baseDir, agent) {
|
|
4238
|
+
try {
|
|
4239
|
+
const path = join5(expandHome(baseDir), "agents", agent, "sessions", "sessions.json");
|
|
4240
|
+
const map = JSON.parse(readFileSync3(path, "utf8"));
|
|
4241
|
+
let best = null;
|
|
4242
|
+
for (const [key, entry] of Object.entries(map)) {
|
|
4243
|
+
if (!key.includes(":direct:") || !key.endsWith(":owner")) continue;
|
|
4244
|
+
const sid = entry?.sessionId;
|
|
4245
|
+
if (typeof sid !== "string" || !sid) continue;
|
|
4246
|
+
const updatedAt = typeof entry?.updatedAt === "number" ? entry.updatedAt : 0;
|
|
4247
|
+
if (!best || updatedAt > best.updatedAt) best = { sid, updatedAt };
|
|
4248
|
+
}
|
|
4249
|
+
return best?.sid ?? null;
|
|
4250
|
+
} catch {
|
|
4251
|
+
return null;
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
// src/memory/openclaw-session-log.ts
|
|
4256
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
4257
|
+
import { join as join6 } from "node:path";
|
|
4258
|
+
function readRecentTurns(baseDir, agent, sessionId, k) {
|
|
4259
|
+
let raw;
|
|
4260
|
+
try {
|
|
4261
|
+
raw = readFileSync4(join6(expandHome(baseDir), "agents", agent, "sessions", `${sessionId}.jsonl`), "utf8");
|
|
4262
|
+
} catch {
|
|
4263
|
+
return [];
|
|
4264
|
+
}
|
|
4265
|
+
const turns = [];
|
|
4266
|
+
for (const line of raw.split("\n")) {
|
|
4267
|
+
if (!line.trim()) continue;
|
|
4268
|
+
let o;
|
|
4269
|
+
try {
|
|
4270
|
+
o = JSON.parse(line);
|
|
4271
|
+
} catch {
|
|
4272
|
+
continue;
|
|
4273
|
+
}
|
|
4274
|
+
if (o?.type !== "message" || !o.message) continue;
|
|
4275
|
+
const ts = typeof o.timestamp === "string" ? Date.parse(o.timestamp) : NaN;
|
|
4276
|
+
if (Number.isNaN(ts)) continue;
|
|
4277
|
+
const role = o.message.role;
|
|
4278
|
+
if (role === "user") {
|
|
4279
|
+
const text = typeof o.message.content === "string" ? o.message.content.trim() : "";
|
|
4280
|
+
if (text) turns.push({ role: "peer", text, ts });
|
|
4281
|
+
} else if (role === "assistant") {
|
|
4282
|
+
const blocks = Array.isArray(o.message.content) ? o.message.content : [];
|
|
4283
|
+
const text = blocks.filter((b) => !!b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n").trim();
|
|
4284
|
+
if (text) turns.push({ role: "agent", text, ts });
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
return k > 0 ? turns.slice(-k) : [];
|
|
4288
|
+
}
|
|
4289
|
+
|
|
4290
|
+
// src/memory/openclaw-provider.ts
|
|
4291
|
+
var DEFAULT_K2 = 200;
|
|
4292
|
+
var OpenClawMemoryProvider = class {
|
|
4293
|
+
constructor(store, baseDir, agent) {
|
|
4294
|
+
this.store = store;
|
|
4295
|
+
this.baseDir = baseDir;
|
|
4296
|
+
this.agent = agent;
|
|
4297
|
+
}
|
|
4298
|
+
noSession = 0;
|
|
4299
|
+
readFailed = 0;
|
|
4300
|
+
async record(input) {
|
|
4301
|
+
if (input.modality !== "voice") return { seq: 0, deduped: true };
|
|
4302
|
+
return this.store.recordMemory({ ...input, text: `[voice] ${input.text}` });
|
|
4303
|
+
}
|
|
4304
|
+
async context(conversationId, limit) {
|
|
4305
|
+
const k = limit ?? DEFAULT_K2;
|
|
4306
|
+
const voice = await this.store.memoryContext(conversationId, k);
|
|
4307
|
+
let textTurns = [];
|
|
4308
|
+
try {
|
|
4309
|
+
const sid = resolveOwnerDmSession(this.baseDir, this.agent);
|
|
4310
|
+
if (!sid) {
|
|
4311
|
+
this.noSession++;
|
|
4312
|
+
return voice;
|
|
4313
|
+
}
|
|
4314
|
+
textTurns = readRecentTurns(this.baseDir, this.agent, sid, k).map((t) => ({
|
|
4315
|
+
seq: 0,
|
|
4316
|
+
role: t.role,
|
|
4317
|
+
text: t.text,
|
|
4318
|
+
ts: t.ts
|
|
4319
|
+
}));
|
|
4320
|
+
} catch {
|
|
4321
|
+
this.readFailed++;
|
|
4322
|
+
return voice;
|
|
4323
|
+
}
|
|
4324
|
+
const tagged = [
|
|
4325
|
+
...textTurns.map((t, i) => ({ ...t, _src: 0, _i: i })),
|
|
4326
|
+
...voice.recent.map((t, i) => ({ ...t, _src: 1, _i: i }))
|
|
4327
|
+
];
|
|
4328
|
+
tagged.sort((a, b) => {
|
|
4329
|
+
const at = a.ts ?? 0, bt = b.ts ?? 0;
|
|
4330
|
+
if (at !== bt) return at - bt;
|
|
4331
|
+
if (a._src !== b._src) return a._src - b._src;
|
|
4332
|
+
return a._i - b._i;
|
|
4333
|
+
});
|
|
4334
|
+
const recent = (k > 0 ? tagged.slice(-k) : []).map((t, i) => ({
|
|
4335
|
+
seq: i + 1,
|
|
4336
|
+
role: t.role,
|
|
4337
|
+
peerId: t.peerId,
|
|
4338
|
+
text: t.text,
|
|
4339
|
+
ts: t.ts
|
|
4340
|
+
}));
|
|
4341
|
+
return { recent, facts: voice.facts };
|
|
4342
|
+
}
|
|
4343
|
+
stats() {
|
|
4344
|
+
return { openclaw_no_session: this.noSession, openclaw_read_failed: this.readFailed };
|
|
4345
|
+
}
|
|
4346
|
+
};
|
|
4347
|
+
|
|
4348
|
+
// src/memory/provider.ts
|
|
4349
|
+
var SecureComsLocalMemory = class {
|
|
4350
|
+
constructor(store) {
|
|
4351
|
+
this.store = store;
|
|
4352
|
+
}
|
|
4353
|
+
record(input) {
|
|
4354
|
+
return this.store.recordMemory(input);
|
|
4355
|
+
}
|
|
4356
|
+
context(conversationId, limit) {
|
|
4357
|
+
return this.store.memoryContext(conversationId, limit);
|
|
4358
|
+
}
|
|
4359
|
+
};
|
|
4360
|
+
var NullMemory = class {
|
|
4361
|
+
async record() {
|
|
4362
|
+
return { seq: 0, deduped: false };
|
|
4363
|
+
}
|
|
4364
|
+
async context() {
|
|
4365
|
+
return { recent: [], facts: [] };
|
|
4366
|
+
}
|
|
4367
|
+
};
|
|
4368
|
+
function createMemoryProvider(provider, store, baseDir, agent) {
|
|
4369
|
+
if (provider === "none") return new NullMemory();
|
|
4370
|
+
if (provider === "hermes") return new HermesMemoryProvider(baseDir ?? "~/.hermes");
|
|
4371
|
+
if (provider === "openclaw") return new OpenClawMemoryProvider(store, baseDir ?? "~/.openclaw", agent ?? "");
|
|
4372
|
+
return new SecureComsLocalMemory(store);
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
// src/memory-taps.ts
|
|
4376
|
+
async function recordInboundMemory(sink, channelId, senderId, text, messageId) {
|
|
4377
|
+
try {
|
|
4378
|
+
await sink.record({ conversationId: channelId, role: "peer", peerId: senderId, text, modality: "text", messageId });
|
|
4379
|
+
} catch {
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
async function recordOutboundMemory(sink, channelId, text, messageId) {
|
|
4383
|
+
try {
|
|
4384
|
+
await sink.record({ conversationId: channelId, role: "agent", text, modality: "text", messageId });
|
|
4385
|
+
} catch {
|
|
4386
|
+
}
|
|
3739
4387
|
}
|
|
3740
4388
|
|
|
3741
4389
|
// src/bridge-agent.ts
|
|
@@ -3812,6 +4460,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
3812
4460
|
identityId: cfg.identityId,
|
|
3813
4461
|
cipherFor: () => new KeyedInboxCipher(deriveMlsStoreKey(seed))
|
|
3814
4462
|
});
|
|
4463
|
+
this.memory = createMemoryProvider(this.cfg.memoryProvider, this.store, this.cfg.memoryBaseDir, this.cfg.memoryAgent);
|
|
3815
4464
|
this.provider = new MlsGroupKeyProvider({
|
|
3816
4465
|
store: this.store,
|
|
3817
4466
|
backend: this.backend,
|
|
@@ -3891,7 +4540,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
3891
4540
|
const j = await res.json();
|
|
3892
4541
|
return String(j.attachment_id);
|
|
3893
4542
|
};
|
|
3894
|
-
this.attachmentsDirImpl = deps.attachmentsDir ??
|
|
4543
|
+
this.attachmentsDirImpl = deps.attachmentsDir ?? join7(dirname3(cfg.storePath), "attachments");
|
|
3895
4544
|
this.action = {
|
|
3896
4545
|
begin: (channelId) => this.actionBegin(channelId),
|
|
3897
4546
|
start: (opts) => this.actionStart(opts),
|
|
@@ -4063,6 +4712,13 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4063
4712
|
listeners = /* @__PURE__ */ new Set();
|
|
4064
4713
|
startedAt = 0;
|
|
4065
4714
|
store;
|
|
4715
|
+
memory;
|
|
4716
|
+
/** Task 2: warn-once-per-channel guard for the enclave-map fallback (below). */
|
|
4717
|
+
enclaveFallbackWarned = /* @__PURE__ */ new Set();
|
|
4718
|
+
/** Task 2: dedupe guard so ingestOnce writes a channel's enclave at most once
|
|
4719
|
+
* per process — a channel's enclave is immutable, so no need to UPSERT on
|
|
4720
|
+
* every message on the hot receive path (audit M2). */
|
|
4721
|
+
channelEnclaveRecorded = /* @__PURE__ */ new Set();
|
|
4066
4722
|
latency;
|
|
4067
4723
|
/** Track 5: injectable blob download fn (default: fetch-based). */
|
|
4068
4724
|
downloadBlobImpl;
|
|
@@ -4102,6 +4758,26 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4102
4758
|
sweepJitter() {
|
|
4103
4759
|
return Math.floor(this.rng() * _BridgeAgent.SELF_HEAL_SWEEP_JITTER_MS);
|
|
4104
4760
|
}
|
|
4761
|
+
/**
|
|
4762
|
+
* Task 2 (multi-enclave core fix): resolve the enclave to stamp on outbound
|
|
4763
|
+
* traffic for `channelId`. The daemon previously stamped `cfg.enclaveId`
|
|
4764
|
+
* (its "home" enclave) on EVERY channel, so an agent admitted to a second
|
|
4765
|
+
* enclave got a backend 400 on every send/reply there. The channel→enclave
|
|
4766
|
+
* map (`channel_meta`, Task 1) is fed by inbound envelopes (`ingestOnce`)
|
|
4767
|
+
* and by `listChannels`; a channel not yet in the map (never seen inbound,
|
|
4768
|
+
* never discovered) falls back to home and logs once so the fallback is
|
|
4769
|
+
* diagnosable (audit H3) rather than silently wrong.
|
|
4770
|
+
*/
|
|
4771
|
+
resolveEnclave(channelId) {
|
|
4772
|
+
const mapped = this.store.getChannelMeta(channelId)?.enclaveId;
|
|
4773
|
+
if (mapped) return mapped;
|
|
4774
|
+
if (!this.enclaveFallbackWarned.has(channelId)) {
|
|
4775
|
+
this.enclaveFallbackWarned.add(channelId);
|
|
4776
|
+
process.stderr.write(`[bridge] send.enclave_fallback channel=${channelId} stamped=home
|
|
4777
|
+
`);
|
|
4778
|
+
}
|
|
4779
|
+
return this.cfg.enclaveId;
|
|
4780
|
+
}
|
|
4105
4781
|
async reconcileJoin(channelId) {
|
|
4106
4782
|
if (this.joinState.get(channelId) === "JOINED") return true;
|
|
4107
4783
|
try {
|
|
@@ -4315,7 +4991,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4315
4991
|
groupKey: this.provider,
|
|
4316
4992
|
messageId: id,
|
|
4317
4993
|
channelId,
|
|
4318
|
-
enclaveId: this.
|
|
4994
|
+
enclaveId: this.resolveEnclave(channelId),
|
|
4319
4995
|
senderId: this.cfg.identityId,
|
|
4320
4996
|
senderType: "agent",
|
|
4321
4997
|
type: "text",
|
|
@@ -4333,6 +5009,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4333
5009
|
(envelope) => this.latency.time("send.publish", () => this.transport.publish(envelope))
|
|
4334
5010
|
);
|
|
4335
5011
|
this.latency.record("send.total", performance.now() - sendStart);
|
|
5012
|
+
void recordOutboundMemory(this.memory, channelId, text, id);
|
|
4336
5013
|
return { message_id: id, status: "sent" };
|
|
4337
5014
|
}
|
|
4338
5015
|
async sendA2A(channelId, a2a, messageId) {
|
|
@@ -4351,7 +5028,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4351
5028
|
groupKey: this.provider,
|
|
4352
5029
|
messageId: id,
|
|
4353
5030
|
channelId,
|
|
4354
|
-
enclaveId: this.
|
|
5031
|
+
enclaveId: this.resolveEnclave(channelId),
|
|
4355
5032
|
senderId: this.cfg.identityId,
|
|
4356
5033
|
senderType: "agent",
|
|
4357
5034
|
type: "a2a",
|
|
@@ -4404,7 +5081,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4404
5081
|
groupKey: this.provider,
|
|
4405
5082
|
messageId: id,
|
|
4406
5083
|
channelId,
|
|
4407
|
-
enclaveId: this.
|
|
5084
|
+
enclaveId: this.resolveEnclave(channelId),
|
|
4408
5085
|
senderId: this.cfg.identityId,
|
|
4409
5086
|
senderType: "agent",
|
|
4410
5087
|
type: "artifact_share",
|
|
@@ -4424,42 +5101,91 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4424
5101
|
this.latency.record("send.total", performance.now() - sendStart);
|
|
4425
5102
|
return { message_id: id, status: "sent" };
|
|
4426
5103
|
}
|
|
4427
|
-
|
|
4428
|
-
|
|
5104
|
+
/** Task 3: delegates to the configured MemoryProvider (SecureComsLocal | Null). */
|
|
5105
|
+
async recordMemory(input) {
|
|
5106
|
+
return this.memory.record(input);
|
|
5107
|
+
}
|
|
5108
|
+
async memoryContext(conversationId, limit) {
|
|
5109
|
+
return this.memory.context(conversationId, limit);
|
|
5110
|
+
}
|
|
5111
|
+
/**
|
|
5112
|
+
* List channels in one enclave (default: the daemon's own home enclave).
|
|
5113
|
+
* Task 3: called per-*known* enclave by refreshChannelMeta so a
|
|
5114
|
+
* cross-enclave room's is_dm gets resolved — see the scope caveat there
|
|
5115
|
+
* (same-tenant only; a cross-tenant enclave 404s under the agent's bearer).
|
|
5116
|
+
*/
|
|
5117
|
+
async listChannels(enclaveId = this.cfg.enclaveId) {
|
|
5118
|
+
const url = `${this.cfg.apiBase}/api/v1/enclaves/${enclaveId}/channels?caller_identity_id=${encodeURIComponent(this.cfg.identityId)}`;
|
|
4429
5119
|
const res = await this.fetchImpl(url, {
|
|
4430
5120
|
headers: { authorization: `Bearer ${this.cfg.apiKey}` }
|
|
4431
5121
|
});
|
|
4432
5122
|
if (!res.ok) throw new Error(`list_channels failed: ${res.status}`);
|
|
4433
5123
|
const rows = await res.json();
|
|
5124
|
+
for (const r of rows) this.store.recordChannelEnclave(r.channel_id, r.enclave_id);
|
|
4434
5125
|
return rows.map((r) => ({
|
|
4435
5126
|
channel_id: r.channel_id,
|
|
4436
5127
|
name: r.name ?? null,
|
|
4437
5128
|
is_dm: r.is_dm,
|
|
4438
|
-
joined: true
|
|
5129
|
+
joined: true,
|
|
5130
|
+
enclave_id: r.enclave_id
|
|
4439
5131
|
}));
|
|
4440
5132
|
}
|
|
4441
|
-
/**
|
|
5133
|
+
/**
|
|
5134
|
+
* A live channelMeta entry, or undefined if missing OR expired past the
|
|
5135
|
+
* TTL. A never-cached-this-process channel (no in-memory entry at all —
|
|
5136
|
+
* cold start, or every listChannels() call in refreshChannelMeta failed,
|
|
5137
|
+
* e.g. network down) falls back to the store's persisted is_dm (Task 3) so
|
|
5138
|
+
* a cross-enclave room stays classified across a restart instead of
|
|
5139
|
+
* reverting to UNKNOWN/fail-open. The store has no TTL (is_dm is immutable
|
|
5140
|
+
* per channel); `name` is not persisted so it comes back undefined on this
|
|
5141
|
+
* path and re-resolves once a listChannels() call succeeds.
|
|
5142
|
+
*
|
|
5143
|
+
* A TTL-*expired* in-memory entry deliberately does NOT use the store
|
|
5144
|
+
* fallback and is still treated as a miss — that miss is what drives
|
|
5145
|
+
* ingestOnce to retry a refresh each cycle (S-2: keeping `name` fresh after
|
|
5146
|
+
* a rename); serving the persisted is_dm there would silently suppress
|
|
5147
|
+
* that retry forever.
|
|
5148
|
+
*/
|
|
4442
5149
|
freshMeta(channelId) {
|
|
4443
5150
|
const hit = this.channelMeta.get(channelId);
|
|
4444
|
-
if (
|
|
4445
|
-
|
|
4446
|
-
|
|
5151
|
+
if (hit) {
|
|
5152
|
+
if (Date.now() - hit.at < CHANNEL_META_TTL_MS) return { is_dm: hit.is_dm, name: hit.name };
|
|
5153
|
+
return void 0;
|
|
5154
|
+
}
|
|
5155
|
+
const persisted = this.store.getChannelMeta(channelId);
|
|
5156
|
+
if (persisted?.isDm !== void 0) {
|
|
5157
|
+
return { is_dm: persisted.isDm, name: void 0 };
|
|
5158
|
+
}
|
|
5159
|
+
return void 0;
|
|
4447
5160
|
}
|
|
4448
5161
|
/**
|
|
4449
|
-
*
|
|
4450
|
-
*
|
|
4451
|
-
*
|
|
4452
|
-
*
|
|
4453
|
-
*
|
|
5162
|
+
* Whole-listing refresh of channelMeta, one listChannels() call per *known*
|
|
5163
|
+
* enclave — the daemon's own home enclave plus every enclave it has learned
|
|
5164
|
+
* about from inbound envelopes or a prior discovery (store.distinctEnclaveIds()).
|
|
5165
|
+
* Called AT MOST ONCE per ingest cycle (the caller guards with a
|
|
5166
|
+
* cycle-local "refreshed" flag). Task 3 (M1): without this, a cross-enclave
|
|
5167
|
+
* room is never listed under the home-enclave-only call, so its is_dm never
|
|
5168
|
+
* resolves and the @mention filter fails open for that room.
|
|
5169
|
+
*
|
|
5170
|
+
* Each enclave is wrapped in its own try/catch (scope caveat — audit
|
|
5171
|
+
* H2/tenant): list_channels_in_enclave filters on tenant_id, so a
|
|
5172
|
+
* same-tenant enclave the agent has learned about resolves fine, but a
|
|
5173
|
+
* cross-TENANT federated-partner enclave 404s under this agent's bearer;
|
|
5174
|
+
* that enclave's channels are left unresolved (fail open) without aborting
|
|
5175
|
+
* the other enclaves in the set.
|
|
4454
5176
|
*/
|
|
4455
5177
|
async refreshChannelMeta() {
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
5178
|
+
const enclaves = /* @__PURE__ */ new Set([this.cfg.enclaveId, ...this.store.distinctEnclaveIds()]);
|
|
5179
|
+
const now = Date.now();
|
|
5180
|
+
for (const encId of enclaves) {
|
|
5181
|
+
try {
|
|
5182
|
+
const channels = await this.listChannels(encId);
|
|
5183
|
+
for (const c of channels) {
|
|
5184
|
+
this.channelMeta.set(c.channel_id, { is_dm: c.is_dm, name: c.name, at: now });
|
|
5185
|
+
this.store.recordChannelIsDm(c.channel_id, c.is_dm);
|
|
5186
|
+
}
|
|
5187
|
+
} catch {
|
|
4461
5188
|
}
|
|
4462
|
-
} catch {
|
|
4463
5189
|
}
|
|
4464
5190
|
}
|
|
4465
5191
|
/** A live roster for a channel, or undefined if missing OR expired past the TTL. */
|
|
@@ -4565,6 +5291,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4565
5291
|
async reportStatus(channelId, state, detail) {
|
|
4566
5292
|
const joined = await this.reconcileJoin(channelId);
|
|
4567
5293
|
if (!joined) throw new ChannelPendingError(channelId);
|
|
5294
|
+
const enc = this.store.getChannelMeta(channelId)?.enclaveId;
|
|
5295
|
+
if (!enc) return;
|
|
4568
5296
|
await sendWithSelfHeal(
|
|
4569
5297
|
this.provider,
|
|
4570
5298
|
channelId,
|
|
@@ -4573,7 +5301,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4573
5301
|
groupKey: this.provider,
|
|
4574
5302
|
messageId: crypto.randomUUID(),
|
|
4575
5303
|
channelId,
|
|
4576
|
-
enclaveId:
|
|
5304
|
+
enclaveId: enc,
|
|
4577
5305
|
senderId: this.cfg.identityId,
|
|
4578
5306
|
senderType: "agent",
|
|
4579
5307
|
type: "system_event",
|
|
@@ -4620,7 +5348,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4620
5348
|
groupKey: this.provider,
|
|
4621
5349
|
messageId: crypto.randomUUID(),
|
|
4622
5350
|
channelId,
|
|
4623
|
-
enclaveId: this.
|
|
5351
|
+
enclaveId: this.resolveEnclave(channelId),
|
|
4624
5352
|
senderId: this.cfg.identityId,
|
|
4625
5353
|
senderType: "agent",
|
|
4626
5354
|
type: "system_event",
|
|
@@ -4864,6 +5592,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4864
5592
|
const acked = [];
|
|
4865
5593
|
for (const m of entries) {
|
|
4866
5594
|
try {
|
|
5595
|
+
const encId = m.envelope.enclave_id;
|
|
5596
|
+
if (encId && !this.channelEnclaveRecorded.has(m.channelId)) {
|
|
5597
|
+
this.channelEnclaveRecorded.add(m.channelId);
|
|
5598
|
+
this.store.recordChannelEnclave(m.channelId, encId);
|
|
5599
|
+
}
|
|
4867
5600
|
const parsed = JSON.parse(
|
|
4868
5601
|
new TextDecoder().decode(m.plaintext)
|
|
4869
5602
|
);
|
|
@@ -4935,6 +5668,7 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
4935
5668
|
sender_is_agent: senderIsAgent
|
|
4936
5669
|
});
|
|
4937
5670
|
this.capBuffer();
|
|
5671
|
+
void recordInboundMemory(this.memory, m.channelId, m.envelope.sender_id, parsed.text, m.messageId);
|
|
4938
5672
|
}
|
|
4939
5673
|
}
|
|
4940
5674
|
acked.push(m.queueId);
|
|
@@ -5031,7 +5765,8 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
5031
5765
|
failed_attachments: this.failedAttachments,
|
|
5032
5766
|
delivery_pings: this.pingsReceived,
|
|
5033
5767
|
uptime_s: this.startedAt ? Math.floor((Date.now() - this.startedAt) / 1e3) : 0,
|
|
5034
|
-
version: BRIDGE_VERSION
|
|
5768
|
+
version: BRIDGE_VERSION,
|
|
5769
|
+
...this.memory.stats?.() ?? {}
|
|
5035
5770
|
};
|
|
5036
5771
|
}
|
|
5037
5772
|
/**
|
|
@@ -5087,11 +5822,11 @@ var BridgeAgent = class _BridgeAgent {
|
|
|
5087
5822
|
|
|
5088
5823
|
// src/auth.ts
|
|
5089
5824
|
import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
|
|
5090
|
-
import { existsSync, readFileSync as
|
|
5825
|
+
import { existsSync, readFileSync as readFileSync5, writeFileSync } from "node:fs";
|
|
5091
5826
|
function resolveToken(opts) {
|
|
5092
5827
|
if (opts.token) return opts.token;
|
|
5093
5828
|
const f = opts.tokenFile;
|
|
5094
|
-
if (f && existsSync(f)) return
|
|
5829
|
+
if (f && existsSync(f)) return readFileSync5(f, "utf8").trim();
|
|
5095
5830
|
const tok = randomBytes3(32).toString("base64url");
|
|
5096
5831
|
if (f) writeFileSync(f, tok, { mode: 384 });
|
|
5097
5832
|
return tok;
|
|
@@ -5206,6 +5941,17 @@ var statusSchema = z.object({
|
|
|
5206
5941
|
state: z.enum(["idle", "working", "stalled", "error"]),
|
|
5207
5942
|
detail: z.string().optional()
|
|
5208
5943
|
});
|
|
5944
|
+
var memoryRecordSchema = z.object({
|
|
5945
|
+
conversation_id: z.string().min(1),
|
|
5946
|
+
role: z.enum(["peer", "agent"]),
|
|
5947
|
+
peer_id: z.string().optional(),
|
|
5948
|
+
text: z.string(),
|
|
5949
|
+
modality: z.enum(["text", "voice"]),
|
|
5950
|
+
message_id: z.string().optional(),
|
|
5951
|
+
idempotency_key: z.string().optional()
|
|
5952
|
+
}).refine((d) => Boolean(d.message_id || d.idempotency_key), {
|
|
5953
|
+
message: "memory record requires message_id or idempotency_key"
|
|
5954
|
+
});
|
|
5209
5955
|
function json(res, status, body) {
|
|
5210
5956
|
const s = JSON.stringify(body);
|
|
5211
5957
|
res.writeHead(status, { "content-type": "application/json" });
|
|
@@ -5299,6 +6045,26 @@ function createHttpServer(agent, opts) {
|
|
|
5299
6045
|
await agent.reportStatus(parsed.data.channel_id, parsed.data.state, parsed.data.detail);
|
|
5300
6046
|
return json(res, 200, { ok: true });
|
|
5301
6047
|
}
|
|
6048
|
+
if (method === "POST" && path === "/v1/memory/record") {
|
|
6049
|
+
const parsed = memoryRecordSchema.safeParse(JSON.parse(await readBody(req, OTHER_CAP)));
|
|
6050
|
+
if (!parsed.success) return jsonError(res, "invalid_request", parsed.error.issues);
|
|
6051
|
+
const r = await agent.recordMemory({
|
|
6052
|
+
conversationId: parsed.data.conversation_id,
|
|
6053
|
+
role: parsed.data.role,
|
|
6054
|
+
peerId: parsed.data.peer_id,
|
|
6055
|
+
text: parsed.data.text,
|
|
6056
|
+
modality: parsed.data.modality,
|
|
6057
|
+
messageId: parsed.data.message_id,
|
|
6058
|
+
idempotencyKey: parsed.data.idempotency_key
|
|
6059
|
+
});
|
|
6060
|
+
return json(res, 200, r);
|
|
6061
|
+
}
|
|
6062
|
+
if (method === "GET" && path === "/v1/memory/context") {
|
|
6063
|
+
const conv = url.searchParams.get("conversation_id");
|
|
6064
|
+
if (!conv) return jsonError(res, "invalid_request", "conversation_id required");
|
|
6065
|
+
const limit = Math.max(1, Math.min(200, Number(url.searchParams.get("limit") ?? 50)));
|
|
6066
|
+
return json(res, 200, await agent.memoryContext(conv, limit));
|
|
6067
|
+
}
|
|
5302
6068
|
if (method === "GET" && path === "/v1/messages") {
|
|
5303
6069
|
const channel = url.searchParams.get("channel_id") ?? void 0;
|
|
5304
6070
|
const wait = Math.max(0, Math.min(50, Number(url.searchParams.get("wait") ?? 0)));
|
|
@@ -5629,10 +6395,10 @@ async function handleInbound(sock, agent, data, inflight) {
|
|
|
5629
6395
|
}
|
|
5630
6396
|
|
|
5631
6397
|
// src/bin.ts
|
|
5632
|
-
import { readFileSync as
|
|
5633
|
-
import { join as
|
|
6398
|
+
import { readFileSync as readFileSync6, mkdirSync, writeFileSync as writeFileSync2, rmSync, existsSync as existsSync2, realpathSync } from "node:fs";
|
|
6399
|
+
import { join as join8, dirname as dirname5 } from "node:path";
|
|
5634
6400
|
import { fileURLToPath } from "node:url";
|
|
5635
|
-
import { homedir, userInfo } from "node:os";
|
|
6401
|
+
import { homedir as homedir2, userInfo } from "node:os";
|
|
5636
6402
|
import { spawnSync } from "node:child_process";
|
|
5637
6403
|
|
|
5638
6404
|
// src/service/install.ts
|
|
@@ -5826,7 +6592,7 @@ function runServiceVerb(verb) {
|
|
|
5826
6592
|
label,
|
|
5827
6593
|
execPath: process.execPath,
|
|
5828
6594
|
binScript,
|
|
5829
|
-
home:
|
|
6595
|
+
home: homedir2(),
|
|
5830
6596
|
uid: userInfo().uid
|
|
5831
6597
|
};
|
|
5832
6598
|
try {
|
|
@@ -5878,11 +6644,11 @@ async function main() {
|
|
|
5878
6644
|
`);
|
|
5879
6645
|
process.exit(1);
|
|
5880
6646
|
}
|
|
5881
|
-
const tokenFile = daemon.tokenFile ??
|
|
6647
|
+
const tokenFile = daemon.tokenFile ?? join8(dirname5(credentialPath), "bridge-token");
|
|
5882
6648
|
const token = resolveToken({ tokenFile, token: daemon.token });
|
|
5883
6649
|
const agent = new BridgeAgent(cfg);
|
|
5884
6650
|
await agent.start();
|
|
5885
|
-
const tls = daemon.tls ? { cert:
|
|
6651
|
+
const tls = daemon.tls ? { cert: readFileSync6(daemon.tlsCert, "utf8"), key: readFileSync6(daemon.tlsKey, "utf8") } : void 0;
|
|
5886
6652
|
const { server, closeAllSseStreams } = createHttpServer(agent, { token, allowOrigins: daemon.allowOrigins, tls, websocket: true });
|
|
5887
6653
|
const agentWs = attachAgentWebSocket(server, agent, { token, allowOrigins: daemon.allowOrigins });
|
|
5888
6654
|
let down = false;
|