@openparachute/vault 0.7.3-rc.13 → 0.7.3-rc.15
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/core/src/content-range-constants.ts +19 -0
- package/core/src/content-range.ts +5 -8
- package/core/src/mcp-manifest.test.ts +200 -0
- package/core/src/mcp-manifest.ts +736 -0
- package/core/src/mcp.ts +48 -628
- package/package.json +1 -1
- package/src/live-frame-parity.test.ts +21 -0
- package/src/subscriptions.ts +13 -1
- package/src/ws-server.ts +9 -1
- package/src/ws-subscribe.test.ts +87 -0
- package/src/ws-subscribe.ts +25 -6
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-range constants with ZERO runtime dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `content-range.ts` (which imports `QueryError` from
|
|
5
|
+
* `query-operators.ts` → `bun:sqlite`) so the pure-data MCP tool manifest
|
|
6
|
+
* (`mcp-manifest.ts`) can reference `MIN_CONTENT_LENGTH` in a tool
|
|
7
|
+
* `description` without dragging the sqlite driver into its import graph —
|
|
8
|
+
* the front-of-house Wave 0 workerd invariant. `content-range.ts` re-exports
|
|
9
|
+
* `MIN_CONTENT_LENGTH` from here, so every existing importer is unaffected.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Minimum accepted `content_length`. A UTF-8 codepoint is at most 4 bytes,
|
|
14
|
+
* so any budget >= 4 is guaranteed to make progress (the codepoint at the
|
|
15
|
+
* window start always fits). Budgets 1–3 could stall forever on a 4-byte
|
|
16
|
+
* emoji (empty slice, next_offset == offset); rejecting them up front is
|
|
17
|
+
* deterministic and simpler than a runtime "no progress" error.
|
|
18
|
+
*/
|
|
19
|
+
export const MIN_CONTENT_LENGTH = 4;
|
|
@@ -30,14 +30,11 @@
|
|
|
30
30
|
|
|
31
31
|
import { QueryError } from "./query-operators.js";
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* deterministic and simpler than a runtime "no progress" error.
|
|
39
|
-
*/
|
|
40
|
-
export const MIN_CONTENT_LENGTH = 4;
|
|
33
|
+
// `MIN_CONTENT_LENGTH` lives in the dependency-free `content-range-constants.ts`
|
|
34
|
+
// so the pure-data MCP tool manifest can import it without pulling `bun:sqlite`
|
|
35
|
+
// (front-of-house Wave 0). Re-exported here so existing importers are unchanged.
|
|
36
|
+
export { MIN_CONTENT_LENGTH } from "./content-range-constants.js";
|
|
37
|
+
import { MIN_CONTENT_LENGTH } from "./content-range-constants.js";
|
|
41
38
|
|
|
42
39
|
export interface ContentRange {
|
|
43
40
|
/** Byte offset (UTF-8) to start reading from. */
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pin test for the pure-data MCP tool manifest (front-of-house Wave 0).
|
|
3
|
+
*
|
|
4
|
+
* Two invariants, plus the workerd import-graph guard:
|
|
5
|
+
*
|
|
6
|
+
* 1. `generateMcpTools` emits EXACTLY the manifest — every emitted tool's
|
|
7
|
+
* {name, description, inputSchema, requiredVerb} deep-equals its
|
|
8
|
+
* `MCP_TOOL_MANIFEST` entry, in manifest order, gated by `condition`.
|
|
9
|
+
* This is the drift pin: the manifest is the single source of tool
|
|
10
|
+
* metadata, and the refactor that split behavior (execute) from data
|
|
11
|
+
* (this manifest) changed NOTHING observable. It fails the moment the
|
|
12
|
+
* built tools and the manifest disagree.
|
|
13
|
+
*
|
|
14
|
+
* 2. The observable contract shape — the ordered tool names, their scope
|
|
15
|
+
* verbs, and their inclusion conditions (no-seam → 13 core; +tickets → 2;
|
|
16
|
+
* +bytes → 1) — matches the table captured from `main` at extraction
|
|
17
|
+
* time. A verb or a conditional-inclusion change is a wire-contract event
|
|
18
|
+
* and must update this table deliberately.
|
|
19
|
+
*
|
|
20
|
+
* 3. IMPORT-GRAPH INVARIANT: `mcp-manifest.ts`'s transitive relative-import
|
|
21
|
+
* closure is free of `bun:sqlite` (and every other `bun:` / `node:`
|
|
22
|
+
* runtime builtin), so the identity worker can import it under Cloudflare
|
|
23
|
+
* workerd via the same `file:../../../parachute-vault/core` dep the vault
|
|
24
|
+
* worker uses — without dragging in the sqlite driver.
|
|
25
|
+
*/
|
|
26
|
+
import { describe, test, expect } from "bun:test";
|
|
27
|
+
import { readFileSync } from "node:fs";
|
|
28
|
+
import { dirname, resolve } from "node:path";
|
|
29
|
+
import { generateMcpTools, type McpToolDef } from "./mcp.js";
|
|
30
|
+
import { MCP_TOOL_MANIFEST, type McpToolCondition, type McpToolVerb } from "./mcp-manifest.js";
|
|
31
|
+
|
|
32
|
+
// A store stand-in: generateMcpTools only reads `store.db` to close over it in
|
|
33
|
+
// the (never-invoked here) execute closures. No tool is called in this file, so
|
|
34
|
+
// an empty object is enough to build the tool set.
|
|
35
|
+
const fakeStore = { db: {} } as any;
|
|
36
|
+
|
|
37
|
+
const ticketOpts = {
|
|
38
|
+
attachmentTickets: {
|
|
39
|
+
provider: {} as any,
|
|
40
|
+
vaultName: "pin",
|
|
41
|
+
urlBase: "https://host/vault/pin",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const bytesOpts = { attachmentBytes: { provider: {} as any } };
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The observable contract, as emitted by `main` the day this manifest was
|
|
48
|
+
* extracted (verified byte-identical against a snapshot of main's
|
|
49
|
+
* `generateMcpTools`). Order is emission order.
|
|
50
|
+
*/
|
|
51
|
+
const EXPECTED: ReadonlyArray<{
|
|
52
|
+
name: string;
|
|
53
|
+
requiredVerb: McpToolVerb;
|
|
54
|
+
condition: McpToolCondition;
|
|
55
|
+
hasResultContent: boolean;
|
|
56
|
+
}> = [
|
|
57
|
+
{ name: "query-notes", requiredVerb: "read", condition: "core", hasResultContent: false },
|
|
58
|
+
{ name: "create-note", requiredVerb: "write", condition: "core", hasResultContent: false },
|
|
59
|
+
{ name: "update-note", requiredVerb: "write", condition: "core", hasResultContent: false },
|
|
60
|
+
{ name: "delete-note", requiredVerb: "write", condition: "core", hasResultContent: false },
|
|
61
|
+
{ name: "list-tags", requiredVerb: "read", condition: "core", hasResultContent: false },
|
|
62
|
+
{ name: "update-tag", requiredVerb: "admin", condition: "core", hasResultContent: false },
|
|
63
|
+
{ name: "delete-tag", requiredVerb: "admin", condition: "core", hasResultContent: false },
|
|
64
|
+
{ name: "rename-tag", requiredVerb: "admin", condition: "core", hasResultContent: false },
|
|
65
|
+
{ name: "merge-tags", requiredVerb: "admin", condition: "core", hasResultContent: false },
|
|
66
|
+
{ name: "find-path", requiredVerb: "read", condition: "core", hasResultContent: false },
|
|
67
|
+
{ name: "vault-info", requiredVerb: "read", condition: "core", hasResultContent: false },
|
|
68
|
+
{ name: "prune-schema", requiredVerb: "admin", condition: "core", hasResultContent: false },
|
|
69
|
+
{ name: "doctor", requiredVerb: "read", condition: "core", hasResultContent: false },
|
|
70
|
+
{ name: "request-attachment-upload", requiredVerb: "write", condition: "attachment-tickets", hasResultContent: false },
|
|
71
|
+
{ name: "request-attachment-download", requiredVerb: "read", condition: "attachment-tickets", hasResultContent: false },
|
|
72
|
+
{ name: "read-attachment", requiredVerb: "read", condition: "attachment-bytes", hasResultContent: true },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
function names(tools: McpToolDef[]): string[] {
|
|
76
|
+
return tools.map((t) => t.name);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("MCP_TOOL_MANIFEST — shape", () => {
|
|
80
|
+
test("manifest order + verbs + conditions match the captured contract", () => {
|
|
81
|
+
expect(
|
|
82
|
+
MCP_TOOL_MANIFEST.map((e) => ({ name: e.name, requiredVerb: e.requiredVerb, condition: e.condition })),
|
|
83
|
+
).toEqual(EXPECTED.map((e) => ({ name: e.name, requiredVerb: e.requiredVerb, condition: e.condition })));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("every manifest entry carries a non-empty description + object inputSchema", () => {
|
|
87
|
+
for (const e of MCP_TOOL_MANIFEST) {
|
|
88
|
+
expect(typeof e.description).toBe("string");
|
|
89
|
+
expect(e.description.length).toBeGreaterThan(0);
|
|
90
|
+
expect(e.inputSchema).toBeInstanceOf(Object);
|
|
91
|
+
expect((e.inputSchema as any).type).toBe("object");
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("generateMcpTools — conditional inclusion (byte-for-byte with main)", () => {
|
|
97
|
+
test("no seams → exactly the 13 core-condition tools, in manifest order", () => {
|
|
98
|
+
const tools = generateMcpTools(fakeStore);
|
|
99
|
+
expect(names(tools)).toEqual(EXPECTED.filter((e) => e.condition === "core").map((e) => e.name));
|
|
100
|
+
expect(tools.length).toBe(13);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("attachmentTickets seam appends the 2 ticket tools (upload=write, download=read)", () => {
|
|
104
|
+
const tools = generateMcpTools(fakeStore, ticketOpts);
|
|
105
|
+
expect(names(tools)).toEqual(
|
|
106
|
+
EXPECTED.filter((e) => e.condition === "core" || e.condition === "attachment-tickets").map((e) => e.name),
|
|
107
|
+
);
|
|
108
|
+
expect(tools.length).toBe(15);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("attachmentBytes seam appends read-attachment only", () => {
|
|
112
|
+
const tools = generateMcpTools(fakeStore, bytesOpts);
|
|
113
|
+
expect(names(tools)).toEqual(
|
|
114
|
+
EXPECTED.filter((e) => e.condition === "core" || e.condition === "attachment-bytes").map((e) => e.name),
|
|
115
|
+
);
|
|
116
|
+
expect(tools.length).toBe(14);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("both seams → all 16 tools in emission order", () => {
|
|
120
|
+
const tools = generateMcpTools(fakeStore, { ...ticketOpts, ...bytesOpts });
|
|
121
|
+
expect(names(tools)).toEqual(EXPECTED.map((e) => e.name));
|
|
122
|
+
expect(tools.length).toBe(16);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("generateMcpTools — the emitted set IS the manifest (drift pin)", () => {
|
|
127
|
+
const tools = generateMcpTools(fakeStore, { ...ticketOpts, ...bytesOpts });
|
|
128
|
+
const includedEntries = MCP_TOOL_MANIFEST; // all conditions satisfied by both seams
|
|
129
|
+
|
|
130
|
+
test("emitted {name,description,inputSchema,requiredVerb} deep-equals the manifest, in order", () => {
|
|
131
|
+
expect(
|
|
132
|
+
tools.map((t) => ({
|
|
133
|
+
name: t.name,
|
|
134
|
+
description: t.description,
|
|
135
|
+
inputSchema: t.inputSchema,
|
|
136
|
+
requiredVerb: t.requiredVerb,
|
|
137
|
+
})),
|
|
138
|
+
).toEqual(
|
|
139
|
+
includedEntries.map((e) => ({
|
|
140
|
+
name: e.name,
|
|
141
|
+
description: e.description,
|
|
142
|
+
inputSchema: e.inputSchema,
|
|
143
|
+
requiredVerb: e.requiredVerb,
|
|
144
|
+
})),
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("only read-attachment carries a resultContent wrapper", () => {
|
|
149
|
+
for (let i = 0; i < tools.length; i++) {
|
|
150
|
+
expect(typeof tools[i]!.resultContent === "function").toBe(EXPECTED[i]!.hasResultContent);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("mcp-manifest.ts — workerd import-graph invariant (bun:sqlite-free)", () => {
|
|
156
|
+
// Walk the transitive RELATIVE-import closure of mcp-manifest.ts on disk and
|
|
157
|
+
// collect every bare (non-relative) specifier it reaches. A bare `bun:*` /
|
|
158
|
+
// `node:*` anywhere in that closure would break the identity worker's
|
|
159
|
+
// workerd build. Uses Bun's real import parser (not a regex) so prose in
|
|
160
|
+
// descriptions/comments can't produce phantom specifiers, and so type-only
|
|
161
|
+
// imports — erased at runtime, irrelevant to the workerd graph — are elided.
|
|
162
|
+
const transpiler = new Bun.Transpiler({ loader: "ts" });
|
|
163
|
+
function transitiveBareSpecifiers(entryTs: string): Set<string> {
|
|
164
|
+
const bare = new Set<string>();
|
|
165
|
+
const seen = new Set<string>();
|
|
166
|
+
const stack = [entryTs];
|
|
167
|
+
while (stack.length) {
|
|
168
|
+
const file = stack.pop()!;
|
|
169
|
+
if (seen.has(file)) continue;
|
|
170
|
+
seen.add(file);
|
|
171
|
+
const specs = transpiler.scanImports(readFileSync(file, "utf8")).map((i) => i.path);
|
|
172
|
+
for (const spec of specs) {
|
|
173
|
+
if (spec.startsWith(".")) {
|
|
174
|
+
// Resolve `./x.js` (or extensionless) to the on-disk `.ts` source.
|
|
175
|
+
const base = resolve(dirname(file), spec);
|
|
176
|
+
const candidate = base.endsWith(".js") ? base.slice(0, -3) + ".ts" : base + ".ts";
|
|
177
|
+
stack.push(candidate);
|
|
178
|
+
} else {
|
|
179
|
+
bare.add(spec);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return bare;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
test("transitive closure reaches no bun:/node: runtime builtin (esp. bun:sqlite)", () => {
|
|
187
|
+
const entry = resolve(import.meta.dir, "mcp-manifest.ts");
|
|
188
|
+
const bare = transitiveBareSpecifiers(entry);
|
|
189
|
+
expect([...bare]).not.toContain("bun:sqlite");
|
|
190
|
+
const runtimeBuiltins = [...bare].filter((s) => s.startsWith("bun:") || s.startsWith("node:"));
|
|
191
|
+
expect(runtimeBuiltins).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("as a positive control, the SAME scan over mcp.ts DOES reach bun:sqlite", () => {
|
|
195
|
+
// Proves the scanner actually follows imports (a vacuous scan would pass
|
|
196
|
+
// the invariant above for the wrong reason).
|
|
197
|
+
const bare = transitiveBareSpecifiers(resolve(import.meta.dir, "mcp.ts"));
|
|
198
|
+
expect([...bare]).toContain("bun:sqlite");
|
|
199
|
+
});
|
|
200
|
+
});
|