enigma-memory 0.1.18 → 0.1.22

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 (276) hide show
  1. package/README.md +76 -24
  2. package/apps/cli/bin/enigma-desktop.mjs +140 -0
  3. package/apps/cli/bin/enigma-terminal.mjs +78 -0
  4. package/apps/cli/bin/enigma.mjs +1923 -285
  5. package/apps/desktop/electron-main.cjs +217 -0
  6. package/apps/desktop/package.json +12 -0
  7. package/apps/desktop/src/app.js +264 -7
  8. package/apps/desktop/src/index.html +3514 -1373
  9. package/apps/desktop/src/launch-electron.mjs +51 -0
  10. package/apps/desktop/src/server.mjs +2914 -0
  11. package/apps/desktop/src/styles.css +2972 -260
  12. package/apps/desktop/src/zk-browser-prove.mjs +53 -0
  13. package/apps/desktop/src/zk-state.mjs +1789 -0
  14. package/apps/gateway/bin/enigma-gateway.mjs +102 -5
  15. package/apps/gateway/src/server.mjs +271 -8
  16. package/apps/ios/EnigmaCore/Package.swift +12 -0
  17. package/apps/ios/EnigmaCore/Sources/EnigmaCore/EnigmaAPIClient.swift +227 -0
  18. package/apps/ios/EnigmaCore/Sources/EnigmaCore/Models.swift +278 -0
  19. package/apps/ios/EnigmaCore/Sources/EnigmaCore/PKCE.swift +96 -0
  20. package/apps/ios/EnigmaCore/Sources/EnigmaCore/PrivacyMinimizer.swift +187 -0
  21. package/apps/ios/EnigmaCore/Sources/EnigmaCore/ToolModels.swift +129 -0
  22. package/apps/ios/EnigmaCore/Tests/EnigmaCoreTests/EnigmaCoreTests.swift +42 -0
  23. package/apps/ios/EnigmaIOS/Enigma/AppModel.swift +346 -0
  24. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AccentColor.colorset/Contents.json +12 -0
  25. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AppIcon.appiconset/Contents.json +11 -0
  26. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AppIcon.appiconset/EnigmaAppIcon.png +0 -0
  27. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/Contents.json +3 -0
  28. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/LaunchBackground.colorset/Contents.json +12 -0
  29. package/apps/ios/EnigmaIOS/Enigma/ChatView.swift +181 -0
  30. package/apps/ios/EnigmaIOS/Enigma/CouncilView.swift +78 -0
  31. package/apps/ios/EnigmaIOS/Enigma/CreateView.swift +152 -0
  32. package/apps/ios/EnigmaIOS/Enigma/EnigmaApp.swift +52 -0
  33. package/apps/ios/EnigmaIOS/Enigma/Info.plist +52 -0
  34. package/apps/ios/EnigmaIOS/Enigma/NaturalLanguagePrivacyTagger.swift +26 -0
  35. package/apps/ios/EnigmaIOS/Enigma/OAuthClient.swift +321 -0
  36. package/apps/ios/EnigmaIOS/Enigma/OnboardingView.swift +105 -0
  37. package/apps/ios/EnigmaIOS/Enigma/PrivateVaultView.swift +275 -0
  38. package/apps/ios/EnigmaIOS/Enigma/SecureStore.swift +76 -0
  39. package/apps/ios/EnigmaIOS/Enigma/SettingsView.swift +60 -0
  40. package/apps/ios/EnigmaIOS/Enigma/Theme.swift +80 -0
  41. package/apps/ios/EnigmaIOS/EnigmaIOS.xcodeproj/project.pbxproj +211 -0
  42. package/apps/ios/EnigmaIOS/EnigmaIOS.xcodeproj/xcshareddata/xcschemes/Enigma.xcscheme +23 -0
  43. package/apps/native-host/README.md +19 -8
  44. package/apps/native-host/bin/enigma-native-host.mjs +229 -13
  45. package/apps/relay/bin/enigma-relay.mjs +103 -5
  46. package/apps/relay/src/federation-runtime.mjs +618 -0
  47. package/apps/relay/src/server.mjs +310 -9
  48. package/apps/verifier/bin/enigma-verify.mjs +327 -11
  49. package/cortex-v3/circuits/build/intent_vk_bytes.json +35 -0
  50. package/cortex-v3/circuits/build/sale_vk_bytes.json +35 -0
  51. package/cortex-v3/circuits/build/vk_bytes.json +32 -0
  52. package/cortex-v3/proving-assets.json +64 -0
  53. package/cortex-v3/zk/BUILD-CONTRACT.md +87 -0
  54. package/cortex-v3/zk/action-transition-vk.json +119 -0
  55. package/cortex-v3/zk/alias-adversarial.test.mjs +220 -0
  56. package/cortex-v3/zk/groth16-verify-child.mjs +17 -0
  57. package/cortex-v3/zk/intent-witness.mjs +365 -0
  58. package/cortex-v3/zk/intent-witness.test.mjs +485 -0
  59. package/cortex-v3/zk/proving-assets.mjs +203 -0
  60. package/cortex-v3/zk/sale-witness.mjs +783 -0
  61. package/cortex-v3/zk/sale-witness.test.mjs +784 -0
  62. package/cortex-v3/zk/sealed-sale-release-vk.json +119 -0
  63. package/cortex-v3/zk/settlement-evidence.mjs +722 -0
  64. package/cortex-v3/zk/setup-intent.mjs +688 -0
  65. package/cortex-v3/zk/setup-sale.mjs +666 -0
  66. package/cortex-v3/zk/setup.mjs +594 -0
  67. package/cortex-v3/zk/witness.mjs +184 -0
  68. package/cortex-v3/zk/zk-codec.mjs +232 -0
  69. package/cortex-v3/zk/zk-codec.test.mjs +293 -0
  70. package/cortex-v3/zk/zk-settle.mjs +370 -0
  71. package/cortex-v3/zk/zk-tree.mjs +256 -0
  72. package/cortex-v3/zk/zk-tree.test.mjs +419 -0
  73. package/deploy/docker-compose.local-production-simulation.yml +36 -0
  74. package/docs/browser-extension-install.md +8 -6
  75. package/docs/client-connectors.md +15 -11
  76. package/docs/developer-ecosystem.md +15 -13
  77. package/docs/enigma-memory-ready-conformance.md +11 -9
  78. package/docs/install-anywhere.md +61 -28
  79. package/docs/installers-and-desktop.md +8 -7
  80. package/docs/novelty-invention-candidates.md +161 -161
  81. package/docs/proof-network-claim-boundaries.md +320 -318
  82. package/examples/01-quickstart-agent/index.mjs +49 -0
  83. package/examples/01_agent_memory_quickstart.mjs +57 -0
  84. package/examples/02-multi-agent-swarm/index.mjs +57 -0
  85. package/examples/02_cross_model_passport.mjs +64 -0
  86. package/examples/03-langchain-memory/index.mjs +41 -0
  87. package/examples/03_poseidon_commitment_verification.mjs +71 -0
  88. package/examples/04-python-trading-agent/trader.py +49 -0
  89. package/examples/README.md +27 -0
  90. package/examples/ci/github-actions.yml +7 -2
  91. package/package.json +142 -11
  92. package/packages/adapters/PACKAGE_CONTRACT.md +1 -1
  93. package/packages/connectors/src/index.js +196 -4
  94. package/packages/connectors/swarm-router.mjs +168 -0
  95. package/packages/core/src/index.js +248 -1
  96. package/packages/core/src/version.mjs +7 -0
  97. package/packages/dev-tools/package.json +19 -0
  98. package/packages/dev-tools/src/index.js +4 -0
  99. package/packages/dev-tools/src/memory-benchmark-suite.js +112 -0
  100. package/packages/dev-tools/src/swarm-simulator.js +101 -0
  101. package/packages/dev-tools/src/vault-inspector.js +114 -0
  102. package/packages/dev-tools/src/vector-benchmark.js +100 -0
  103. package/packages/developer-platform/src/access-credentials.js +341 -0
  104. package/packages/developer-platform/src/http.js +132 -0
  105. package/packages/developer-platform/src/index.js +4 -0
  106. package/packages/developer-platform/src/usage-http.js +60 -0
  107. package/packages/developer-platform/src/usage.js +295 -0
  108. package/packages/enclave-runtime/attestation.mjs +159 -0
  109. package/packages/enclave-runtime/index.mjs +47 -0
  110. package/packages/enclave-runtime/session-manager.mjs +253 -0
  111. package/packages/enclave-runtime/zeroization-proof.mjs +227 -0
  112. package/packages/enigma-reflex/package.json +14 -0
  113. package/packages/enigma-reflex/src/index.js +204 -0
  114. package/packages/enigma-reflex/training/generate-dataset.mjs +40 -0
  115. package/packages/enigma-reflex/training/requirements.txt +8 -0
  116. package/packages/enigma-reflex/training/train.py +314 -0
  117. package/packages/enigma-weave/LICENSE +22 -0
  118. package/packages/enigma-weave/UPSTREAM.json +21 -0
  119. package/packages/enigma-weave/package.json +14 -0
  120. package/packages/enigma-weave/src/index.js +286 -0
  121. package/packages/hosted-cloud/src/index.js +80 -5
  122. package/packages/importers/src/index.js +432 -0
  123. package/packages/inference-runtime/src/browser.js +401 -0
  124. package/packages/inference-runtime/src/chat.js +265 -0
  125. package/packages/inference-runtime/src/code.js +407 -0
  126. package/packages/inference-runtime/src/contracts.js +162 -0
  127. package/packages/inference-runtime/src/http.js +232 -0
  128. package/packages/inference-runtime/src/image.js +186 -0
  129. package/packages/inference-runtime/src/index.js +10 -0
  130. package/packages/inference-runtime/src/model-router.js +320 -0
  131. package/packages/inference-runtime/src/platform.js +125 -0
  132. package/packages/inference-runtime/src/privacy.js +400 -0
  133. package/packages/inference-runtime/src/video.js +253 -0
  134. package/packages/mcp-server/README.md +22 -6
  135. package/packages/mcp-server/bin/enigma-mcp.mjs +2 -1
  136. package/packages/mcp-server/src/index.js +1418 -105
  137. package/packages/mcp-server/src/oauth.js +561 -0
  138. package/packages/mcp-server/src/private-handoff.js +84 -0
  139. package/packages/mcp-server/src/remote-http.js +273 -0
  140. package/packages/mcp-server/src/remote-policy.js +72 -0
  141. package/packages/mcp-server/swarm-bridge.mjs +361 -0
  142. package/packages/mesh/index.d.ts +283 -0
  143. package/packages/mesh/package.json +23 -0
  144. package/packages/mesh/src/crypto.js +189 -0
  145. package/packages/mesh/src/federation-packets.js +353 -0
  146. package/packages/mesh/src/gossip.js +311 -0
  147. package/packages/mesh/src/index.js +6 -0
  148. package/packages/mesh/src/protocol.js +255 -0
  149. package/packages/mesh/src/router.js +279 -0
  150. package/packages/mesh/src/transport.js +306 -0
  151. package/packages/passport/src/index.js +426 -1
  152. package/packages/private-economy/src/credits-http.js +100 -0
  153. package/packages/private-economy/src/credits.js +447 -0
  154. package/packages/private-economy/src/index.js +5 -0
  155. package/packages/private-economy/src/payments-http.js +120 -0
  156. package/packages/private-economy/src/payments.js +509 -0
  157. package/packages/private-economy/src/x402.js +346 -0
  158. package/packages/proof-network/PACKAGE_CONTRACT.md +21 -0
  159. package/packages/rag/index.d.ts +182 -0
  160. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/THIRD_PARTY_LICENSES.txt +207 -0
  161. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/config.json +25 -0
  162. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/onnx/model_quantized.onnx +0 -0
  163. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/sha256-manifest.json +28 -0
  164. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer.json +30686 -0
  165. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer_config.json +15 -0
  166. package/packages/rag/package.json +27 -0
  167. package/packages/rag/src/blinded-search.js +109 -0
  168. package/packages/rag/src/bm25.js +169 -0
  169. package/packages/rag/src/embeddings.js +459 -0
  170. package/packages/rag/src/hybrid.js +76 -0
  171. package/packages/rag/src/index.js +38 -0
  172. package/packages/rag/src/reranker.js +61 -0
  173. package/packages/rag/src/research.js +107 -0
  174. package/packages/rag/src/vector-store.js +430 -0
  175. package/packages/rag/src/verify-model-artifacts.mjs +4 -0
  176. package/packages/sdk/index.d.ts +760 -0
  177. package/packages/sdk/package.json +33 -0
  178. package/packages/sdk/python/README.md +24 -0
  179. package/packages/sdk/python/enigma_sdk.py +250 -0
  180. package/packages/sdk/python/pyproject.toml +34 -0
  181. package/packages/sdk/python/requirements.txt +1 -0
  182. package/packages/sdk/python/setup.py +20 -0
  183. package/packages/sdk/src/federation/capability-grant.js +389 -0
  184. package/packages/sdk/src/federation/federation-router.js +360 -0
  185. package/packages/sdk/src/federation/ghostmesh-bridge.js +497 -0
  186. package/packages/sdk/src/federation/index.js +3 -0
  187. package/packages/sdk/src/index.js +1796 -0
  188. package/packages/sdk/src/intelligence/contradiction.js +337 -0
  189. package/packages/sdk/src/intelligence/decision-engine.js +155 -0
  190. package/packages/sdk/src/intelligence/index.js +4 -0
  191. package/packages/sdk/src/intelligence/ontology.js +122 -0
  192. package/packages/sdk/src/intelligence/temporal.js +123 -0
  193. package/packages/sdk/src/market-client.js +142 -0
  194. package/packages/sdk/src/mesh-client.js +110 -0
  195. package/packages/sdk/src/middleware/index.js +3 -0
  196. package/packages/sdk/src/middleware/langchain.js +159 -0
  197. package/packages/sdk/src/middleware/llamaindex.js +101 -0
  198. package/packages/sdk/src/middleware/vercel-ai.js +112 -0
  199. package/packages/sdk/src/rag-client.js +85 -0
  200. package/packages/sdk/src/swarm-orchestrator.js +260 -0
  201. package/packages/settlement/PACKAGE_CONTRACT.md +1 -1
  202. package/packages/snapcompact/THIRD_PARTY_LICENSES.txt +40 -0
  203. package/packages/snapcompact/assets/8x13-latin1.bdf +3837 -0
  204. package/packages/snapcompact/index.d.ts +284 -0
  205. package/packages/snapcompact/package.json +25 -0
  206. package/packages/snapcompact/src/index.js +716 -0
  207. package/packages/storage/PACKAGE_CONTRACT.md +1 -1
  208. package/packages/terminal-console/animations.mjs +240 -0
  209. package/packages/terminal-console/auto-anchor.mjs +220 -0
  210. package/packages/terminal-console/banner.mjs +91 -0
  211. package/packages/terminal-console/commands.mjs +459 -0
  212. package/packages/terminal-console/delegation.mjs +152 -0
  213. package/packages/terminal-console/index.mjs +5 -0
  214. package/packages/terminal-console/outbox.mjs +143 -0
  215. package/packages/terminal-console/phantom-bridge.mjs +637 -0
  216. package/packages/terminal-console/repl.mjs +136 -0
  217. package/packages/terminal-console/signer-store.mjs +130 -0
  218. package/packages/terminal-console/solana-rpc.mjs +214 -0
  219. package/packages/terminal-console/solana-transport.mjs +189 -0
  220. package/packages/terminal-tui/dashboard.mjs +214 -0
  221. package/packages/terminal-tui/index.mjs +28 -0
  222. package/packages/terminal-tui/merkle-tree-renderer.mjs +268 -0
  223. package/packages/terminal-tui/telemetry-hud.mjs +137 -0
  224. package/packages/vault/index.d.ts +449 -0
  225. package/packages/vault/package.json +27 -0
  226. package/packages/vault/src/e2ee.mjs +393 -0
  227. package/packages/vault/src/enclave.js +481 -0
  228. package/packages/vault/src/erasure.js +207 -0
  229. package/packages/vault/src/index.js +1018 -155
  230. package/packages/vault/src/persistence.js +307 -0
  231. package/packages/vault/src/poseidon.js +354 -0
  232. package/packages/vault/src/receipt.js +459 -0
  233. package/scripts/benchmark-optical-context.mjs +166 -0
  234. package/scripts/bootstrap-enigma.mjs +502 -0
  235. package/scripts/build-edge-backend-workers.mjs +20 -5
  236. package/scripts/build-goal-completion-audit.mjs +72 -25
  237. package/scripts/build-hosted-api-key-lifecycle.mjs +26 -8
  238. package/scripts/build-hosted-customer-lifecycle.mjs +20 -3
  239. package/scripts/build-hosted-probe-worker.mjs +19 -4
  240. package/scripts/build-installer-assets.mjs +41 -21
  241. package/scripts/build-operator-evidence-starter.mjs +59 -1
  242. package/scripts/build-production-backend-env-kit.mjs +2 -0
  243. package/scripts/build-production-unblocker.mjs +3 -0
  244. package/scripts/check.mjs +17 -3
  245. package/scripts/collect-hosted-backend-live-evidence.mjs +49 -12
  246. package/scripts/install-enigma-local.mjs +18 -5
  247. package/scripts/release-audit.mjs +65 -109
  248. package/scripts/release-provenance.mjs +6 -0
  249. package/scripts/run-backend-readiness-smoke.mjs +112 -10
  250. package/scripts/scan-secrets.mjs +1 -0
  251. package/scripts/simulate-production-env.mjs +7 -2
  252. package/scripts/validate-hosted-backend-live.mjs +112 -1
  253. package/specs/antibody-pack-v1.schema.json +95 -0
  254. package/specs/antigen-envelope-v1.schema.json +81 -0
  255. package/specs/boundary-manifest-v1.schema.json +35 -35
  256. package/specs/capsule-v1.schema.json +55 -55
  257. package/specs/claim-boundary-manifest-v1.schema.json +22 -22
  258. package/specs/claim-ledger-v1.schema.json +291 -0
  259. package/specs/context-passport-v1.schema.json +59 -0
  260. package/specs/deletion-tombstone-v1.schema.json +26 -26
  261. package/specs/evidence-packet-v1.schema.json +177 -0
  262. package/specs/hosted-backend-live-evidence-v1.schema.json +72 -3
  263. package/specs/immune-scan-report-v1.schema.json +112 -0
  264. package/specs/lifecycle-receipt-log-v1.schema.json +67 -0
  265. package/specs/memory-atom-v1.schema.json +59 -0
  266. package/specs/memory-event-v1.schema.json +42 -42
  267. package/specs/passport-v1.schema.json +50 -50
  268. package/specs/proof-of-non-use-v1.schema.json +65 -0
  269. package/specs/quarantine-record-v1.schema.json +126 -0
  270. package/specs/receipt-v1.schema.json +61 -61
  271. package/specs/state-checkpoint-v1.schema.json +37 -37
  272. package/specs/trust-bundle-v1.schema.json +56 -56
  273. package/specs/trust-card-v1.schema.json +119 -0
  274. package/docs/proof-network-launch-plan.md +0 -421
  275. package/packages/metering/PACKAGE_CONTRACT.md +0 -20
  276. package/scripts/build-ai-orchestration-plan.mjs +0 -248
@@ -0,0 +1,401 @@
1
+ import dns from 'node:dns/promises';
2
+ import net from 'node:net';
3
+ import { createPublicKey, randomUUID, verify as verifySignature } from 'node:crypto';
4
+
5
+ export const ENIGMA_BROWSER_SESSION_SCHEMA = 'enigma.browser_session.v1';
6
+ const TERMINAL = new Set(['burned', 'failed']);
7
+ const CONTROL_ACTIONS = new Set(['reload', 'back', 'forward', 'click', 'type', 'keypress', 'scroll']);
8
+
9
+ function requiredString(value, field, maxLength = 16_000) {
10
+ if (typeof value !== 'string' || !value.trim()) throw new TypeError(`${field} must be a non-empty string`);
11
+ const normalized = value.trim();
12
+ if (normalized.length > maxLength) throw new TypeError(`${field} exceeds ${maxLength} characters`);
13
+ return normalized;
14
+ }
15
+
16
+ function boundedInteger(value, field, minimum, maximum, fallback) {
17
+ const candidate = value === undefined ? fallback : value;
18
+ if (!Number.isInteger(candidate) || candidate < minimum || candidate > maximum) {
19
+ throw new TypeError(`${field} must be an integer from ${minimum} to ${maximum}`);
20
+ }
21
+ return candidate;
22
+ }
23
+
24
+ function isDeniedIp(address) {
25
+ const normalized = String(address).toLowerCase().split('%')[0];
26
+ if (net.isIPv4(normalized)) {
27
+ const [a, b, c] = normalized.split('.').map(Number);
28
+ return a === 0 || a === 10 || a === 127 || (a === 100 && b >= 64 && b <= 127)
29
+ || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31)
30
+ || (a === 192 && b === 0) || (a === 192 && b === 168)
31
+ || (a === 198 && (b === 18 || b === 19)) || (a === 198 && b === 51 && c === 100)
32
+ || (a === 203 && b === 0 && c === 113) || a >= 224;
33
+ }
34
+ if (net.isIPv6(normalized)) {
35
+ return normalized === '::' || normalized === '::1' || normalized.startsWith('fc') || normalized.startsWith('fd')
36
+ || normalized.startsWith('fe8') || normalized.startsWith('fe9') || normalized.startsWith('fea')
37
+ || normalized.startsWith('feb') || normalized.startsWith('ff') || normalized.startsWith('2001:db8:')
38
+ || normalized.startsWith('2001:10:');
39
+ }
40
+ return true;
41
+ }
42
+
43
+ export async function assertPublicNavigationUrl(value, lookup = dns.lookup) {
44
+ const url = new URL(requiredString(value, 'url', 8_192));
45
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') throw new TypeError('browser navigation only supports HTTP and HTTPS');
46
+ if (url.username || url.password) throw new TypeError('browser navigation URL must not contain credentials');
47
+ const hostname = url.hostname.toLowerCase();
48
+ if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname.endsWith('.local') || hostname.endsWith('.internal')) {
49
+ throw new Error('private or local browser destination blocked');
50
+ }
51
+ if (net.isIP(hostname)) {
52
+ if (isDeniedIp(hostname)) throw new Error('private or special-use browser destination blocked');
53
+ } else {
54
+ const answers = await lookup(hostname, { all: true, verbatim: true });
55
+ if (!answers.length || answers.some(({ address }) => isDeniedIp(address))) {
56
+ throw new Error('browser destination resolves to a private or special-use address');
57
+ }
58
+ }
59
+ return url.toString();
60
+ }
61
+ function normalizeViewerOrigin(value) {
62
+ const url = new URL(requiredString(value, 'browser viewer origin', 4_096));
63
+ const localHttp = url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname);
64
+ if (url.protocol !== 'https:' && !localHttp) throw new TypeError('browser viewer origin must use HTTPS outside localhost');
65
+ if (url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
66
+ throw new TypeError('browser viewer origin must contain only scheme, host, and port');
67
+ }
68
+ return url.origin;
69
+ }
70
+
71
+ function validateViewerUrl(value, expectedOrigin) {
72
+ if (!value) return null;
73
+ const url = new URL(requiredString(value, 'browser stream URL', 8_192));
74
+ if (url.origin !== expectedOrigin) throw new Error('browser broker returned a viewer URL outside the configured origin');
75
+ if (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname))) {
76
+ throw new Error('browser stream URL must use HTTPS outside localhost');
77
+ }
78
+ if (url.username || url.password) throw new Error('browser stream URL must not use URL authority credentials');
79
+ return url.toString();
80
+ }
81
+
82
+
83
+ function normalizeDisplay(input = {}) {
84
+ const profile = input.profile || 'sharp';
85
+ const presets = {
86
+ efficient: { profile: 'efficient', width: 1440, height: 810, fps: 30 },
87
+ sharp: { profile: 'sharp', width: 1920, height: 1080, fps: 30 },
88
+ ultra: { profile: 'ultra', width: 2560, height: 1440, fps: 24 },
89
+ };
90
+ if (presets[profile] && input.width === undefined && input.height === undefined && input.fps === undefined) return presets[profile];
91
+ const display = {
92
+ profile: presets[profile] ? profile : 'custom',
93
+ width: boundedInteger(input.width, 'display.width', 720, 2560, 1920),
94
+ height: boundedInteger(input.height, 'display.height', 600, 2160, 1080),
95
+ fps: boundedInteger(input.fps, 'display.fps', 1, 60, 30),
96
+ };
97
+ if (display.width * display.height > 4_100_000 || display.width / display.height < 0.5 || display.width / display.height > 2.75) {
98
+ throw new TypeError('display dimensions exceed the private browser pixel or aspect-ratio limit');
99
+ }
100
+ return display;
101
+ }
102
+
103
+ function normalizeTabs(value) {
104
+ if (!Array.isArray(value)) return [];
105
+ return value.slice(0, 5).map((tab) => ({
106
+ id: requiredString(tab.id, 'tab id', 512),
107
+ title: typeof tab.title === 'string' ? tab.title.slice(0, 512) : '',
108
+ url: typeof tab.url === 'string' ? tab.url.slice(0, 8_192) : 'about:blank',
109
+ zoom: Number.isFinite(tab.zoom) ? tab.zoom : 1,
110
+ }));
111
+ }
112
+ const REQUIRED_EGRESS_CONTROLS = Object.freeze([
113
+ 'dnsRebindingProtected',
114
+ 'redirectsRevalidated',
115
+ 'privateCidrsBlocked',
116
+ 'metadataEndpointsBlocked',
117
+ 'quicBlocked',
118
+ 'nonProxiedWebRtcBlocked',
119
+ 'interactiveNavigationCovered',
120
+ 'subresourcesCovered',
121
+ ]);
122
+
123
+ function normalizeEgressPolicy(value) {
124
+ if (!value || typeof value !== 'object') throw new Error('browser broker returned no egress enforcement policy');
125
+ const layer = String(value.layer || value.enforcedAt || '');
126
+ if (!['network-namespace', 'egress-proxy', 'network-firewall'].includes(layer)) {
127
+ throw new Error('browser broker egress policy is not enforced below the browser UI');
128
+ }
129
+ const policyHash = requiredString(value.policyHash ?? value.policy_hash, 'egress policy hash', 512);
130
+ for (const control of REQUIRED_EGRESS_CONTROLS) {
131
+ if (value[control] !== true) throw new Error(`browser broker egress control is missing: ${control}`);
132
+ }
133
+ return Object.freeze({
134
+ layer,
135
+ policyHash,
136
+ ...Object.fromEntries(REQUIRED_EGRESS_CONTROLS.map((control) => [control, true])),
137
+ });
138
+ }
139
+
140
+
141
+ function publicSession(session) {
142
+ return {
143
+ schema: ENIGMA_BROWSER_SESSION_SCHEMA,
144
+ id: session.id,
145
+ provider: session.provider.id,
146
+ state: session.state,
147
+ createdAt: session.createdAt,
148
+ expiresAt: session.expiresAt,
149
+ idleExpiresAt: session.idleExpiresAt,
150
+ display: session.display,
151
+ tabs: session.tabs,
152
+ activeTabId: session.activeTabId,
153
+ streamUrl: session.state === 'connected' ? session.streamUrl : null,
154
+ privacy: {
155
+ ephemeralProfile: true,
156
+ privateNetworkBlocked: session.egressVerified === true,
157
+ egressLayer: session.egressPolicy?.layer || null,
158
+ egressPolicyHash: session.egressPolicy?.policyHash || null,
159
+ clipboardBlocked: true,
160
+ downloadsBlocked: true,
161
+ burnClears: ['cookies', 'cache', 'history', 'downloads', 'temporary-profile'],
162
+ },
163
+ };
164
+ }
165
+
166
+ export function createRemoteBrowserBrokerProvider(options = {}) {
167
+ const id = String(options.id || 'remote-neko-broker');
168
+ const baseUrl = new URL(requiredString(options.baseUrl, 'browser broker baseUrl', 4_096));
169
+ if (baseUrl.protocol !== 'https:' && !(baseUrl.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(baseUrl.hostname))) {
170
+ throw new TypeError('browser broker must use HTTPS outside localhost');
171
+ }
172
+ const viewerOrigin = normalizeViewerOrigin(options.viewerOrigin);
173
+ const apiKey = options.apiKey ? requiredString(options.apiKey, 'browser broker apiKey', 4_096) : null;
174
+ const timeoutMs = boundedInteger(options.timeoutMs, 'timeoutMs', 1_000, 120_000, 30_000);
175
+
176
+ async function request(route, requestOptions = {}) {
177
+ const controller = new AbortController();
178
+ const timer = setTimeout(() => controller.abort(new Error('browser broker timed out')), timeoutMs);
179
+ try {
180
+ const response = await fetch(new URL(route.replace(/^\/+/, ''), baseUrl), {
181
+ method: requestOptions.method || 'GET',
182
+ headers: {
183
+ Accept: 'application/json',
184
+ ...(requestOptions.body === undefined ? {} : { 'Content-Type': 'application/json' }),
185
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
186
+ ...(requestOptions.sessionToken ? { 'X-Enigma-Browser-Session': requestOptions.sessionToken } : {}),
187
+ },
188
+ body: requestOptions.body === undefined ? undefined : JSON.stringify(requestOptions.body),
189
+ signal: controller.signal,
190
+ });
191
+ const payload = response.status === 204 ? {} : await response.json().catch(() => ({}));
192
+ if (!response.ok) throw new Error(payload?.error?.message || `browser broker failed with HTTP ${response.status}`);
193
+ return payload;
194
+ } finally {
195
+ clearTimeout(timer);
196
+ }
197
+ }
198
+
199
+ function handle(value, current = {}) {
200
+ const session = value?.session || value;
201
+ if (!session || typeof session !== 'object') throw new Error('browser broker returned no session');
202
+ return {
203
+ providerSessionId: requiredString(session.id ?? current.providerSessionId, 'provider session id', 512),
204
+ sessionToken: requiredString(session.sessionToken ?? session.session_token ?? current.sessionToken, 'browser session token', 4_096),
205
+ state: session.state || current.state || 'ready',
206
+ streamUrl: validateViewerUrl(session.streamUrl ?? current.streamUrl ?? null, viewerOrigin),
207
+ tabs: normalizeTabs(session.tabs ?? current.tabs),
208
+ activeTabId: session.activeTabId ?? current.activeTabId ?? null,
209
+ egressPolicy: session.egressPolicy ?? session.egress_policy ?? current.egressPolicy ?? null,
210
+ egressAttestation: session.egressAttestation ?? session.egress_attestation ?? current.egressAttestation ?? null,
211
+ };
212
+ }
213
+
214
+ return Object.freeze({
215
+ id,
216
+ viewerOrigin,
217
+ async create({ display }) { return handle(await request('v1/sessions', { method: 'POST', body: { display } })); },
218
+ async status(current) { return handle(await request(`v1/sessions/${encodeURIComponent(current.providerSessionId)}/status`, { sessionToken: current.sessionToken }), current); },
219
+ async connect(current) { return handle(await request(`v1/sessions/${encodeURIComponent(current.providerSessionId)}/connect`, { method: 'POST', sessionToken: current.sessionToken }), current); },
220
+ async navigate(current, input) {
221
+ return request(`v1/sessions/${encodeURIComponent(current.providerSessionId)}/navigate`, {
222
+ method: 'POST', sessionToken: current.sessionToken, body: { url: input.url, tabId: input.tabId || null },
223
+ });
224
+ },
225
+ async control(current, input) {
226
+ return request(`v1/sessions/${encodeURIComponent(current.providerSessionId)}/control`, {
227
+ method: 'POST', sessionToken: current.sessionToken, body: input,
228
+ });
229
+ },
230
+ async render(current) {
231
+ return request(`v1/sessions/${encodeURIComponent(current.providerSessionId)}/render`, { sessionToken: current.sessionToken });
232
+ },
233
+ async destroy(current) {
234
+ await request(`v1/sessions/${encodeURIComponent(current.providerSessionId)}`, { method: 'DELETE', sessionToken: current.sessionToken });
235
+ try {
236
+ await request(`v1/sessions/${encodeURIComponent(current.providerSessionId)}/status`, { sessionToken: current.sessionToken });
237
+ } catch (error) {
238
+ if (/not found|ended|404/i.test(error.message)) return;
239
+ throw error;
240
+ }
241
+ throw new Error('browser broker cleanup verification failed: session still exists');
242
+ },
243
+ });
244
+ }
245
+
246
+ export function createDisposableBrowserRuntime(options = {}) {
247
+ const provider = options.provider;
248
+ if (!provider || typeof provider.create !== 'function' || typeof provider.destroy !== 'function') {
249
+ throw new TypeError('disposable browser requires an OS/container-isolated broker provider');
250
+ }
251
+ const ttlMs = boundedInteger(options.ttlMs, 'ttlMs', 60_000, 24 * 60 * 60_000, 60 * 60_000);
252
+ const idleTtlMs = boundedInteger(options.idleTtlMs, 'idleTtlMs', 60_000, ttlMs, 10 * 60_000);
253
+ const maxSessions = boundedInteger(options.maxSessions, 'maxSessions', 1, 20, 3);
254
+ const viewerOrigin = normalizeViewerOrigin(options.viewerOrigin || provider.viewerOrigin);
255
+ if (typeof options.verifyEgressAttestation !== 'function') {
256
+ throw new TypeError('disposable browser requires verifyEgressAttestation(); interactive browsing is disabled without broker-side egress proof');
257
+ }
258
+ const sessions = new Map();
259
+
260
+ async function verifyProviderState(session, state) {
261
+ const egressPolicy = normalizeEgressPolicy(state?.egressPolicy);
262
+ const viewerUrl = validateViewerUrl(state?.streamUrl || null, viewerOrigin);
263
+ if (session?.egressPolicy && session.egressPolicy.policyHash !== egressPolicy.policyHash) {
264
+ throw new Error('browser broker changed its egress policy during the session');
265
+ }
266
+ const verified = await options.verifyEgressAttestation(state.egressAttestation, {
267
+ provider: provider.id,
268
+ providerSessionId: state.providerSessionId,
269
+ policy: egressPolicy,
270
+ viewerOrigin,
271
+ viewerUrl,
272
+ });
273
+ if (verified !== true) throw new Error('browser broker egress attestation verification failed');
274
+ return egressPolicy;
275
+ }
276
+
277
+ async function applyProviderState(session, state) {
278
+ if (!state) return;
279
+ session.egressPolicy = await verifyProviderState(session, state);
280
+ session.egressVerified = true;
281
+ session.handle = { ...session.handle, ...state };
282
+ session.state = state.state || session.state;
283
+ session.tabs = normalizeTabs(state.tabs ?? session.tabs);
284
+ session.activeTabId = state.activeTabId ?? session.activeTabId;
285
+ session.streamUrl = state.streamUrl ?? session.streamUrl;
286
+ session.idleExpiresAt = Math.min(session.expiresAt, Date.now() + idleTtlMs);
287
+ }
288
+
289
+ async function burn(id) {
290
+ const session = sessions.get(requiredString(id, 'session id', 512));
291
+ if (!session) throw new Error('browser session not found');
292
+ if (session.state === 'burned') return publicSession(session);
293
+ session.state = 'burning';
294
+ try {
295
+ await provider.destroy(session.handle);
296
+ } catch (error) {
297
+ session.state = 'cleanup_failed';
298
+ const cleanupError = new Error(`browser cleanup failed; retry burn: ${error.message}`);
299
+ cleanupError.code = 'cleanup_failed';
300
+ throw cleanupError;
301
+ }
302
+ session.state = 'burned';
303
+ session.streamUrl = null;
304
+ session.tabs = [];
305
+ session.handle = null;
306
+ return publicSession(session);
307
+ }
308
+
309
+ async function get(id, { allowTerminal = false } = {}) {
310
+ const session = sessions.get(requiredString(id, 'session id', 512));
311
+ if (!session) throw new Error('browser session not found');
312
+ if (session.state === 'cleanup_failed' && !allowTerminal) throw new Error('browser session cleanup failed; retry burn');
313
+ if (Date.now() >= session.expiresAt || Date.now() >= session.idleExpiresAt) await burn(session.id);
314
+ if (!allowTerminal && TERMINAL.has(session.state)) throw new Error(`browser session is ${session.state}`);
315
+ return session;
316
+ }
317
+
318
+ async function refresh(session) {
319
+ if (!TERMINAL.has(session.state) && session.state !== 'cleanup_failed' && provider.status) {
320
+ await applyProviderState(session, await provider.status(session.handle));
321
+ }
322
+ return publicSession(session);
323
+ }
324
+
325
+ return Object.freeze({
326
+ async create(input = {}) {
327
+ const active = [...sessions.values()].filter((session) => !TERMINAL.has(session.state)).length;
328
+ if (active >= maxSessions) throw new Error('browser session capacity reached');
329
+ const display = normalizeDisplay(input.display || {});
330
+ const now = Date.now();
331
+ const state = await provider.create({ display });
332
+ const egressPolicy = await verifyProviderState(null, state);
333
+ const session = {
334
+ id: randomUUID(), provider, handle: state, state: state.state || 'ready', createdAt: now,
335
+ expiresAt: now + ttlMs, idleExpiresAt: Math.min(now + ttlMs, now + idleTtlMs), display,
336
+ tabs: normalizeTabs(state.tabs), activeTabId: state.activeTabId || null, streamUrl: state.streamUrl || null,
337
+ egressPolicy, egressVerified: true,
338
+ };
339
+ sessions.set(session.id, session);
340
+ return publicSession(session);
341
+ },
342
+ async status(id) { return refresh(await get(id)); },
343
+ async connect(id) {
344
+ const session = await get(id);
345
+ if (!provider.connect) throw new Error('browser provider does not support interactive streaming');
346
+ await applyProviderState(session, await provider.connect(session.handle));
347
+ if (!session.streamUrl) throw new Error('browser provider returned no stream URL');
348
+ session.state = 'connected';
349
+ return publicSession(session);
350
+ },
351
+ async navigate(id, input = {}) {
352
+ const session = await get(id);
353
+ const url = await assertPublicNavigationUrl(input.url, options.lookup || dns.lookup);
354
+ await provider.navigate(session.handle, { url, tabId: input.tabId || session.activeTabId });
355
+ session.idleExpiresAt = Math.min(session.expiresAt, Date.now() + idleTtlMs);
356
+ return refresh(session);
357
+ },
358
+ async control(id, input = {}) {
359
+ const session = await get(id);
360
+ const action = requiredString(input.action, 'action', 64);
361
+ if (!CONTROL_ACTIONS.has(action)) throw new TypeError(`unsupported browser control action: ${action}`);
362
+ const result = await provider.control(session.handle, { ...input, action, tabId: input.tabId || session.activeTabId });
363
+ session.idleExpiresAt = Math.min(session.expiresAt, Date.now() + idleTtlMs);
364
+ return { ok: true, result, session: await refresh(session) };
365
+ },
366
+ async render(id) {
367
+ const session = await get(id);
368
+ const render = await provider.render(session.handle);
369
+ if (!render || typeof render !== 'object') throw new Error('browser provider returned no render result');
370
+ return render;
371
+ },
372
+ burn,
373
+ async close() {
374
+ await Promise.all([...sessions.values()].filter((session) => session.state !== 'burned').map((session) => burn(session.id)));
375
+ },
376
+ });
377
+ }
378
+
379
+ export function createEgressAttestationVerifier(options = {}) {
380
+ const publicKey = createPublicKey(requiredString(options.publicKey, 'egress attestation public key', 16_384));
381
+ const clockSkewMs = boundedInteger(options.clockSkewMs, 'clockSkewMs', 0, 300_000, 30_000);
382
+ return async function verifyEgressAttestation(attestation, context) {
383
+ try {
384
+ if (!attestation || typeof attestation !== 'object') return false;
385
+ const payloadBytes = Buffer.from(requiredString(attestation.payload, 'egress attestation payload', 32_768), 'base64url');
386
+ const signature = Buffer.from(requiredString(attestation.signature, 'egress attestation signature', 4_096), 'base64url');
387
+ if (!verifySignature(null, payloadBytes, publicKey, signature)) return false;
388
+ const payload = JSON.parse(payloadBytes.toString('utf8'));
389
+ if (payload.schema !== 'enigma.browser_egress_attestation.v1') return false;
390
+ if (payload.provider !== context.provider || payload.providerSessionId !== context.providerSessionId) return false;
391
+ if (payload.policyHash !== context.policy.policyHash) return false;
392
+ if (payload.viewerOrigin !== context.viewerOrigin) return false;
393
+ if ((payload.viewerUrl || null) !== (context.viewerUrl || null)) return false;
394
+ const expiresAt = Date.parse(payload.expiresAt);
395
+ if (!Number.isFinite(expiresAt) || expiresAt + clockSkewMs < Date.now()) return false;
396
+ return true;
397
+ } catch {
398
+ return false;
399
+ }
400
+ };
401
+ }
@@ -0,0 +1,265 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ export const ENIGMA_CHAT_RESPONSE_SCHEMA = 'enigma.chat_response.v1';
4
+
5
+ function normalizeRequest(input = {}) {
6
+ if (!Array.isArray(input.messages) || !input.messages.length) throw new TypeError('messages must be a non-empty array');
7
+ const messages = input.messages.map((message, index) => {
8
+ if (!message || typeof message !== 'object') throw new TypeError(`messages[${index}] must be an object`);
9
+ const role = String(message.role || '');
10
+ if (!['system', 'user', 'assistant', 'tool'].includes(role)) throw new TypeError(`messages[${index}].role is invalid`);
11
+ if (typeof message.content !== 'string' && !Array.isArray(message.content)) throw new TypeError(`messages[${index}].content is invalid`);
12
+ return { ...message, role };
13
+ });
14
+ const privacyMode = String(input.privacyMode ?? input.privacy_mode ?? 'smart').toLowerCase();
15
+ if (!['off', 'smart', 'full'].includes(privacyMode)) throw new TypeError('privacy mode must be off, smart, or full');
16
+ const reasoningEffort = input.reasoningEffort ?? input.reasoning_effort;
17
+ if (reasoningEffort !== undefined && !['none', 'low', 'medium', 'high'].includes(reasoningEffort)) {
18
+ throw new TypeError('reasoning effort must be none, low, medium, or high');
19
+ }
20
+ return {
21
+ messages,
22
+ model: String(input.model || 'enigma-auto'),
23
+ temperature: Number.isFinite(input.temperature) ? Math.max(0, Math.min(2, input.temperature)) : 0.7,
24
+ maxTokens: Number.isInteger(input.maxTokens ?? input.max_tokens) ? Math.max(1, Math.min(131_072, input.maxTokens ?? input.max_tokens)) : 2_048,
25
+ stream: input.stream === true,
26
+ privacyMode,
27
+ privacyProof: input.privacyProof !== false && input.privacy_proof !== false,
28
+ reasoningEffort,
29
+ webSearch: input.webSearch === true || input.web_search === true,
30
+ tools: Array.isArray(input.tools) ? input.tools : undefined,
31
+ requirePrivateRoute: input.requirePrivateRoute === true || input.require_private_route === true,
32
+ council: normalizeCouncil(input.council),
33
+ signal: input.signal,
34
+ };
35
+ }
36
+
37
+ function normalizeCouncil(value) {
38
+ if (!value) return null;
39
+ if (typeof value !== 'object') throw new TypeError('council must be an object');
40
+ const models = [...new Set((value.models || []).map(String).filter(Boolean))].slice(0, 8);
41
+ if (models.length < 2) throw new TypeError('council requires at least two distinct models');
42
+ return { models, synthesizer: String(value.synthesizer || 'enigma-auto') };
43
+ }
44
+
45
+ function sumUsage(results) {
46
+ return results.reduce((sum, result) => {
47
+ const usage = result?.usage || {};
48
+ sum.prompt_tokens += Number(usage.prompt_tokens || usage.input_tokens || 0);
49
+ sum.completion_tokens += Number(usage.completion_tokens || usage.output_tokens || 0);
50
+ sum.total_tokens += Number(usage.total_tokens || 0);
51
+ return sum;
52
+ }, { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
53
+ }
54
+
55
+ function sseEvent(type, payload) {
56
+ return { type, at: Date.now(), ...payload };
57
+ }
58
+
59
+ export function createChatBackend(options = {}) {
60
+ const router = options.router;
61
+ function routeDisclosure(route, fallbackModel) {
62
+ return {
63
+ model: route?.model || fallbackModel || 'unknown',
64
+ provider: route?.provider || 'unknown',
65
+ trust: route?.trust || 'unknown',
66
+ retentionPolicy: route?.retentionPolicy || 'unknown',
67
+ retentionEvidence: route?.retentionEvidence || null,
68
+ };
69
+ }
70
+
71
+ function aggregateRetention(routes) {
72
+ const normalized = routes.map((route) => routeDisclosure(route));
73
+ const policies = new Set(normalized.map((route) => route.retentionPolicy));
74
+ const policy = policies.has('unknown') ? 'unknown' : policies.size === 1 ? [...policies][0] : 'mixed';
75
+ return { policy, routes: normalized };
76
+ }
77
+
78
+ const privacy = options.privacy;
79
+ if (!router || typeof router.complete !== 'function' || typeof router.listModels !== 'function') {
80
+ throw new TypeError('chat backend requires a model router');
81
+ }
82
+ if (!privacy || typeof privacy.minimize !== 'function') throw new TypeError('chat backend requires a Smart Privacy engine');
83
+
84
+ async function runSingle(request, minimized, privateState, callbacks) {
85
+ let sawStreamDelta = false;
86
+ const result = await router.complete({
87
+ model: request.model,
88
+ messages: minimized.messages,
89
+ temperature: request.temperature,
90
+ maxTokens: request.maxTokens,
91
+ stream: request.stream,
92
+ reasoningEffort: request.reasoningEffort,
93
+ webSearch: request.webSearch,
94
+ tools: request.tools,
95
+ requirePrivateRoute: request.requirePrivateRoute,
96
+ safeForExternal: minimized.safeForExternal,
97
+ signal: request.signal,
98
+ }, {
99
+ onEvent(event) {
100
+ if (event.type === 'delta') {
101
+ sawStreamDelta = true;
102
+ const delta = privateState.restoreChunk(event.delta);
103
+ if (delta) callbacks.onEvent?.(sseEvent('delta', { delta }));
104
+ return;
105
+ }
106
+ if (event.type === 'thinking') {
107
+ const delta = privateState.restore(event.delta);
108
+ callbacks.onEvent?.(sseEvent('thinking', { delta }));
109
+ return;
110
+ }
111
+ callbacks.onEvent?.(sseEvent(event.type, event));
112
+ },
113
+ });
114
+ if (sawStreamDelta) {
115
+ const tail = privateState.restoreChunk('', { final: true });
116
+ if (tail) callbacks.onEvent?.(sseEvent('delta', { delta: tail }));
117
+ }
118
+ return { ...result, answer: privateState.restore(result.answer), thinking: result.thinking ? privateState.restore(result.thinking) : null };
119
+ }
120
+
121
+ async function runCouncil(request, minimized, privateState, callbacks) {
122
+ callbacks.onEvent?.(sseEvent('council', { status: 'started', panelSize: request.council.models.length }));
123
+ const candidates = await Promise.all(request.council.models.map(async (model, index) => {
124
+ callbacks.onEvent?.(sseEvent('council', { status: 'candidate_started', index, model }));
125
+ let attemptedRoute = routeDisclosure(null, model);
126
+ try {
127
+ const result = await router.complete({
128
+ model,
129
+ messages: minimized.messages,
130
+ temperature: request.temperature,
131
+ maxTokens: request.maxTokens,
132
+ stream: false,
133
+ reasoningEffort: request.reasoningEffort,
134
+ webSearch: request.webSearch,
135
+ requirePrivateRoute: request.requirePrivateRoute,
136
+ safeForExternal: minimized.safeForExternal,
137
+ signal: request.signal,
138
+ }, {
139
+ onEvent(event) {
140
+ if (event.type === 'route') attemptedRoute = routeDisclosure(event, model);
141
+ },
142
+ });
143
+ attemptedRoute = routeDisclosure(result.route, model);
144
+ callbacks.onEvent?.(sseEvent('council', { status: 'candidate_done', index, model }));
145
+ return { model, result, attemptedRoute };
146
+ } catch (error) {
147
+ callbacks.onEvent?.(sseEvent('council', { status: 'candidate_failed', index, model, message: error.message }));
148
+ return { model, error, attemptedRoute };
149
+ }
150
+ }));
151
+ const successful = candidates.filter((candidate) => candidate.result);
152
+ if (!successful.length) {
153
+ const error = new Error('every council candidate failed');
154
+ error.code = 'council_failed';
155
+ error.retentionDisclosure = aggregateRetention(candidates.map((candidate) => candidate.attemptedRoute));
156
+ throw error;
157
+ }
158
+ const candidateText = successful.map((candidate, index) => `Candidate ${index + 1} (${candidate.model}):\n${candidate.result.answer}`).join('\n\n');
159
+ const synthesisMessages = [
160
+ ...minimized.messages,
161
+ {
162
+ role: 'system',
163
+ content: 'Synthesize the strongest complete answer from the candidate responses below. Correct errors; do not mention the panel or candidates.',
164
+ },
165
+ { role: 'user', content: candidateText },
166
+ ];
167
+ callbacks.onEvent?.(sseEvent('council', { status: 'synthesizing', successful: successful.length, failed: candidates.length - successful.length }));
168
+ let synthesisRoute = routeDisclosure(null, request.council.synthesizer);
169
+ const synthesis = await router.complete({
170
+ model: request.council.synthesizer,
171
+ messages: synthesisMessages,
172
+ temperature: Math.min(request.temperature, 0.5),
173
+ maxTokens: request.maxTokens,
174
+ stream: false,
175
+ reasoningEffort: request.reasoningEffort || 'high',
176
+ requirePrivateRoute: request.requirePrivateRoute,
177
+ safeForExternal: minimized.safeForExternal,
178
+ signal: request.signal,
179
+ }, {
180
+ onEvent(event) {
181
+ if (event.type === 'route') synthesisRoute = routeDisclosure(event, request.council.synthesizer);
182
+ },
183
+ });
184
+ synthesisRoute = routeDisclosure(synthesis.route, request.council.synthesizer);
185
+ callbacks.onEvent?.(sseEvent('council', { status: 'done', successful: successful.length, failed: candidates.length - successful.length }));
186
+ return {
187
+ ...synthesis,
188
+ answer: privateState.restore(synthesis.answer),
189
+ thinking: synthesis.thinking ? privateState.restore(synthesis.thinking) : null,
190
+ usage: sumUsage([...successful.map((candidate) => candidate.result), synthesis]),
191
+ retentionDisclosure: aggregateRetention([
192
+ ...candidates.map((candidate) => candidate.attemptedRoute),
193
+ synthesisRoute,
194
+ ]),
195
+ council: {
196
+ models: request.council.models,
197
+ synthesizer: synthesis.route.model,
198
+ successful: successful.length,
199
+ failed: candidates.length - successful.length,
200
+ receipts: [...successful.map((candidate) => candidate.result.receipt), synthesis.receipt],
201
+ retention_routes: [
202
+ ...candidates.map((candidate) => ({ ...candidate.attemptedRoute, status: candidate.result ? 'succeeded' : 'failed' })),
203
+ { ...synthesisRoute, status: 'succeeded', role: 'synthesizer' },
204
+ ],
205
+ },
206
+ };
207
+ }
208
+
209
+ return Object.freeze({
210
+ listModels: () => router.listModels(),
211
+ async complete(input = {}, callbacks = {}) {
212
+ const request = normalizeRequest(input);
213
+ callbacks.onEvent?.(sseEvent('privacy', { status: 'minimizing', mode: request.privacyMode }));
214
+ const minimized = await privacy.minimize(request.messages, { mode: request.privacyMode });
215
+ const privateState = minimized.privateState;
216
+ callbacks.onEvent?.(sseEvent('privacy', {
217
+ status: 'ready',
218
+ mode: request.privacyMode,
219
+ summary: minimized.receipt.summary,
220
+ safeForExternal: minimized.safeForExternal,
221
+ }));
222
+ const startedAt = Date.now();
223
+ try {
224
+ const result = request.council
225
+ ? await runCouncil(request, minimized, privateState, callbacks)
226
+ : await runSingle(request, minimized, privateState, callbacks);
227
+ const response = {
228
+ id: `chatcmpl_${randomUUID()}`,
229
+ object: 'chat.completion',
230
+ created: Math.floor(Date.now() / 1_000),
231
+ model: result.route.model,
232
+ choices: [{
233
+ index: 0,
234
+ message: { role: 'assistant', content: result.answer, ...(result.thinking ? { reasoning_content: result.thinking } : {}) },
235
+ finish_reason: result.finishReason || 'stop',
236
+ }],
237
+ usage: result.usage || null,
238
+ enigma_parameters: {
239
+ schema: ENIGMA_CHAT_RESPONSE_SCHEMA,
240
+ route: result.route,
241
+ route_receipt: result.receipt,
242
+ privacy: request.privacyProof ? minimized.receipt : undefined,
243
+ council: result.council || undefined,
244
+ retention: result.retentionDisclosure?.policy || result.route.retentionPolicy || 'unknown',
245
+ retention_evidence: result.retentionDisclosure?.routes || result.route.retentionEvidence || null,
246
+ elapsed_ms: Date.now() - startedAt,
247
+ },
248
+ };
249
+ await options.onUsage?.({
250
+ modality: 'text',
251
+ provider: result.route.provider,
252
+ model: result.route.model,
253
+ inputTokens: Number(result.usage?.prompt_tokens || 0),
254
+ outputTokens: Number(result.usage?.completion_tokens || 0),
255
+ totalTokens: Number(result.usage?.total_tokens || 0),
256
+ routeReceiptId: result.receipt.receiptId,
257
+ });
258
+ callbacks.onEvent?.(sseEvent('done', { response }));
259
+ return response;
260
+ } finally {
261
+ privateState.dispose();
262
+ }
263
+ },
264
+ });
265
+ }