@securecoms/agent-bridge 0.3.1 → 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.
Files changed (45) hide show
  1. package/README.md +11 -1
  2. package/dist/bin.js +892 -118
  3. package/dist/bridge-agent.d.ts +69 -8
  4. package/dist/config.d.ts +6 -0
  5. package/dist/http-server.d.ts +42 -0
  6. package/dist/index.js +844 -62
  7. package/dist/memory/hermes-provider.d.ts +21 -0
  8. package/dist/memory/hermes-session-map.d.ts +8 -0
  9. package/dist/memory/hermes-store.d.ts +32 -0
  10. package/dist/memory/hermes-test-fixture.d.ts +5 -0
  11. package/dist/memory/openclaw-provider.d.ts +24 -0
  12. package/dist/memory/openclaw-session-log.d.ts +15 -0
  13. package/dist/memory/openclaw-session-map.d.ts +7 -0
  14. package/dist/memory/provider.d.ts +29 -0
  15. package/dist/memory-taps.d.ts +13 -0
  16. package/dist/preflight-modern.cjs +35 -0
  17. package/dist/preflight-modern.d.cts +1 -0
  18. package/dist/preflight-upgrade.d.cts +30 -0
  19. package/dist/preflight.cjs +277 -0
  20. package/dist/preflight.d.cts +14 -0
  21. package/package.json +3 -5
  22. package/dist/attachments.js +0 -49
  23. package/dist/auth.js +0 -56
  24. package/dist/bin.js.pre-c1-f803007 +0 -5795
  25. package/dist/bin.js.pre-c3-096a9a3 +0 -5832
  26. package/dist/bin.js.pre-ffrebuild-b162d4a +0 -5639
  27. package/dist/bin.js.pre-kpheal-20260708-150924 +0 -5531
  28. package/dist/bin.js.pre-kppersist-20260708-152341 +0 -5543
  29. package/dist/bin.js.pre-p0-20260708-102415 +0 -5196
  30. package/dist/bin.js.pre-p1b-02f47e1 +0 -5639
  31. package/dist/bin.js.pre-warmfix-20260708-153705 +0 -5613
  32. package/dist/bin.js.pretrace-113205 +0 -5395
  33. package/dist/bridge-agent.js +0 -831
  34. package/dist/config.js +0 -74
  35. package/dist/coordination.js +0 -63
  36. package/dist/errors.js +0 -25
  37. package/dist/http-server.js +0 -220
  38. package/dist/index.js.pre-p0-20260708-102415 +0 -4879
  39. package/dist/latency.js +0 -87
  40. package/dist/rate-limit.js +0 -56
  41. package/dist/service/config.js +0 -26
  42. package/dist/service/install.js +0 -60
  43. package/dist/service/launchd.js +0 -30
  44. package/dist/service/systemd.js +0 -25
  45. 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 = bytesToHex(MLSGroupManager.serializeKeyPackage(bundle.publicPackage));
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 = bytesToHex(MLSGroupManager.serializeKeyPackage(bundle.publicPackage));
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: bytesToHex(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: bytesToHex(welcome) })) : []
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: bytesToHex(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: bytesToHex(welcome) })) : []
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: bytesToHex(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: bytesToHex(welcome) }] : []
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: bytesToHex(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 : bytesToHex(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: bytesToHex(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: bytesToHex(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: bytesToHex(welcome) })) : []
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 bytesToHex2, hexToBytes as hexToBytes2 } from "@agentvault/crypto";
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 sha2562 } from "@noble/hashes/sha256";
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(sha2562, identityKeyMaterial, void 0, MLS_STORE_INFO, 32);
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 sha2563 } from "@noble/hashes/sha256";
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 sha2564 } from "@noble/hashes/sha256";
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 bytesToHex3, hexToBytes as hexToBytes3 } from "@agentvault/crypto";
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 = 4;
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: bytesToHex3(MLSGroupManager2.serializeKeyPackage(bundle.publicPackage)),
3220
- initPriv: bytesToHex3(pp.initPrivateKey),
3221
- hpkePriv: bytesToHex3(pp.hpkePrivateKey),
3222
- sigPriv: bytesToHex3(pp.signaturePrivateKey)
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);
@@ -3395,8 +3722,59 @@ var SqliteMlsStateStore = class {
3395
3722
 
3396
3723
  // src/bridge-agent.ts
3397
3724
  import { decryptFile, encryptFile, computeFileDigest, base64ToBytes as base64ToBytes2, bytesToBase64 as bytesToBase642 } from "@agentvault/crypto";
3398
- import { promises as fsp } from "node:fs";
3725
+
3726
+ // src/service/resolve-bin.ts
3399
3727
  import { join as join2, dirname as dirname2 } from "node:path";
3728
+ import { createRequire } from "node:module";
3729
+ var GlobalInstallPermissionError = class extends Error {
3730
+ constructor(message) {
3731
+ super(message);
3732
+ this.name = "GlobalInstallPermissionError";
3733
+ }
3734
+ };
3735
+ function isEphemeralPath(p) {
3736
+ return /[\\/]_npx[\\/]/.test(p);
3737
+ }
3738
+ function refersToSameFile(a, b, realpath) {
3739
+ if (a === b) return true;
3740
+ try {
3741
+ return realpath(a) === realpath(b);
3742
+ } catch {
3743
+ return false;
3744
+ }
3745
+ }
3746
+ function resolveStableBinScript(input, deps) {
3747
+ const ephemeral = isEphemeralPath(input.binScript);
3748
+ if (!ephemeral) {
3749
+ if (deps.fileExists(input.binScript)) return input.binScript;
3750
+ throw new Error(`bin path does not exist and is not an npx cache path: ${input.binScript}`);
3751
+ }
3752
+ const pkgRoot = dirname2(dirname2(input.binScript));
3753
+ const inst = deps.run("npm", ["i", "-g", pkgRoot, "--install-links"]);
3754
+ if (inst.status !== 0) {
3755
+ if (/EACCES|permission denied/i.test(inst.stderr)) {
3756
+ throw new GlobalInstallPermissionError(
3757
+ `Can't write the global npm prefix. Run this once, then re-run install:
3758
+ npm i -g ${input.packageName}`
3759
+ );
3760
+ }
3761
+ throw new Error(`npm i -g ${pkgRoot} failed (exit ${inst.status})${inst.stderr ? `: ${inst.stderr.trim()}` : ""}`);
3762
+ }
3763
+ const root = deps.run("npm", ["root", "-g"]);
3764
+ if (root.status !== 0) throw new Error(`npm root -g failed (exit ${root.status})`);
3765
+ const stable = join2(root.stdout.trim(), input.packageName, "dist", "bin.js");
3766
+ if (!deps.fileExists(stable)) throw new Error(`global install did not produce ${stable}`);
3767
+ return stable;
3768
+ }
3769
+ function loadPackageMeta(importMetaUrl) {
3770
+ const require2 = createRequire(importMetaUrl);
3771
+ const pkg = require2("../package.json");
3772
+ return { name: pkg.name, version: pkg.version };
3773
+ }
3774
+
3775
+ // src/bridge-agent.ts
3776
+ import { promises as fsp } from "node:fs";
3777
+ import { join as join7, dirname as dirname3 } from "node:path";
3400
3778
 
3401
3779
  // src/latency.ts
3402
3780
  var DEFAULT_CAPACITY = 1024;
@@ -3670,8 +4048,8 @@ async function prepareAttachment(filePath, caption, channelId, deps) {
3670
4048
  }
3671
4049
  async function materializeAttachment(body, ctx, deps) {
3672
4050
  const ciphertext = await deps.downloadBlob(body.blobId);
3673
- const sha2565 = deps.computeFileDigest(ciphertext);
3674
- if (sha2565 !== body.digest) {
4051
+ const sha2566 = deps.computeFileDigest(ciphertext);
4052
+ if (sha2566 !== body.digest) {
3675
4053
  throw new Error(`attachment digest mismatch for blob ${body.blobId}`);
3676
4054
  }
3677
4055
  const plain = deps.decryptFile(
@@ -3684,7 +4062,328 @@ async function materializeAttachment(body, ctx, deps) {
3684
4062
  const path = `${dir}/${ctx.messageId}-${safeName}`;
3685
4063
  await deps.mkdirp(dir);
3686
4064
  await deps.writeFile(path, plain);
3687
- return { filename: safeName, mime: body.mime, size: plain.length, path, sha256: sha2565 };
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
+ }
3688
4387
  }
3689
4388
 
3690
4389
  // src/bridge-agent.ts
@@ -3729,7 +4428,13 @@ var UnknownActionRunError = class extends Error {
3729
4428
  this.name = "UnknownActionRunError";
3730
4429
  }
3731
4430
  };
3732
- var BRIDGE_VERSION = "0.0.0";
4431
+ var BRIDGE_VERSION = (() => {
4432
+ try {
4433
+ return loadPackageMeta(import.meta.url).version;
4434
+ } catch {
4435
+ return "0.0.0";
4436
+ }
4437
+ })();
3733
4438
  function hexToBytes4(h) {
3734
4439
  return new Uint8Array(Buffer.from(h, "hex"));
3735
4440
  }
@@ -3755,6 +4460,7 @@ var BridgeAgent = class _BridgeAgent {
3755
4460
  identityId: cfg.identityId,
3756
4461
  cipherFor: () => new KeyedInboxCipher(deriveMlsStoreKey(seed))
3757
4462
  });
4463
+ this.memory = createMemoryProvider(this.cfg.memoryProvider, this.store, this.cfg.memoryBaseDir, this.cfg.memoryAgent);
3758
4464
  this.provider = new MlsGroupKeyProvider({
3759
4465
  store: this.store,
3760
4466
  backend: this.backend,
@@ -3834,7 +4540,7 @@ var BridgeAgent = class _BridgeAgent {
3834
4540
  const j = await res.json();
3835
4541
  return String(j.attachment_id);
3836
4542
  };
3837
- this.attachmentsDirImpl = deps.attachmentsDir ?? join2(dirname2(cfg.storePath), "attachments");
4543
+ this.attachmentsDirImpl = deps.attachmentsDir ?? join7(dirname3(cfg.storePath), "attachments");
3838
4544
  this.action = {
3839
4545
  begin: (channelId) => this.actionBegin(channelId),
3840
4546
  start: (opts) => this.actionStart(opts),
@@ -4006,6 +4712,13 @@ var BridgeAgent = class _BridgeAgent {
4006
4712
  listeners = /* @__PURE__ */ new Set();
4007
4713
  startedAt = 0;
4008
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();
4009
4722
  latency;
4010
4723
  /** Track 5: injectable blob download fn (default: fetch-based). */
4011
4724
  downloadBlobImpl;
@@ -4045,6 +4758,26 @@ var BridgeAgent = class _BridgeAgent {
4045
4758
  sweepJitter() {
4046
4759
  return Math.floor(this.rng() * _BridgeAgent.SELF_HEAL_SWEEP_JITTER_MS);
4047
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
+ }
4048
4781
  async reconcileJoin(channelId) {
4049
4782
  if (this.joinState.get(channelId) === "JOINED") return true;
4050
4783
  try {
@@ -4258,7 +4991,7 @@ var BridgeAgent = class _BridgeAgent {
4258
4991
  groupKey: this.provider,
4259
4992
  messageId: id,
4260
4993
  channelId,
4261
- enclaveId: this.cfg.enclaveId,
4994
+ enclaveId: this.resolveEnclave(channelId),
4262
4995
  senderId: this.cfg.identityId,
4263
4996
  senderType: "agent",
4264
4997
  type: "text",
@@ -4276,6 +5009,7 @@ var BridgeAgent = class _BridgeAgent {
4276
5009
  (envelope) => this.latency.time("send.publish", () => this.transport.publish(envelope))
4277
5010
  );
4278
5011
  this.latency.record("send.total", performance.now() - sendStart);
5012
+ void recordOutboundMemory(this.memory, channelId, text, id);
4279
5013
  return { message_id: id, status: "sent" };
4280
5014
  }
4281
5015
  async sendA2A(channelId, a2a, messageId) {
@@ -4294,7 +5028,7 @@ var BridgeAgent = class _BridgeAgent {
4294
5028
  groupKey: this.provider,
4295
5029
  messageId: id,
4296
5030
  channelId,
4297
- enclaveId: this.cfg.enclaveId,
5031
+ enclaveId: this.resolveEnclave(channelId),
4298
5032
  senderId: this.cfg.identityId,
4299
5033
  senderType: "agent",
4300
5034
  type: "a2a",
@@ -4347,7 +5081,7 @@ var BridgeAgent = class _BridgeAgent {
4347
5081
  groupKey: this.provider,
4348
5082
  messageId: id,
4349
5083
  channelId,
4350
- enclaveId: this.cfg.enclaveId,
5084
+ enclaveId: this.resolveEnclave(channelId),
4351
5085
  senderId: this.cfg.identityId,
4352
5086
  senderType: "agent",
4353
5087
  type: "artifact_share",
@@ -4367,42 +5101,91 @@ var BridgeAgent = class _BridgeAgent {
4367
5101
  this.latency.record("send.total", performance.now() - sendStart);
4368
5102
  return { message_id: id, status: "sent" };
4369
5103
  }
4370
- async listChannels() {
4371
- const url = `${this.cfg.apiBase}/api/v1/enclaves/${this.cfg.enclaveId}/channels?caller_identity_id=${encodeURIComponent(this.cfg.identityId)}`;
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)}`;
4372
5119
  const res = await this.fetchImpl(url, {
4373
5120
  headers: { authorization: `Bearer ${this.cfg.apiKey}` }
4374
5121
  });
4375
5122
  if (!res.ok) throw new Error(`list_channels failed: ${res.status}`);
4376
5123
  const rows = await res.json();
5124
+ for (const r of rows) this.store.recordChannelEnclave(r.channel_id, r.enclave_id);
4377
5125
  return rows.map((r) => ({
4378
5126
  channel_id: r.channel_id,
4379
5127
  name: r.name ?? null,
4380
5128
  is_dm: r.is_dm,
4381
- joined: true
5129
+ joined: true,
5130
+ enclave_id: r.enclave_id
4382
5131
  }));
4383
5132
  }
4384
- /** A live channelMeta entry, or undefined if missing OR expired past the TTL. */
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
+ */
4385
5149
  freshMeta(channelId) {
4386
5150
  const hit = this.channelMeta.get(channelId);
4387
- if (!hit) return void 0;
4388
- if (Date.now() - hit.at >= CHANNEL_META_TTL_MS) return void 0;
4389
- return { is_dm: hit.is_dm, name: hit.name };
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;
4390
5160
  }
4391
5161
  /**
4392
- * One-shot, whole-listing refresh of channelMeta. Called AT MOST ONCE per
4393
- * ingest cycle (the caller guards with a cycle-local "refreshed" flag), so a
4394
- * burst of K unknown channels fires a single listChannels() instead of K. Any
4395
- * listChannels failure is swallowed (UNKNOWN → fail open) so ingest never
4396
- * blocks on the network.
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.
4397
5176
  */
4398
5177
  async refreshChannelMeta() {
4399
- try {
4400
- const now = Date.now();
4401
- const channels = await this.listChannels();
4402
- for (const c of channels) {
4403
- this.channelMeta.set(c.channel_id, { is_dm: c.is_dm, name: c.name, at: now });
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 {
4404
5188
  }
4405
- } catch {
4406
5189
  }
4407
5190
  }
4408
5191
  /** A live roster for a channel, or undefined if missing OR expired past the TTL. */
@@ -4508,6 +5291,8 @@ var BridgeAgent = class _BridgeAgent {
4508
5291
  async reportStatus(channelId, state, detail) {
4509
5292
  const joined = await this.reconcileJoin(channelId);
4510
5293
  if (!joined) throw new ChannelPendingError(channelId);
5294
+ const enc = this.store.getChannelMeta(channelId)?.enclaveId;
5295
+ if (!enc) return;
4511
5296
  await sendWithSelfHeal(
4512
5297
  this.provider,
4513
5298
  channelId,
@@ -4516,7 +5301,7 @@ var BridgeAgent = class _BridgeAgent {
4516
5301
  groupKey: this.provider,
4517
5302
  messageId: crypto.randomUUID(),
4518
5303
  channelId,
4519
- enclaveId: this.cfg.enclaveId,
5304
+ enclaveId: enc,
4520
5305
  senderId: this.cfg.identityId,
4521
5306
  senderType: "agent",
4522
5307
  type: "system_event",
@@ -4563,7 +5348,7 @@ var BridgeAgent = class _BridgeAgent {
4563
5348
  groupKey: this.provider,
4564
5349
  messageId: crypto.randomUUID(),
4565
5350
  channelId,
4566
- enclaveId: this.cfg.enclaveId,
5351
+ enclaveId: this.resolveEnclave(channelId),
4567
5352
  senderId: this.cfg.identityId,
4568
5353
  senderType: "agent",
4569
5354
  type: "system_event",
@@ -4807,6 +5592,11 @@ var BridgeAgent = class _BridgeAgent {
4807
5592
  const acked = [];
4808
5593
  for (const m of entries) {
4809
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
+ }
4810
5600
  const parsed = JSON.parse(
4811
5601
  new TextDecoder().decode(m.plaintext)
4812
5602
  );
@@ -4878,6 +5668,7 @@ var BridgeAgent = class _BridgeAgent {
4878
5668
  sender_is_agent: senderIsAgent
4879
5669
  });
4880
5670
  this.capBuffer();
5671
+ void recordInboundMemory(this.memory, m.channelId, m.envelope.sender_id, parsed.text, m.messageId);
4881
5672
  }
4882
5673
  }
4883
5674
  acked.push(m.queueId);
@@ -4974,7 +5765,8 @@ var BridgeAgent = class _BridgeAgent {
4974
5765
  failed_attachments: this.failedAttachments,
4975
5766
  delivery_pings: this.pingsReceived,
4976
5767
  uptime_s: this.startedAt ? Math.floor((Date.now() - this.startedAt) / 1e3) : 0,
4977
- version: BRIDGE_VERSION
5768
+ version: BRIDGE_VERSION,
5769
+ ...this.memory.stats?.() ?? {}
4978
5770
  };
4979
5771
  }
4980
5772
  /**
@@ -5030,11 +5822,11 @@ var BridgeAgent = class _BridgeAgent {
5030
5822
 
5031
5823
  // src/auth.ts
5032
5824
  import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
5033
- import { existsSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
5825
+ import { existsSync, readFileSync as readFileSync5, writeFileSync } from "node:fs";
5034
5826
  function resolveToken(opts) {
5035
5827
  if (opts.token) return opts.token;
5036
5828
  const f = opts.tokenFile;
5037
- if (f && existsSync(f)) return readFileSync2(f, "utf8").trim();
5829
+ if (f && existsSync(f)) return readFileSync5(f, "utf8").trim();
5038
5830
  const tok = randomBytes3(32).toString("base64url");
5039
5831
  if (f) writeFileSync(f, tok, { mode: 384 });
5040
5832
  return tok;
@@ -5149,6 +5941,17 @@ var statusSchema = z.object({
5149
5941
  state: z.enum(["idle", "working", "stalled", "error"]),
5150
5942
  detail: z.string().optional()
5151
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
+ });
5152
5955
  function json(res, status, body) {
5153
5956
  const s = JSON.stringify(body);
5154
5957
  res.writeHead(status, { "content-type": "application/json" });
@@ -5242,6 +6045,26 @@ function createHttpServer(agent, opts) {
5242
6045
  await agent.reportStatus(parsed.data.channel_id, parsed.data.state, parsed.data.detail);
5243
6046
  return json(res, 200, { ok: true });
5244
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
+ }
5245
6068
  if (method === "GET" && path === "/v1/messages") {
5246
6069
  const channel = url.searchParams.get("channel_id") ?? void 0;
5247
6070
  const wait = Math.max(0, Math.min(50, Number(url.searchParams.get("wait") ?? 0)));
@@ -5572,14 +6395,14 @@ async function handleInbound(sock, agent, data, inflight) {
5572
6395
  }
5573
6396
 
5574
6397
  // src/bin.ts
5575
- import { readFileSync as readFileSync3, mkdirSync, writeFileSync as writeFileSync2, rmSync, existsSync as existsSync2, realpathSync } from "node:fs";
5576
- import { join as join4, dirname as dirname5 } from "node:path";
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";
5577
6400
  import { fileURLToPath } from "node:url";
5578
- import { homedir, userInfo } from "node:os";
6401
+ import { homedir as homedir2, userInfo } from "node:os";
5579
6402
  import { spawnSync } from "node:child_process";
5580
6403
 
5581
6404
  // src/service/install.ts
5582
- import { dirname as dirname3 } from "node:path";
6405
+ import { dirname as dirname4 } from "node:path";
5583
6406
 
5584
6407
  // src/service/config.ts
5585
6408
  import { basename as basename2, extname, isAbsolute, resolve } from "node:path";
@@ -5663,7 +6486,7 @@ WantedBy=default.target
5663
6486
  var ServiceUnsupportedError = class extends Error {
5664
6487
  };
5665
6488
  function tokenHint(input) {
5666
- return `${dirname3(input.credentialPath)}/bridge-token`;
6489
+ return `${dirname4(input.credentialPath)}/bridge-token`;
5667
6490
  }
5668
6491
  function requireOk(r, what) {
5669
6492
  if (r.status !== 0) throw new Error(`${what} failed (exit ${r.status})${r.stderr ? `: ${r.stderr.trim()}` : ""}`);
@@ -5671,8 +6494,8 @@ function requireOk(r, what) {
5671
6494
  function installService(input, deps) {
5672
6495
  const cfg = deriveServiceConfig(input);
5673
6496
  if (deps.platform === "darwin") {
5674
- deps.mkdirp(dirname3(cfg.plistPath));
5675
- deps.mkdirp(dirname3(cfg.logPath));
6497
+ deps.mkdirp(dirname4(cfg.plistPath));
6498
+ deps.mkdirp(dirname4(cfg.logPath));
5676
6499
  deps.writeFile(cfg.plistPath, renderLaunchdPlist(cfg));
5677
6500
  deps.run("launchctl", ["bootout", `gui/${cfg.uid}/${cfg.label}`]);
5678
6501
  requireOk(deps.run("launchctl", ["bootstrap", `gui/${cfg.uid}`, cfg.plistPath]), "launchctl bootstrap");
@@ -5681,7 +6504,7 @@ function installService(input, deps) {
5681
6504
  return { label: cfg.label, filePath: cfg.plistPath, logHint: cfg.logPath, tokenHint: tokenHint(input) };
5682
6505
  }
5683
6506
  if (deps.platform === "linux") {
5684
- deps.mkdirp(dirname3(cfg.unitPath));
6507
+ deps.mkdirp(dirname4(cfg.unitPath));
5685
6508
  deps.writeFile(cfg.unitPath, renderSystemdUnit(cfg));
5686
6509
  deps.run("systemctl", ["--user", "daemon-reload"]);
5687
6510
  deps.run("loginctl", ["enable-linger", deps.user]);
@@ -5708,55 +6531,6 @@ function uninstallService(input, deps) {
5708
6531
  throw new ServiceUnsupportedError(`Service uninstall isn't supported on ${deps.platform} yet.`);
5709
6532
  }
5710
6533
 
5711
- // src/service/resolve-bin.ts
5712
- import { join as join3, dirname as dirname4 } from "node:path";
5713
- import { createRequire } from "node:module";
5714
- var GlobalInstallPermissionError = class extends Error {
5715
- constructor(message) {
5716
- super(message);
5717
- this.name = "GlobalInstallPermissionError";
5718
- }
5719
- };
5720
- function isEphemeralPath(p) {
5721
- return /[\\/]_npx[\\/]/.test(p);
5722
- }
5723
- function refersToSameFile(a, b, realpath) {
5724
- if (a === b) return true;
5725
- try {
5726
- return realpath(a) === realpath(b);
5727
- } catch {
5728
- return false;
5729
- }
5730
- }
5731
- function resolveStableBinScript(input, deps) {
5732
- const ephemeral = isEphemeralPath(input.binScript);
5733
- if (!ephemeral) {
5734
- if (deps.fileExists(input.binScript)) return input.binScript;
5735
- throw new Error(`bin path does not exist and is not an npx cache path: ${input.binScript}`);
5736
- }
5737
- const pkgRoot = dirname4(dirname4(input.binScript));
5738
- const inst = deps.run("npm", ["i", "-g", pkgRoot, "--install-links"]);
5739
- if (inst.status !== 0) {
5740
- if (/EACCES|permission denied/i.test(inst.stderr)) {
5741
- throw new GlobalInstallPermissionError(
5742
- `Can't write the global npm prefix. Run this once, then re-run install:
5743
- npm i -g ${input.packageName}`
5744
- );
5745
- }
5746
- throw new Error(`npm i -g ${pkgRoot} failed (exit ${inst.status})${inst.stderr ? `: ${inst.stderr.trim()}` : ""}`);
5747
- }
5748
- const root = deps.run("npm", ["root", "-g"]);
5749
- if (root.status !== 0) throw new Error(`npm root -g failed (exit ${root.status})`);
5750
- const stable = join3(root.stdout.trim(), input.packageName, "dist", "bin.js");
5751
- if (!deps.fileExists(stable)) throw new Error(`global install did not produce ${stable}`);
5752
- return stable;
5753
- }
5754
- function loadPackageMeta(importMetaUrl) {
5755
- const require2 = createRequire(importMetaUrl);
5756
- const pkg = require2("../package.json");
5757
- return { name: pkg.name, version: pkg.version };
5758
- }
5759
-
5760
6534
  // src/bin.ts
5761
6535
  function nodeMajorOk(version) {
5762
6536
  return Number(version.split(".")[0]) >= 24;
@@ -5818,7 +6592,7 @@ function runServiceVerb(verb) {
5818
6592
  label,
5819
6593
  execPath: process.execPath,
5820
6594
  binScript,
5821
- home: homedir(),
6595
+ home: homedir2(),
5822
6596
  uid: userInfo().uid
5823
6597
  };
5824
6598
  try {
@@ -5870,11 +6644,11 @@ async function main() {
5870
6644
  `);
5871
6645
  process.exit(1);
5872
6646
  }
5873
- const tokenFile = daemon.tokenFile ?? join4(dirname5(credentialPath), "bridge-token");
6647
+ const tokenFile = daemon.tokenFile ?? join8(dirname5(credentialPath), "bridge-token");
5874
6648
  const token = resolveToken({ tokenFile, token: daemon.token });
5875
6649
  const agent = new BridgeAgent(cfg);
5876
6650
  await agent.start();
5877
- const tls = daemon.tls ? { cert: readFileSync3(daemon.tlsCert, "utf8"), key: readFileSync3(daemon.tlsKey, "utf8") } : void 0;
6651
+ const tls = daemon.tls ? { cert: readFileSync6(daemon.tlsCert, "utf8"), key: readFileSync6(daemon.tlsKey, "utf8") } : void 0;
5878
6652
  const { server, closeAllSseStreams } = createHttpServer(agent, { token, allowOrigins: daemon.allowOrigins, tls, websocket: true });
5879
6653
  const agentWs = attachAgentWebSocket(server, agent, { token, allowOrigins: daemon.allowOrigins });
5880
6654
  let down = false;