brane 0.1.1 → 0.3.1

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 (352) hide show
  1. package/AGENTS.md +367 -0
  2. package/CHANGELOG.md +66 -0
  3. package/CONTRIBUTING.md +101 -0
  4. package/DESIGN.md +405 -0
  5. package/Dockerfile +61 -0
  6. package/LICENSE +656 -17
  7. package/README.md +174 -2
  8. package/SECURITY.md +213 -0
  9. package/dist/absorb/brane-id.js +11 -0
  10. package/dist/absorb/chatgpt.js +161 -0
  11. package/dist/absorb/claude-code.js +504 -0
  12. package/dist/absorb/index.js +543 -0
  13. package/dist/absorb/organelle.js +345 -0
  14. package/dist/absorb/run.js +520 -0
  15. package/dist/absorb/sources.js +168 -0
  16. package/dist/absorb/types.js +3 -0
  17. package/dist/agent-env.js +45 -0
  18. package/dist/agents.js +1168 -0
  19. package/dist/claude-live-session.js +124 -0
  20. package/dist/cli.js +50 -0
  21. package/dist/codex-tool-log.js +144 -0
  22. package/dist/commands/absorb.js +291 -0
  23. package/dist/commands/attach.js +31 -0
  24. package/dist/commands/certify.js +62 -0
  25. package/dist/commands/config.js +133 -0
  26. package/dist/commands/cost.js +126 -0
  27. package/dist/commands/daemon.js +350 -0
  28. package/dist/commands/doctor.js +262 -0
  29. package/dist/commands/fold.js +95 -0
  30. package/dist/commands/list.js +89 -0
  31. package/dist/commands/mcp.js +84 -0
  32. package/dist/commands/meld.js +360 -0
  33. package/dist/commands/memory.js +215 -0
  34. package/dist/commands/revive.js +74 -0
  35. package/dist/commands/run.js +85 -0
  36. package/dist/commands/spawn.js +1101 -0
  37. package/dist/commands/token.js +154 -0
  38. package/dist/copilot-tool-log.js +145 -0
  39. package/dist/cost/pricing.js +116 -0
  40. package/dist/cost/reprice.js +136 -0
  41. package/dist/crush-tool-log.js +293 -0
  42. package/dist/daemon/agent-scope.js +116 -0
  43. package/dist/daemon/awaiting.js +54 -0
  44. package/dist/daemon/cert-manager.js +349 -0
  45. package/dist/daemon/client.js +262 -0
  46. package/dist/daemon/gated-routes.js +112 -0
  47. package/dist/daemon/keep-awake.js +103 -0
  48. package/dist/daemon/local-socket.js +145 -0
  49. package/dist/daemon/presence-gate.js +206 -0
  50. package/dist/daemon/server.js +6353 -0
  51. package/dist/daemon/signal-source.js +35 -0
  52. package/dist/daemon/tailscale-rebind.js +58 -0
  53. package/dist/daemon/token-ring.js +263 -0
  54. package/dist/daemon/token-routes.js +190 -0
  55. package/dist/db/events.js +152 -0
  56. package/dist/db/mentions.js +179 -0
  57. package/dist/db/schema.js +643 -0
  58. package/dist/db/tokens.js +624 -0
  59. package/dist/db/tool-uses.js +647 -0
  60. package/dist/diagram/index.js +85 -0
  61. package/dist/diagram/label.js +455 -0
  62. package/dist/diagram/layout.js +476 -0
  63. package/dist/diagram/links.js +50 -0
  64. package/dist/diagram/metrics.js +152 -0
  65. package/dist/diagram/parse.js +487 -0
  66. package/dist/diagram/svg.js +389 -0
  67. package/dist/diagram/types.js +9 -0
  68. package/dist/export.js +629 -0
  69. package/dist/files/archive-types.js +2 -0
  70. package/dist/files/archive.js +182 -0
  71. package/dist/files/browse.js +327 -0
  72. package/dist/files/bytes.js +162 -0
  73. package/dist/files/codecs.js +33 -0
  74. package/dist/files/find.js +212 -0
  75. package/dist/files/tar.js +223 -0
  76. package/dist/files/zip.js +209 -0
  77. package/dist/folds.js +72 -0
  78. package/dist/grammars/ebnf.js +92 -0
  79. package/dist/highlight.js +250 -0
  80. package/dist/ids.js +24 -0
  81. package/dist/markdown-diagnostics.js +233 -0
  82. package/dist/math.js +168 -0
  83. package/dist/mcp/server.js +886 -0
  84. package/dist/memory/embedder.js +145 -0
  85. package/dist/memory/embedding-worker.js +71 -0
  86. package/dist/memory/embeddings.js +91 -0
  87. package/dist/memory/index.js +232 -0
  88. package/dist/memory/storage.js +587 -0
  89. package/dist/memory/wiki.js +688 -0
  90. package/dist/memory/wordpiece.js +247 -0
  91. package/dist/mentions-scan.js +40 -0
  92. package/dist/presence.js +259 -0
  93. package/dist/session-affinity.js +185 -0
  94. package/dist/shared/api-request.js +124 -0
  95. package/dist/shared/app-origin.js +26 -0
  96. package/dist/shared/block-glyphs.js +114 -0
  97. package/dist/shared/diagram-css.js +17 -0
  98. package/dist/shared/export-selection.js +101 -0
  99. package/dist/shared/find-in-text.js +110 -0
  100. package/dist/shared/glow-hues.js +225 -0
  101. package/dist/shared/hex-dump.js +62 -0
  102. package/dist/shared/hex-pages.js +52 -0
  103. package/dist/shared/hit-drum.js +79 -0
  104. package/dist/shared/known-nids.js +58 -0
  105. package/dist/shared/list-scroll-keys.js +56 -0
  106. package/dist/shared/markdown-lists.js +72 -0
  107. package/dist/shared/markdown-math.js +485 -0
  108. package/dist/shared/markdown-mentions.js +79 -0
  109. package/dist/shared/markdown-nids.js +41 -0
  110. package/dist/shared/markdown-protocols.js +53 -0
  111. package/dist/shared/markdown-refs.js +123 -0
  112. package/dist/shared/membrane-invasion.js +342 -0
  113. package/dist/shared/membrane-motion.js +83 -0
  114. package/dist/shared/membrane-palette.js +253 -0
  115. package/dist/shared/membrane-terrain.js +212 -0
  116. package/dist/shared/message-selection.js +123 -0
  117. package/dist/shared/mo-glyph.js +57 -0
  118. package/dist/shared/narrative-format.js +237 -0
  119. package/dist/shared/narrative-menu.js +84 -0
  120. package/dist/shared/narrative-sort.js +47 -0
  121. package/dist/shared/narrative-status.js +52 -0
  122. package/dist/shared/no-pan.js +85 -0
  123. package/dist/shared/pane-layout.js +50 -0
  124. package/dist/shared/payload-inline.js +83 -0
  125. package/dist/shared/speaker-color.js +61 -0
  126. package/dist/shared/token-rotation.js +14 -0
  127. package/dist/shared/view-url.js +216 -0
  128. package/dist/shared/wiki-links.js +70 -0
  129. package/dist/shared/wiki-window.js +4 -0
  130. package/dist/tailscale.js +55 -0
  131. package/dist/test/absorb-claude.test.js +892 -0
  132. package/dist/test/agent-scope.test.js +193 -0
  133. package/dist/test/agents.test.js +55 -0
  134. package/dist/test/api-request.test.js +122 -0
  135. package/dist/test/app-host-literal.test.js +53 -0
  136. package/dist/test/awaiting-glow.test.js +54 -0
  137. package/dist/test/awaiting.test.js +122 -0
  138. package/dist/test/bind-token.test.js +88 -0
  139. package/dist/test/block-glyphs.test.js +68 -0
  140. package/dist/test/cert-manager.test.js +262 -0
  141. package/dist/test/claude-live-session.test.js +110 -0
  142. package/dist/test/codex-tool-log.test.js +568 -0
  143. package/dist/test/config.test.js +71 -0
  144. package/dist/test/copilot-tool-log.test.js +93 -0
  145. package/dist/test/cors-preflight.test.js +56 -0
  146. package/dist/test/cost-linger.test.js +31 -0
  147. package/dist/test/cost-pricing.test.js +72 -0
  148. package/dist/test/cost-reprice.test.js +139 -0
  149. package/dist/test/cost-sessions.test.js +301 -0
  150. package/dist/test/crush-tool-log.test.js +289 -0
  151. package/dist/test/daemon-client.test.js +109 -0
  152. package/dist/test/daemon-socket.test.js +293 -0
  153. package/dist/test/diagram-arrows.test.js +205 -0
  154. package/dist/test/diagram-export.test.js +210 -0
  155. package/dist/test/diagram-labels.test.js +313 -0
  156. package/dist/test/diagram-parse.test.js +191 -0
  157. package/dist/test/diagram-render.test.js +175 -0
  158. package/dist/test/diagram-review.test.js +288 -0
  159. package/dist/test/diagram-self-loop-labels.test.js +303 -0
  160. package/dist/test/embedder.test.js +84 -0
  161. package/dist/test/events.test.js +123 -0
  162. package/dist/test/export-selection.test.js +102 -0
  163. package/dist/test/export.test.js +505 -0
  164. package/dist/test/files-browse.test.js +393 -0
  165. package/dist/test/files-find.test.js +272 -0
  166. package/dist/test/find-in-text.test.js +86 -0
  167. package/dist/test/first-paint-viewport.test.js +59 -0
  168. package/dist/test/fixtures/embedding-golden.js +23 -0
  169. package/dist/test/fixtures/wordpiece-golden.js +167 -0
  170. package/dist/test/glow-hues.test.js +202 -0
  171. package/dist/test/harness-cost.test.js +423 -0
  172. package/dist/test/helpers.js +21 -0
  173. package/dist/test/hex-pages.test.js +68 -0
  174. package/dist/test/highlight.test.js +69 -0
  175. package/dist/test/hit-drum.test.js +71 -0
  176. package/dist/test/hook-config.test.js +299 -0
  177. package/dist/test/json-gzip.test.js +70 -0
  178. package/dist/test/key-routing.test.js +58 -0
  179. package/dist/test/known-nids.test.js +106 -0
  180. package/dist/test/link-preview.test.js +67 -0
  181. package/dist/test/list-scroll-keys.test.js +66 -0
  182. package/dist/test/listen-own-words.test.js +147 -0
  183. package/dist/test/listen-timeout.test.js +33 -0
  184. package/dist/test/markdown-diagnostics.test.js +121 -0
  185. package/dist/test/markdown-lists.test.js +182 -0
  186. package/dist/test/markdown-math.test.js +256 -0
  187. package/dist/test/markdown-nids.test.js +146 -0
  188. package/dist/test/markdown-protocols.test.js +139 -0
  189. package/dist/test/mcp-bridge-cancel.test.js +98 -0
  190. package/dist/test/mcp-bridge-reconnect.test.js +121 -0
  191. package/dist/test/mcp-narrative.test.js +193 -0
  192. package/dist/test/mcp.test.js +548 -0
  193. package/dist/test/meld.test.js +143 -0
  194. package/dist/test/membrane-invasion.test.js +482 -0
  195. package/dist/test/membrane-motion.test.js +101 -0
  196. package/dist/test/membrane-palette.test.js +166 -0
  197. package/dist/test/membrane-terrain.test.js +170 -0
  198. package/dist/test/memory-log.test.js +202 -0
  199. package/dist/test/mentions-e2e.test.js +261 -0
  200. package/dist/test/mentions-ledger.test.js +265 -0
  201. package/dist/test/mentions-listen.test.js +265 -0
  202. package/dist/test/mentions.test.js +238 -0
  203. package/dist/test/message-selection.test.js +114 -0
  204. package/dist/test/mo-glyph.test.js +22 -0
  205. package/dist/test/model-resolution.test.js +166 -0
  206. package/dist/test/narrate-validate.test.js +77 -0
  207. package/dist/test/narrate-wait.test.js +160 -0
  208. package/dist/test/narrative-format.test.js +219 -0
  209. package/dist/test/narrative-menu.test.js +150 -0
  210. package/dist/test/narrative-sort.test.js +75 -0
  211. package/dist/test/no-pan.test.js +60 -0
  212. package/dist/test/pane-layout.test.js +153 -0
  213. package/dist/test/paths.test.js +37 -0
  214. package/dist/test/payload-inline.test.js +91 -0
  215. package/dist/test/precache-fonts.test.js +72 -0
  216. package/dist/test/presence-gate.test.js +440 -0
  217. package/dist/test/presence.test.js +160 -0
  218. package/dist/test/read-events.test.js +220 -0
  219. package/dist/test/remote-meld.test.js +128 -0
  220. package/dist/test/revive-cli.test.js +93 -0
  221. package/dist/test/search-floor.test.js +206 -0
  222. package/dist/test/search-fusion.test.js +168 -0
  223. package/dist/test/search.test.js +205 -0
  224. package/dist/test/security.test.js +48 -0
  225. package/dist/test/selection-wiring.test.js +61 -0
  226. package/dist/test/session-affinity.test.js +295 -0
  227. package/dist/test/signal-source.test.js +32 -0
  228. package/dist/test/socket-account.test.js +268 -0
  229. package/dist/test/spawn-toml.test.js +72 -0
  230. package/dist/test/speaker-color.test.js +90 -0
  231. package/dist/test/tailscale-rebind.test.js +79 -0
  232. package/dist/test/terminal-cursor.test.js +61 -0
  233. package/dist/test/terminal-resync.test.js +23 -0
  234. package/dist/test/terminal-size.test.js +36 -0
  235. package/dist/test/thin-cli.test.js +382 -0
  236. package/dist/test/token-bootstrap.test.js +272 -0
  237. package/dist/test/token-devices.test.js +646 -0
  238. package/dist/test/token-lapse.test.js +94 -0
  239. package/dist/test/token-ring.test.js +188 -0
  240. package/dist/test/token-rotate-cli.test.js +58 -0
  241. package/dist/test/token-routes.test.js +95 -0
  242. package/dist/test/tool-uses.test.js +1006 -0
  243. package/dist/test/usage-ledger.test.js +126 -0
  244. package/dist/test/view-url.test.js +627 -0
  245. package/dist/test/wiki-links.test.js +57 -0
  246. package/dist/test/wiki-sources.test.js +85 -0
  247. package/dist/test/wiki-window.test.js +31 -0
  248. package/dist/test/wiki.test.js +399 -0
  249. package/dist/test/wordpiece.test.js +68 -0
  250. package/dist/test/wrangler-route.test.js +27 -0
  251. package/dist/tls.js +148 -0
  252. package/dist/tmux.js +213 -0
  253. package/dist/user-agent.js +46 -0
  254. package/docker-entrypoint.sh +154 -0
  255. package/docs/README.md +3 -0
  256. package/docs/absorb.md +231 -0
  257. package/docs/cost-ledger.md +125 -0
  258. package/docs/daemon-api.md +189 -0
  259. package/docs/diagrams.md +163 -0
  260. package/docs/doctor.md +36 -0
  261. package/docs/export-scope.md +69 -0
  262. package/docs/files-view.md +133 -0
  263. package/docs/link-previews.md +38 -0
  264. package/docs/math.md +32 -0
  265. package/docs/mcp.md +69 -0
  266. package/docs/memory-search.md +75 -0
  267. package/docs/mentions.md +101 -0
  268. package/docs/multi-brane.md +14 -0
  269. package/docs/passkeys.md +165 -0
  270. package/docs/presence.md +202 -0
  271. package/docs/releasing.md +104 -0
  272. package/docs/rendering-warnings.md +22 -0
  273. package/docs/request-layer.md +72 -0
  274. package/docs/selection-store.md +50 -0
  275. package/docs/session-affinity.md +61 -0
  276. package/docs/terminal-mirroring.md +243 -0
  277. package/docs/token-rotation.md +431 -0
  278. package/docs/view-urls.md +134 -0
  279. package/docs/web-app.md +23 -0
  280. package/docs/wiki.md +43 -0
  281. package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
  282. package/expo/dist/apple-touch-icon.png +0 -0
  283. package/expo/dist/favicon.svg +3 -0
  284. package/expo/dist/index.html +177 -0
  285. package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  286. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  287. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  288. package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  289. package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  290. package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  291. package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  292. package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  293. package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  294. package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  295. package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  296. package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  297. package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  298. package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  299. package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  300. package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  301. package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  302. package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  303. package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  304. package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  305. package/expo/dist/katex/katex.min.css +1 -0
  306. package/expo/dist/manifest.json +16 -0
  307. package/expo/dist/metadata.json +1 -0
  308. package/expo/dist/og/membrane.v1.png +0 -0
  309. package/expo/dist/pdf.min.mjs +28 -0
  310. package/expo/dist/pdf.worker.min.mjs +28 -0
  311. package/expo/dist/sw.js +154 -0
  312. package/package.json +79 -27
  313. package/protocols/LIVE_loop.md +407 -0
  314. package/protocols/bebrief.md +41 -0
  315. package/protocols/digest.md +231 -0
  316. package/protocols/fly.md +266 -0
  317. package/protocols/idle.md +81 -0
  318. package/protocols/keeper.md +829 -0
  319. package/protocols/markdown.md +80 -0
  320. package/protocols/memory.md +419 -0
  321. package/protocols/mentions.md +93 -0
  322. package/protocols/payloads.md +53 -0
  323. package/protocols/prelude.md +91 -0
  324. package/protocols/ramify.md +105 -0
  325. package/protocols/review.md +356 -0
  326. package/protocols/rrr.md +232 -0
  327. package/protocols/scribe.md +429 -0
  328. package/protocols/summarize.md +203 -0
  329. package/protocols/twin.md +174 -0
  330. package/protocols/wiki.md +438 -0
  331. package/protocols/worker.md +38 -0
  332. package/wiki/shared/append-only-event-log.md +148 -0
  333. package/wiki/shared/index.md +53 -0
  334. package/wiki/shared/owning-your-narratives.md +161 -0
  335. package/wiki/shared/premature-ontologies.md +435 -0
  336. package/wiki/shared/sedimentary-superintelligence.md +128 -0
  337. package/wiki/shared/sequestering-the-narrative.md +143 -0
  338. package/wiki/shared/why-wikis-work.md +216 -0
  339. package/.travis.yml +0 -7
  340. package/lib/brane.cjs.js +0 -12
  341. package/lib/brane.cjs.js.map +0 -1
  342. package/lib/brane.d.ts +0 -11
  343. package/lib/brane.js +0 -6
  344. package/lib/brane.js.map +0 -1
  345. package/lib/tests.d.ts +0 -1
  346. package/lib/tests.js +0 -8
  347. package/lib/tests.js.map +0 -1
  348. package/rollup.config.js +0 -21
  349. package/scripts/test.sh +0 -8
  350. package/src/brane.ts +0 -21
  351. package/src/tests.ts +0 -8
  352. package/tsconfig.json +0 -16
@@ -0,0 +1,121 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { spawn } from "node:child_process";
4
+ import { createServer } from "node:http";
5
+ import { mkdtempSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { dirname, join } from "node:path";
8
+ import { fileURLToPath, pathToFileURL } from "node:url";
9
+ import { WebSocketServer } from "ws";
10
+ // The bridge across a lost connection, driven for real: a child process runs
11
+ // `startMcpBridge` against a WebSocket server this test plays daemon on, and the
12
+ // test plays the agent on the child's stdio.
13
+ //
14
+ // A call the bridge had *sent* when the connection went is ambiguous — the
15
+ // daemon may have run it — so the agent is told so. A call still *queued* was
16
+ // never sent, and must go out on reconnect exactly once, with no error. The
17
+ // bridge used to fail both, and then send the queued one anyway: an error the
18
+ // agent would act on (retrying ran the call twice), and a waiting narrate
19
+ // parked on the daemon with nobody left to answer it.
20
+ const BRIDGE = pathToFileURL(join(dirname(fileURLToPath(import.meta.url)), "..", "mcp", "server.js")).href;
21
+ function childEnv(port) {
22
+ const env = { ...process.env, PORT: String(port), HOME: mkdtempSync(join(tmpdir(), "brane-bridge-")) };
23
+ // Whatever session this suite runs inside must not leak into the child's dial.
24
+ for (const k of Object.keys(env)) {
25
+ if (k.startsWith("BRANE_") || k === "CLAUDE_CODE_SESSION_ID" || k === "NODE_TEST_CONTEXT")
26
+ delete env[k];
27
+ }
28
+ return env;
29
+ }
30
+ describe("the MCP bridge across a lost connection", () => {
31
+ it("fails the call it had sent, and sends the queued one once, without an error", async () => {
32
+ const received = [];
33
+ const urls = [];
34
+ let conn = 0;
35
+ const sockets = [];
36
+ const server = createServer();
37
+ const wss = new WebSocketServer({ noServer: true });
38
+ server.on("upgrade", (req, sock, head) => {
39
+ const n = ++conn;
40
+ urls.push(req.url ?? "");
41
+ // The second dial is refused, so the queued call has to survive a failed
42
+ // reconnect (which fires `close`, and with it the fail path) too.
43
+ if (n === 2) {
44
+ sock.write("HTTP/1.1 503 Busy\r\n\r\n");
45
+ sock.destroy();
46
+ return;
47
+ }
48
+ wss.handleUpgrade(req, sock, head, (ws) => {
49
+ sockets.push(ws);
50
+ ws.send(JSON.stringify({ type: "welcome", brane_id: "b", narrative_id: "external-test0001", bind_secret: "s3cret" }));
51
+ ws.on("message", (d) => {
52
+ const msg = JSON.parse(d.toString());
53
+ received.push({ conn: n, msg });
54
+ if (n === 1 && msg.type === "tool_call") {
55
+ ws.terminate(); // the daemon took the call and the connection died
56
+ }
57
+ else if (msg.type === "tool_call") {
58
+ ws.send(JSON.stringify({ type: "tool_result", id: msg.id, result: { ok: msg.tool } }));
59
+ }
60
+ });
61
+ });
62
+ });
63
+ await new Promise((r) => server.listen(0, "127.0.0.1", () => r()));
64
+ const port = server.address().port;
65
+ const child = spawn(process.execPath, ["--input-type=module", "-e",
66
+ `const { startMcpBridge } = await import(${JSON.stringify(BRIDGE)});
67
+ startMcpBridge(async () => "ws://127.0.0.1:" + process.env.PORT + "/ws/mcp?token=t");`], { env: childEnv(port), stdio: ["pipe", "pipe", "pipe"] });
68
+ const replies = new Map();
69
+ let buf = "";
70
+ child.stdout.on("data", (d) => {
71
+ buf += d.toString();
72
+ let i;
73
+ while ((i = buf.indexOf("\n")) >= 0) {
74
+ const line = buf.slice(0, i);
75
+ buf = buf.slice(i + 1);
76
+ if (line.trim()) {
77
+ const m = JSON.parse(line);
78
+ replies.set(m.id, m);
79
+ }
80
+ }
81
+ });
82
+ const call = (id, name, args) => child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method: "tools/call", params: { name, arguments: args } }) + "\n");
83
+ const until = async (pred, ms) => {
84
+ const end = Date.now() + ms;
85
+ while (!pred()) {
86
+ if (Date.now() > end)
87
+ throw new Error("timed out waiting");
88
+ await new Promise((r) => setTimeout(r, 25));
89
+ }
90
+ };
91
+ try {
92
+ await until(() => sockets.length === 1, 3000);
93
+ call(1, "narrate", { say: "hello", wait: true }); // sent on conn 1, then the conn dies
94
+ await until(() => replies.has(1), 3000);
95
+ call(2, "listen", { after: 1, wait: false }); // queued: no connection right now
96
+ await until(() => replies.has(2), 8000); // answered by conn 3
97
+ const r1 = replies.get(1).result;
98
+ assert.equal(r1.isError, true, "the sent call is ambiguous and says so");
99
+ assert.match(r1.content[0].text, /may already have been posted/);
100
+ const r2 = replies.get(2).result;
101
+ assert.ok(!r2.isError, "the queued call was never sent, so it is not failed");
102
+ assert.deepEqual(JSON.parse(r2.content[0].text), { ok: "listen" });
103
+ const calls = received.filter((f) => f.msg.type === "tool_call");
104
+ assert.deepEqual(calls.map((f) => [f.conn, f.msg.tool]), [[1, "narrate"], [3, "listen"]], "each call reaches the daemon exactly once: the failed one is not replayed");
105
+ // #497: an unspawned client adopts the narrative *and* the secret its
106
+ // first welcome hands it, so its reconnect proves it rather than being
107
+ // refused and minted a different narrative.
108
+ const redial = new URL(urls[2], "ws://x");
109
+ assert.equal(redial.searchParams.get("nid"), "external-test0001");
110
+ assert.equal(redial.searchParams.get("secret"), "s3cret");
111
+ }
112
+ finally {
113
+ child.kill();
114
+ for (const s of sockets)
115
+ s.terminate();
116
+ wss.close();
117
+ server.close();
118
+ }
119
+ });
120
+ });
121
+ //# sourceMappingURL=mcp-bridge-reconnect.test.js.map
@@ -0,0 +1,193 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createTestDb } from "./helpers.js";
4
+ import { adoptBindTokens } from "../db/tokens.js";
5
+ import { ensureMcpNarrative, mintExternalNarrative, mintExternalNarrativeWithSecret, resolveMcpBind, randomBase36 } from "../daemon/server.js";
6
+ function getNarrative(db, id) {
7
+ return db
8
+ .prepare("SELECT id, brane_id, agent_type, role, status FROM narratives WHERE id = ?")
9
+ .get(id);
10
+ }
11
+ function narrativeCount(db) {
12
+ return db.prepare("SELECT COUNT(*) as n FROM narratives").get().n;
13
+ }
14
+ describe("randomBase36", () => {
15
+ it("produces a string of the requested length", () => {
16
+ for (const len of [1, 8, 16, 64]) {
17
+ assert.equal(randomBase36(len).length, len);
18
+ }
19
+ });
20
+ it("uses only [0-9a-z]", () => {
21
+ // Draw a large sample so every output character is exercised many times.
22
+ const s = randomBase36(2000);
23
+ assert.match(s, /^[0-9a-z]+$/);
24
+ });
25
+ it("covers the full alphabet without bias-induced gaps", () => {
26
+ // Rejection sampling must still reach every symbol, including the high
27
+ // ones (z = index 35) that a naive `byte % 36` would over- or
28
+ // under-represent. With 5000 draws, all 36 symbols should appear.
29
+ const seen = new Set(randomBase36(5000).split(""));
30
+ assert.equal(seen.size, 36, `expected all 36 symbols, saw ${seen.size}`);
31
+ });
32
+ });
33
+ describe("ensureMcpNarrative", () => {
34
+ it("trusts an id that already names a narrative row (no INSERT, no event)", () => {
35
+ const db = createTestDb();
36
+ db.prepare(`INSERT INTO narratives (id, brane_id, agent_type, role, created_at, status)
37
+ VALUES ('claude-deadbeef', 'brane-test', 'claude', 'reviewer', 0, 'ended')`).run();
38
+ const before = narrativeCount(db);
39
+ const result = ensureMcpNarrative(db, "brane-test", "claude-deadbeef");
40
+ assert.equal(result, "claude-deadbeef");
41
+ assert.equal(narrativeCount(db), before, "must not insert a new row");
42
+ // No seeded event for an existing narrative.
43
+ const events = db
44
+ .prepare("SELECT COUNT(*) as n FROM events WHERE narrative_id = ?")
45
+ .get("claude-deadbeef");
46
+ assert.equal(events.n, 0);
47
+ });
48
+ it("mints a fresh server-side narrative for an unknown (unspawned) id", () => {
49
+ const db = createTestDb();
50
+ const result = ensureMcpNarrative(db, "brane-test", "external-clientchosen");
51
+ // The client-chosen id is discarded; a server id of the canonical shape
52
+ // is returned instead.
53
+ assert.notEqual(result, "external-clientchosen");
54
+ assert.match(result, /^external-[0-9a-z]{8}$/);
55
+ assert.equal(getNarrative(db, "external-clientchosen"), undefined, "client id must not be persisted");
56
+ const row = getNarrative(db, result);
57
+ assert.ok(row, "minted narrative row should exist");
58
+ assert.equal(row.brane_id, "brane-test");
59
+ assert.equal(row.agent_type, "external");
60
+ assert.equal(row.role, "external");
61
+ assert.equal(row.status, "active");
62
+ });
63
+ it("seeds a single `started` event on the minted narrative", () => {
64
+ const db = createTestDb();
65
+ const result = ensureMcpNarrative(db, "brane-test", "external-clientchosen");
66
+ const events = db
67
+ .prepare("SELECT seq, type, content FROM events WHERE narrative_id = ? ORDER BY seq")
68
+ .all(result);
69
+ assert.equal(events.length, 1);
70
+ assert.equal(events[0].seq, 1);
71
+ assert.equal(events[0].type, "started");
72
+ assert.equal(events[0].content, "Connected external MCP client");
73
+ });
74
+ it("mints distinct, unique narratives across repeated unspawned connects", () => {
75
+ // Exercises the uniqueness contract: every unspawned connect produces a
76
+ // fresh row whose id collides with neither the client-supplied value nor
77
+ // any previously minted id. (The internal PRIMARY KEY retry loop is a
78
+ // vanishingly-rare defensive branch and isn't forced here.)
79
+ const db = createTestDb();
80
+ const ids = new Set();
81
+ for (let i = 0; i < 50; i++) {
82
+ ids.add(ensureMcpNarrative(db, "brane-test", "external-placeholder"));
83
+ }
84
+ assert.equal(ids.size, 50, "every unspawned connect should mint a unique id");
85
+ assert.equal(narrativeCount(db), 50);
86
+ });
87
+ });
88
+ describe("resolveMcpBind (per-narrative bind token)", () => {
89
+ // Seed a spawned narrative with a known bind_token + session, and return
90
+ // helpers to inspect it after a bind attempt.
91
+ function seedSpawned(db, id, bindToken, sessionId = "sess-A") {
92
+ db.prepare(`INSERT INTO narratives (id, brane_id, agent_type, role, created_at, status, agent_session_id, bind_token)
93
+ VALUES (?, 'brane-test', 'claude', 'worker', 0, 'active', ?, ?)`).run(id, sessionId, bindToken);
94
+ // As the daemon's start does: the plaintext becomes an agent token row.
95
+ adoptBindTokens(db);
96
+ }
97
+ function eventCount(db, id) {
98
+ return db.prepare("SELECT COUNT(*) as n FROM events WHERE narrative_id = ?").get(id).n;
99
+ }
100
+ function sessionOf(db, id) {
101
+ return db.prepare("SELECT agent_session_id FROM narratives WHERE id = ?").get(id).agent_session_id;
102
+ }
103
+ it("binds to the claimed id when the secret matches", () => {
104
+ const db = createTestDb();
105
+ seedSpawned(db, "claude-alpha", "secret-alpha");
106
+ const result = resolveMcpBind(db, "brane-test", "claude-alpha", "secret-alpha");
107
+ assert.equal(result.nid, "claude-alpha");
108
+ assert.equal(result.minted, false);
109
+ assert.equal(result.rejected, undefined);
110
+ });
111
+ it("rejects a wrong secret: mints external, leaves the claimed narrative untouched", () => {
112
+ const db = createTestDb();
113
+ seedSpawned(db, "claude-alpha", "secret-alpha", "sess-A");
114
+ const result = resolveMcpBind(db, "brane-test", "claude-alpha", "wrong-secret");
115
+ assert.notEqual(result.nid, "claude-alpha");
116
+ assert.match(result.nid, /^external-[0-9a-z]{8}$/);
117
+ assert.equal(result.minted, true);
118
+ assert.deepEqual(result.rejected, { claimedNid: "claude-alpha", reason: "secret_mismatch" });
119
+ // The claimed narrative's session is preserved and nothing leaked into it.
120
+ assert.equal(sessionOf(db, "claude-alpha"), "sess-A", "victim session must be preserved");
121
+ assert.equal(eventCount(db, "claude-alpha"), 0, "no events leak into the claimed narrative");
122
+ });
123
+ it("rejects an absent secret (reason: secret_absent)", () => {
124
+ const db = createTestDb();
125
+ seedSpawned(db, "claude-alpha", "secret-alpha");
126
+ const result = resolveMcpBind(db, "brane-test", "claude-alpha", undefined);
127
+ assert.match(result.nid, /^external-[0-9a-z]{8}$/);
128
+ assert.equal(result.minted, true);
129
+ assert.deepEqual(result.rejected, { claimedNid: "claude-alpha", reason: "secret_absent" });
130
+ assert.equal(sessionOf(db, "claude-alpha"), "sess-A");
131
+ });
132
+ it("mints a fresh external narrative for an unknown (unspawned) id", () => {
133
+ const db = createTestDb();
134
+ const result = resolveMcpBind(db, "brane-test", "external-clientchosen", undefined);
135
+ assert.notEqual(result.nid, "external-clientchosen");
136
+ assert.match(result.nid, /^external-[0-9a-z]{8}$/);
137
+ assert.equal(result.minted, true);
138
+ assert.equal(result.rejected, undefined);
139
+ assert.equal(getNarrative(db, "external-clientchosen"), undefined, "client-chosen id must not be persisted");
140
+ });
141
+ it("a row with a null bind_token is refused, not waved through", () => {
142
+ // The migration grace that used to allow this bind is retired. Such a row
143
+ // has no secret to check against, so there is no way to prove a claim to
144
+ // it: the connection is minted a fresh external id like any other unproven
145
+ // claimant, and the reason names the row rather than the client.
146
+ const db = createTestDb();
147
+ seedSpawned(db, "claude-legacy", null);
148
+ const result = resolveMcpBind(db, "brane-test", "claude-legacy", undefined);
149
+ assert.match(result.nid, /^external-[0-9a-z]{8}$/);
150
+ assert.equal(result.minted, true);
151
+ assert.deepEqual(result.rejected, { claimedNid: "claude-legacy", reason: "no_bind_token" });
152
+ assert.equal(sessionOf(db, "claude-legacy"), "sess-A", "claimed narrative untouched");
153
+ assert.equal(eventCount(db, "claude-legacy"), 0);
154
+ });
155
+ it("a matching secret string that differs from the token is still rejected", () => {
156
+ // Guards against any accidental loose/prefix comparison.
157
+ const db = createTestDb();
158
+ seedSpawned(db, "claude-alpha", "secret-alpha");
159
+ const result = resolveMcpBind(db, "brane-test", "claude-alpha", "secret-alph");
160
+ assert.equal(result.minted, true);
161
+ assert.deepEqual(result.rejected, { claimedNid: "claude-alpha", reason: "secret_mismatch" });
162
+ });
163
+ });
164
+ describe("mintExternalNarrative", () => {
165
+ it("creates an external-shaped row with a seeded started event", () => {
166
+ const db = createTestDb();
167
+ const nid = mintExternalNarrative(db, "brane-test");
168
+ assert.match(nid, /^external-[0-9a-z]{8}$/);
169
+ const row = getNarrative(db, nid);
170
+ assert.ok(row);
171
+ assert.equal(row.agent_type, "external");
172
+ assert.equal(row.role, "external");
173
+ assert.equal(row.status, "active");
174
+ const events = db
175
+ .prepare("SELECT type, content FROM events WHERE narrative_id = ?")
176
+ .all(nid);
177
+ assert.equal(events.length, 1);
178
+ assert.equal(events[0].type, "started");
179
+ });
180
+ it("gives the external a bind token, so its own reconnect authenticates", () => {
181
+ // Externals used to be tokenless, and their reconnects relied on the
182
+ // migration grace to get back to the same narrative. With the grace gone
183
+ // that would fragment one client into a narrative per reconnect, so the
184
+ // mint issues a real secret and `welcome` hands it to the bridge.
185
+ const db = createTestDb();
186
+ const { nid, secret } = mintExternalNarrativeWithSecret(db, "brane-test");
187
+ assert.ok(secret, "minted externals carry an agent token");
188
+ const { bind_token } = db.prepare("SELECT bind_token FROM narratives WHERE id = ?").get(nid);
189
+ assert.equal(bind_token, null, "and no plaintext of it is kept");
190
+ assert.equal(resolveMcpBind(db, "brane-test", nid, secret).nid, nid);
191
+ });
192
+ });
193
+ //# sourceMappingURL=mcp-narrative.test.js.map