brane 0.0.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +362 -0
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +91 -0
- package/DESIGN.md +402 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +205 -0
- package/dist/absorb/brane-id.js +11 -0
- package/dist/absorb/chatgpt.js +161 -0
- package/dist/absorb/claude-code.js +504 -0
- package/dist/absorb/index.js +543 -0
- package/dist/absorb/organelle.js +345 -0
- package/dist/absorb/run.js +520 -0
- package/dist/absorb/sources.js +168 -0
- package/dist/absorb/types.js +3 -0
- package/dist/agent-env.js +45 -0
- package/dist/agents.js +1168 -0
- package/dist/claude-live-session.js +124 -0
- package/dist/cli.js +50 -0
- package/dist/codex-tool-log.js +144 -0
- package/dist/commands/absorb.js +291 -0
- package/dist/commands/attach.js +31 -0
- package/dist/commands/certify.js +62 -0
- package/dist/commands/config.js +116 -0
- package/dist/commands/cost.js +126 -0
- package/dist/commands/daemon.js +350 -0
- package/dist/commands/doctor.js +262 -0
- package/dist/commands/fold.js +95 -0
- package/dist/commands/list.js +89 -0
- package/dist/commands/mcp.js +84 -0
- package/dist/commands/meld.js +332 -0
- package/dist/commands/memory.js +215 -0
- package/dist/commands/revive.js +74 -0
- package/dist/commands/run.js +85 -0
- package/dist/commands/spawn.js +1094 -0
- package/dist/commands/token.js +153 -0
- package/dist/copilot-tool-log.js +145 -0
- package/dist/cost/pricing.js +116 -0
- package/dist/cost/reprice.js +136 -0
- package/dist/crush-tool-log.js +293 -0
- package/dist/daemon/agent-scope.js +116 -0
- package/dist/daemon/awaiting.js +54 -0
- package/dist/daemon/cert-manager.js +349 -0
- package/dist/daemon/client.js +262 -0
- package/dist/daemon/gated-routes.js +112 -0
- package/dist/daemon/keep-awake.js +103 -0
- package/dist/daemon/local-socket.js +145 -0
- package/dist/daemon/presence-gate.js +206 -0
- package/dist/daemon/server.js +6349 -0
- package/dist/daemon/signal-source.js +35 -0
- package/dist/daemon/tailscale-rebind.js +58 -0
- package/dist/daemon/token-ring.js +263 -0
- package/dist/daemon/token-routes.js +190 -0
- package/dist/db/events.js +152 -0
- package/dist/db/mentions.js +179 -0
- package/dist/db/schema.js +633 -0
- package/dist/db/tokens.js +583 -0
- package/dist/db/tool-uses.js +647 -0
- package/dist/diagram/index.js +85 -0
- package/dist/diagram/label.js +455 -0
- package/dist/diagram/layout.js +476 -0
- package/dist/diagram/links.js +50 -0
- package/dist/diagram/metrics.js +152 -0
- package/dist/diagram/parse.js +487 -0
- package/dist/diagram/svg.js +389 -0
- package/dist/diagram/types.js +9 -0
- package/dist/export.js +629 -0
- package/dist/files/archive-types.js +2 -0
- package/dist/files/archive.js +182 -0
- package/dist/files/browse.js +327 -0
- package/dist/files/bytes.js +162 -0
- package/dist/files/codecs.js +33 -0
- package/dist/files/find.js +212 -0
- package/dist/files/tar.js +223 -0
- package/dist/files/zip.js +209 -0
- package/dist/folds.js +72 -0
- package/dist/grammars/ebnf.js +92 -0
- package/dist/highlight.js +250 -0
- package/dist/ids.js +24 -0
- package/dist/markdown-diagnostics.js +233 -0
- package/dist/math.js +168 -0
- package/dist/mcp/server.js +886 -0
- package/dist/memory/embedder.js +145 -0
- package/dist/memory/embedding-worker.js +71 -0
- package/dist/memory/embeddings.js +91 -0
- package/dist/memory/index.js +232 -0
- package/dist/memory/storage.js +587 -0
- package/dist/memory/wiki.js +688 -0
- package/dist/memory/wordpiece.js +247 -0
- package/dist/mentions-scan.js +40 -0
- package/dist/presence.js +259 -0
- package/dist/session-affinity.js +185 -0
- package/dist/shared/api-request.js +124 -0
- package/dist/shared/block-glyphs.js +114 -0
- package/dist/shared/diagram-css.js +17 -0
- package/dist/shared/export-selection.js +101 -0
- package/dist/shared/find-in-text.js +110 -0
- package/dist/shared/glow-hues.js +225 -0
- package/dist/shared/hex-dump.js +62 -0
- package/dist/shared/hex-pages.js +52 -0
- package/dist/shared/hit-drum.js +79 -0
- package/dist/shared/known-nids.js +58 -0
- package/dist/shared/list-scroll-keys.js +56 -0
- package/dist/shared/markdown-lists.js +72 -0
- package/dist/shared/markdown-math.js +485 -0
- package/dist/shared/markdown-mentions.js +79 -0
- package/dist/shared/markdown-nids.js +41 -0
- package/dist/shared/markdown-protocols.js +53 -0
- package/dist/shared/markdown-refs.js +123 -0
- package/dist/shared/membrane-invasion.js +342 -0
- package/dist/shared/membrane-motion.js +83 -0
- package/dist/shared/membrane-palette.js +253 -0
- package/dist/shared/membrane-terrain.js +212 -0
- package/dist/shared/message-selection.js +123 -0
- package/dist/shared/mo-glyph.js +57 -0
- package/dist/shared/narrative-format.js +237 -0
- package/dist/shared/narrative-menu.js +82 -0
- package/dist/shared/narrative-sort.js +47 -0
- package/dist/shared/narrative-status.js +52 -0
- package/dist/shared/no-pan.js +85 -0
- package/dist/shared/pane-layout.js +50 -0
- package/dist/shared/payload-inline.js +83 -0
- package/dist/shared/speaker-color.js +61 -0
- package/dist/shared/token-rotation.js +14 -0
- package/dist/shared/view-url.js +197 -0
- package/dist/shared/wiki-links.js +70 -0
- package/dist/shared/wiki-window.js +4 -0
- package/dist/tailscale.js +55 -0
- package/dist/test/absorb-claude.test.js +892 -0
- package/dist/test/agent-scope.test.js +193 -0
- package/dist/test/agents.test.js +55 -0
- package/dist/test/api-request.test.js +122 -0
- package/dist/test/awaiting-glow.test.js +54 -0
- package/dist/test/awaiting.test.js +122 -0
- package/dist/test/bind-token.test.js +88 -0
- package/dist/test/block-glyphs.test.js +68 -0
- package/dist/test/cert-manager.test.js +262 -0
- package/dist/test/claude-live-session.test.js +110 -0
- package/dist/test/codex-tool-log.test.js +568 -0
- package/dist/test/config.test.js +71 -0
- package/dist/test/copilot-tool-log.test.js +93 -0
- package/dist/test/cors-preflight.test.js +49 -0
- package/dist/test/cost-linger.test.js +31 -0
- package/dist/test/cost-pricing.test.js +72 -0
- package/dist/test/cost-reprice.test.js +139 -0
- package/dist/test/cost-sessions.test.js +301 -0
- package/dist/test/crush-tool-log.test.js +289 -0
- package/dist/test/daemon-client.test.js +109 -0
- package/dist/test/daemon-socket.test.js +293 -0
- package/dist/test/diagram-arrows.test.js +205 -0
- package/dist/test/diagram-export.test.js +210 -0
- package/dist/test/diagram-labels.test.js +313 -0
- package/dist/test/diagram-parse.test.js +191 -0
- package/dist/test/diagram-render.test.js +175 -0
- package/dist/test/diagram-review.test.js +288 -0
- package/dist/test/diagram-self-loop-labels.test.js +303 -0
- package/dist/test/embedder.test.js +84 -0
- package/dist/test/events.test.js +123 -0
- package/dist/test/export-selection.test.js +102 -0
- package/dist/test/export.test.js +505 -0
- package/dist/test/files-browse.test.js +393 -0
- package/dist/test/files-find.test.js +272 -0
- package/dist/test/find-in-text.test.js +86 -0
- package/dist/test/first-paint-viewport.test.js +59 -0
- package/dist/test/fixtures/embedding-golden.js +23 -0
- package/dist/test/fixtures/wordpiece-golden.js +167 -0
- package/dist/test/glow-hues.test.js +202 -0
- package/dist/test/harness-cost.test.js +423 -0
- package/dist/test/helpers.js +21 -0
- package/dist/test/hex-pages.test.js +68 -0
- package/dist/test/highlight.test.js +69 -0
- package/dist/test/hit-drum.test.js +71 -0
- package/dist/test/hook-config.test.js +262 -0
- package/dist/test/json-gzip.test.js +70 -0
- package/dist/test/key-routing.test.js +58 -0
- package/dist/test/known-nids.test.js +106 -0
- package/dist/test/link-preview.test.js +67 -0
- package/dist/test/list-scroll-keys.test.js +66 -0
- package/dist/test/listen-own-words.test.js +147 -0
- package/dist/test/listen-timeout.test.js +33 -0
- package/dist/test/markdown-diagnostics.test.js +121 -0
- package/dist/test/markdown-lists.test.js +182 -0
- package/dist/test/markdown-math.test.js +256 -0
- package/dist/test/markdown-nids.test.js +146 -0
- package/dist/test/markdown-protocols.test.js +139 -0
- package/dist/test/mcp-bridge-cancel.test.js +98 -0
- package/dist/test/mcp-bridge-reconnect.test.js +121 -0
- package/dist/test/mcp-narrative.test.js +193 -0
- package/dist/test/mcp.test.js +548 -0
- package/dist/test/meld.test.js +85 -0
- package/dist/test/membrane-invasion.test.js +482 -0
- package/dist/test/membrane-motion.test.js +101 -0
- package/dist/test/membrane-palette.test.js +166 -0
- package/dist/test/membrane-terrain.test.js +170 -0
- package/dist/test/memory-log.test.js +202 -0
- package/dist/test/mentions-e2e.test.js +261 -0
- package/dist/test/mentions-ledger.test.js +265 -0
- package/dist/test/mentions-listen.test.js +265 -0
- package/dist/test/mentions.test.js +238 -0
- package/dist/test/message-selection.test.js +114 -0
- package/dist/test/mo-glyph.test.js +22 -0
- package/dist/test/model-resolution.test.js +166 -0
- package/dist/test/narrate-validate.test.js +77 -0
- package/dist/test/narrate-wait.test.js +160 -0
- package/dist/test/narrative-format.test.js +219 -0
- package/dist/test/narrative-menu.test.js +137 -0
- package/dist/test/narrative-sort.test.js +75 -0
- package/dist/test/no-pan.test.js +60 -0
- package/dist/test/pane-layout.test.js +153 -0
- package/dist/test/paths.test.js +37 -0
- package/dist/test/payload-inline.test.js +91 -0
- package/dist/test/precache-fonts.test.js +72 -0
- package/dist/test/presence-gate.test.js +440 -0
- package/dist/test/presence.test.js +160 -0
- package/dist/test/read-events.test.js +220 -0
- package/dist/test/remote-meld.test.js +115 -0
- package/dist/test/revive-cli.test.js +93 -0
- package/dist/test/search-floor.test.js +206 -0
- package/dist/test/search-fusion.test.js +168 -0
- package/dist/test/search.test.js +205 -0
- package/dist/test/security.test.js +48 -0
- package/dist/test/selection-wiring.test.js +61 -0
- package/dist/test/session-affinity.test.js +295 -0
- package/dist/test/signal-source.test.js +32 -0
- package/dist/test/socket-account.test.js +268 -0
- package/dist/test/spawn-toml.test.js +72 -0
- package/dist/test/speaker-color.test.js +90 -0
- package/dist/test/tailscale-rebind.test.js +79 -0
- package/dist/test/terminal-cursor.test.js +61 -0
- package/dist/test/terminal-resync.test.js +23 -0
- package/dist/test/thin-cli.test.js +382 -0
- package/dist/test/token-bootstrap.test.js +272 -0
- package/dist/test/token-devices.test.js +587 -0
- package/dist/test/token-lapse.test.js +94 -0
- package/dist/test/token-ring.test.js +188 -0
- package/dist/test/token-rotate-cli.test.js +58 -0
- package/dist/test/token-routes.test.js +95 -0
- package/dist/test/tool-uses.test.js +1006 -0
- package/dist/test/usage-ledger.test.js +126 -0
- package/dist/test/view-url.test.js +581 -0
- package/dist/test/wiki-links.test.js +57 -0
- package/dist/test/wiki-sources.test.js +85 -0
- package/dist/test/wiki-window.test.js +31 -0
- package/dist/test/wiki.test.js +399 -0
- package/dist/test/wordpiece.test.js +68 -0
- package/dist/tls.js +148 -0
- package/dist/tmux.js +202 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +141 -0
- package/docs/README.md +3 -0
- package/docs/absorb.md +231 -0
- package/docs/cost-ledger.md +125 -0
- package/docs/daemon-api.md +189 -0
- package/docs/diagrams.md +163 -0
- package/docs/doctor.md +36 -0
- package/docs/export-scope.md +69 -0
- package/docs/files-view.md +133 -0
- package/docs/link-previews.md +38 -0
- package/docs/math.md +32 -0
- package/docs/mcp.md +69 -0
- package/docs/memory-search.md +75 -0
- package/docs/mentions.md +101 -0
- package/docs/multi-brane.md +14 -0
- package/docs/passkeys.md +165 -0
- package/docs/presence.md +202 -0
- package/docs/releasing.md +53 -0
- package/docs/rendering-warnings.md +22 -0
- package/docs/request-layer.md +72 -0
- package/docs/selection-store.md +50 -0
- package/docs/session-affinity.md +61 -0
- package/docs/terminal-mirroring.md +211 -0
- package/docs/token-rotation.md +413 -0
- package/docs/view-urls.md +114 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e56efc1291dca7258ea00b8108b033a4.js +1097 -0
- package/expo/dist/apple-touch-icon.png +0 -0
- package/expo/dist/favicon.svg +3 -0
- package/expo/dist/index.html +177 -0
- package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/expo/dist/katex/katex.min.css +1 -0
- package/expo/dist/manifest.json +16 -0
- package/expo/dist/metadata.json +1 -0
- package/expo/dist/og/membrane.v1.png +0 -0
- package/expo/dist/pdf.min.mjs +28 -0
- package/expo/dist/pdf.worker.min.mjs +28 -0
- package/expo/dist/sw.js +154 -0
- package/package.json +81 -23
- package/protocols/LIVE_loop.md +407 -0
- package/protocols/bebrief.md +41 -0
- package/protocols/digest.md +231 -0
- package/protocols/fly.md +266 -0
- package/protocols/idle.md +81 -0
- package/protocols/keeper.md +829 -0
- package/protocols/markdown.md +80 -0
- package/protocols/memory.md +419 -0
- package/protocols/mentions.md +93 -0
- package/protocols/payloads.md +53 -0
- package/protocols/prelude.md +91 -0
- package/protocols/ramify.md +105 -0
- package/protocols/review.md +356 -0
- package/protocols/rrr.md +232 -0
- package/protocols/scribe.md +429 -0
- package/protocols/summarize.md +203 -0
- package/protocols/twin.md +174 -0
- package/protocols/wiki.md +438 -0
- package/protocols/worker.md +38 -0
- package/wiki/shared/append-only-event-log.md +148 -0
- package/wiki/shared/index.md +48 -0
- package/wiki/shared/owning-your-narratives.md +161 -0
- package/wiki/shared/premature-ontologies.md +435 -0
- package/wiki/shared/sedimentary-superintelligence.md +128 -0
- package/wiki/shared/sequestering-the-narrative.md +143 -0
- package/wiki/shared/why-wikis-work.md +216 -0
- package/.npmignore +0 -2
- package/.travis.yml +0 -5
- package/index.js +0 -8
- package/index.js~ +0 -12
|
@@ -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
|