@ambitresearch/genie 1.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.
- package/README.md +15 -0
- package/dist/auth/bearer.d.ts +49 -0
- package/dist/auth/bearer.d.ts.map +1 -0
- package/dist/auth/bearer.js +159 -0
- package/dist/auth/bearer.js.map +1 -0
- package/dist/auth/oauth/authorize.d.ts +19 -0
- package/dist/auth/oauth/authorize.d.ts.map +1 -0
- package/dist/auth/oauth/authorize.js +111 -0
- package/dist/auth/oauth/authorize.js.map +1 -0
- package/dist/auth/oauth/config.d.ts +10 -0
- package/dist/auth/oauth/config.d.ts.map +1 -0
- package/dist/auth/oauth/config.js +16 -0
- package/dist/auth/oauth/config.js.map +1 -0
- package/dist/auth/oauth/dcr.d.ts +15 -0
- package/dist/auth/oauth/dcr.d.ts.map +1 -0
- package/dist/auth/oauth/dcr.js +38 -0
- package/dist/auth/oauth/dcr.js.map +1 -0
- package/dist/auth/oauth/jwt.d.ts +13 -0
- package/dist/auth/oauth/jwt.d.ts.map +1 -0
- package/dist/auth/oauth/jwt.js +46 -0
- package/dist/auth/oauth/jwt.js.map +1 -0
- package/dist/auth/oauth/metadata.d.ts +9 -0
- package/dist/auth/oauth/metadata.d.ts.map +1 -0
- package/dist/auth/oauth/metadata.js +19 -0
- package/dist/auth/oauth/metadata.js.map +1 -0
- package/dist/auth/oauth/router.d.ts +32 -0
- package/dist/auth/oauth/router.d.ts.map +1 -0
- package/dist/auth/oauth/router.js +159 -0
- package/dist/auth/oauth/router.js.map +1 -0
- package/dist/auth/oauth/store.d.ts +55 -0
- package/dist/auth/oauth/store.d.ts.map +1 -0
- package/dist/auth/oauth/store.js +63 -0
- package/dist/auth/oauth/store.js.map +1 -0
- package/dist/auth/oauth/token.d.ts +21 -0
- package/dist/auth/oauth/token.d.ts.map +1 -0
- package/dist/auth/oauth/token.js +77 -0
- package/dist/auth/oauth/token.js.map +1 -0
- package/dist/auth/oidc/group-policy.d.ts +43 -0
- package/dist/auth/oidc/group-policy.d.ts.map +1 -0
- package/dist/auth/oidc/group-policy.js +54 -0
- package/dist/auth/oidc/group-policy.js.map +1 -0
- package/dist/auth/oidc/verifier.d.ts +42 -0
- package/dist/auth/oidc/verifier.d.ts.map +1 -0
- package/dist/auth/oidc/verifier.js +145 -0
- package/dist/auth/oidc/verifier.js.map +1 -0
- package/dist/auth/token-cli.d.ts +9 -0
- package/dist/auth/token-cli.d.ts.map +1 -0
- package/dist/auth/token-cli.js +98 -0
- package/dist/auth/token-cli.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +149 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/redact.d.ts +20 -0
- package/dist/config/redact.d.ts.map +1 -0
- package/dist/config/redact.js +77 -0
- package/dist/config/redact.js.map +1 -0
- package/dist/config/secrets.d.ts +100 -0
- package/dist/config/secrets.d.ts.map +1 -0
- package/dist/config/secrets.js +130 -0
- package/dist/config/secrets.js.map +1 -0
- package/dist/framework/html.d.ts +103 -0
- package/dist/framework/html.d.ts.map +1 -0
- package/dist/framework/html.js +271 -0
- package/dist/framework/html.js.map +1 -0
- package/dist/framework/interface.d.ts +130 -0
- package/dist/framework/interface.d.ts.map +1 -0
- package/dist/framework/interface.js +112 -0
- package/dist/framework/interface.js.map +1 -0
- package/dist/framework/react.d.ts +133 -0
- package/dist/framework/react.d.ts.map +1 -0
- package/dist/framework/react.js +243 -0
- package/dist/framework/react.js.map +1 -0
- package/dist/framework/vue.d.ts +99 -0
- package/dist/framework/vue.d.ts.map +1 -0
- package/dist/framework/vue.js +375 -0
- package/dist/framework/vue.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/client.d.ts +81 -0
- package/dist/llm/client.d.ts.map +1 -0
- package/dist/llm/client.js +141 -0
- package/dist/llm/client.js.map +1 -0
- package/dist/llm/component-response.d.ts +111 -0
- package/dist/llm/component-response.d.ts.map +1 -0
- package/dist/llm/component-response.js +167 -0
- package/dist/llm/component-response.js.map +1 -0
- package/dist/llm/generated-files.d.ts +10 -0
- package/dist/llm/generated-files.d.ts.map +1 -0
- package/dist/llm/generated-files.js +26 -0
- package/dist/llm/generated-files.js.map +1 -0
- package/dist/llm/prompts/CHANGELOG.md +70 -0
- package/dist/llm/prompts/generate-component.system.md +99 -0
- package/dist/llm/prompts/refine-component.system.md +112 -0
- package/dist/llm/prompts.d.ts +66 -0
- package/dist/llm/prompts.d.ts.map +1 -0
- package/dist/llm/prompts.js +78 -0
- package/dist/llm/prompts.js.map +1 -0
- package/dist/llm/retry.d.ts +183 -0
- package/dist/llm/retry.d.ts.map +1 -0
- package/dist/llm/retry.js +475 -0
- package/dist/llm/retry.js.map +1 -0
- package/dist/llm/schema.d.ts +129 -0
- package/dist/llm/schema.d.ts.map +1 -0
- package/dist/llm/schema.js +147 -0
- package/dist/llm/schema.js.map +1 -0
- package/dist/llm/validate.d.ts +107 -0
- package/dist/llm/validate.d.ts.map +1 -0
- package/dist/llm/validate.js +174 -0
- package/dist/llm/validate.js.map +1 -0
- package/dist/manifest/compiler.d.ts +112 -0
- package/dist/manifest/compiler.d.ts.map +1 -0
- package/dist/manifest/compiler.js +306 -0
- package/dist/manifest/compiler.js.map +1 -0
- package/dist/manifest/ensure.d.ts +46 -0
- package/dist/manifest/ensure.d.ts.map +1 -0
- package/dist/manifest/ensure.js +49 -0
- package/dist/manifest/ensure.js.map +1 -0
- package/dist/manifest/index.d.ts +13 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +13 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/middleware/plan-guard.d.ts +167 -0
- package/dist/middleware/plan-guard.d.ts.map +1 -0
- package/dist/middleware/plan-guard.js +190 -0
- package/dist/middleware/plan-guard.js.map +1 -0
- package/dist/plans/index.d.ts +71 -0
- package/dist/plans/index.d.ts.map +1 -0
- package/dist/plans/index.js +252 -0
- package/dist/plans/index.js.map +1 -0
- package/dist/schemas/component.schema.json +114 -0
- package/dist/server.d.ts +96 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +258 -0
- package/dist/server.js.map +1 -0
- package/dist/store/empty-manifest.d.ts +58 -0
- package/dist/store/empty-manifest.d.ts.map +1 -0
- package/dist/store/empty-manifest.js +59 -0
- package/dist/store/empty-manifest.js.map +1 -0
- package/dist/store/git-host.d.ts +120 -0
- package/dist/store/git-host.d.ts.map +1 -0
- package/dist/store/git-host.js +796 -0
- package/dist/store/git-host.js.map +1 -0
- package/dist/store/index.d.ts +5 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +4 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/interface.d.ts +322 -0
- package/dist/store/interface.d.ts.map +1 -0
- package/dist/store/interface.js +100 -0
- package/dist/store/interface.js.map +1 -0
- package/dist/store/kit-files.d.ts +88 -0
- package/dist/store/kit-files.d.ts.map +1 -0
- package/dist/store/kit-files.js +199 -0
- package/dist/store/kit-files.js.map +1 -0
- package/dist/store/local.d.ts +70 -0
- package/dist/store/local.d.ts.map +1 -0
- package/dist/store/local.js +651 -0
- package/dist/store/local.js.map +1 -0
- package/dist/store/manifest.d.ts +44 -0
- package/dist/store/manifest.d.ts.map +1 -0
- package/dist/store/manifest.js +116 -0
- package/dist/store/manifest.js.map +1 -0
- package/dist/store/viewer-assets.d.ts +20 -0
- package/dist/store/viewer-assets.d.ts.map +1 -0
- package/dist/store/viewer-assets.js +118 -0
- package/dist/store/viewer-assets.js.map +1 -0
- package/dist/sync/anchor.d.ts +125 -0
- package/dist/sync/anchor.d.ts.map +1 -0
- package/dist/sync/anchor.js +210 -0
- package/dist/sync/anchor.js.map +1 -0
- package/dist/sync/index.d.ts +15 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +15 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/orchestrator.d.ts +160 -0
- package/dist/sync/orchestrator.d.ts.map +1 -0
- package/dist/sync/orchestrator.js +288 -0
- package/dist/sync/orchestrator.js.map +1 -0
- package/dist/tools/bind_kit.d.ts +29 -0
- package/dist/tools/bind_kit.d.ts.map +1 -0
- package/dist/tools/bind_kit.js +70 -0
- package/dist/tools/bind_kit.js.map +1 -0
- package/dist/tools/conjure.d.ts +141 -0
- package/dist/tools/conjure.d.ts.map +1 -0
- package/dist/tools/conjure.js +655 -0
- package/dist/tools/conjure.js.map +1 -0
- package/dist/tools/conjure_screen.d.ts +174 -0
- package/dist/tools/conjure_screen.d.ts.map +1 -0
- package/dist/tools/conjure_screen.js +458 -0
- package/dist/tools/conjure_screen.js.map +1 -0
- package/dist/tools/create_kit.d.ts +32 -0
- package/dist/tools/create_kit.d.ts.map +1 -0
- package/dist/tools/create_kit.js +131 -0
- package/dist/tools/create_kit.js.map +1 -0
- package/dist/tools/create_project.d.ts +267 -0
- package/dist/tools/create_project.d.ts.map +1 -0
- package/dist/tools/create_project.js +546 -0
- package/dist/tools/create_project.js.map +1 -0
- package/dist/tools/delete_files.d.ts +93 -0
- package/dist/tools/delete_files.d.ts.map +1 -0
- package/dist/tools/delete_files.js +276 -0
- package/dist/tools/delete_files.js.map +1 -0
- package/dist/tools/delete_project.d.ts +43 -0
- package/dist/tools/delete_project.d.ts.map +1 -0
- package/dist/tools/delete_project.js +118 -0
- package/dist/tools/delete_project.js.map +1 -0
- package/dist/tools/get_kit.d.ts +28 -0
- package/dist/tools/get_kit.d.ts.map +1 -0
- package/dist/tools/get_kit.js +95 -0
- package/dist/tools/get_kit.js.map +1 -0
- package/dist/tools/get_project.d.ts +37 -0
- package/dist/tools/get_project.d.ts.map +1 -0
- package/dist/tools/get_project.js +71 -0
- package/dist/tools/get_project.js.map +1 -0
- package/dist/tools/list_components.d.ts +42 -0
- package/dist/tools/list_components.d.ts.map +1 -0
- package/dist/tools/list_components.js +125 -0
- package/dist/tools/list_components.js.map +1 -0
- package/dist/tools/list_files.d.ts +32 -0
- package/dist/tools/list_files.d.ts.map +1 -0
- package/dist/tools/list_files.js +109 -0
- package/dist/tools/list_files.js.map +1 -0
- package/dist/tools/list_kits.d.ts +14 -0
- package/dist/tools/list_kits.d.ts.map +1 -0
- package/dist/tools/list_kits.js +41 -0
- package/dist/tools/list_kits.js.map +1 -0
- package/dist/tools/list_projects.d.ts +23 -0
- package/dist/tools/list_projects.d.ts.map +1 -0
- package/dist/tools/list_projects.js +66 -0
- package/dist/tools/list_projects.js.map +1 -0
- package/dist/tools/plan.d.ts +16 -0
- package/dist/tools/plan.d.ts.map +1 -0
- package/dist/tools/plan.js +146 -0
- package/dist/tools/plan.js.map +1 -0
- package/dist/tools/preview.d.ts +289 -0
- package/dist/tools/preview.d.ts.map +1 -0
- package/dist/tools/preview.js +672 -0
- package/dist/tools/preview.js.map +1 -0
- package/dist/tools/read_file.d.ts +40 -0
- package/dist/tools/read_file.d.ts.map +1 -0
- package/dist/tools/read_file.js +114 -0
- package/dist/tools/read_file.js.map +1 -0
- package/dist/tools/refine.d.ts +220 -0
- package/dist/tools/refine.d.ts.map +1 -0
- package/dist/tools/refine.js +501 -0
- package/dist/tools/refine.js.map +1 -0
- package/dist/tools/validate.d.ts +74 -0
- package/dist/tools/validate.d.ts.map +1 -0
- package/dist/tools/validate.js +224 -0
- package/dist/tools/validate.js.map +1 -0
- package/dist/tools/write_files.d.ts +158 -0
- package/dist/tools/write_files.d.ts.map +1 -0
- package/dist/tools/write_files.js +541 -0
- package/dist/tools/write_files.js.map +1 -0
- package/dist/transport.d.ts +77 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +448 -0
- package/dist/transport.js.map +1 -0
- package/dist/ui/card-asset-broker.d.ts +42 -0
- package/dist/ui/card-asset-broker.d.ts.map +1 -0
- package/dist/ui/card-asset-broker.js +479 -0
- package/dist/ui/card-asset-broker.js.map +1 -0
- package/dist/ui/grid-resource.d.ts +252 -0
- package/dist/ui/grid-resource.d.ts.map +1 -0
- package/dist/ui/grid-resource.js +680 -0
- package/dist/ui/grid-resource.js.map +1 -0
- package/dist/ui/viewer-static/index.html +95 -0
- package/dist/ui/viewer-static/viewer.css +451 -0
- package/dist/ui/viewer-static/viewer.js +1405 -0
- package/dist/validate/full-scan.d.ts +87 -0
- package/dist/validate/full-scan.d.ts.map +1 -0
- package/dist/validate/full-scan.js +299 -0
- package/dist/validate/full-scan.js.map +1 -0
- package/dist/validate/index.d.ts +15 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +18 -0
- package/dist/validate/index.js.map +1 -0
- package/dist/validate/marker.d.ts +89 -0
- package/dist/validate/marker.d.ts.map +1 -0
- package/dist/validate/marker.js +87 -0
- package/dist/validate/marker.js.map +1 -0
- package/dist/validate/phash.d.ts +135 -0
- package/dist/validate/phash.d.ts.map +1 -0
- package/dist/validate/phash.js +307 -0
- package/dist/validate/phash.js.map +1 -0
- package/dist/validate/render.d.ts +38 -0
- package/dist/validate/render.d.ts.map +1 -0
- package/dist/validate/render.js +155 -0
- package/dist/validate/render.js.map +1 -0
- package/dist/watch/index.d.ts +12 -0
- package/dist/watch/index.d.ts.map +1 -0
- package/dist/watch/index.js +12 -0
- package/dist/watch/index.js.map +1 -0
- package/dist/watch/watcher.d.ts +65 -0
- package/dist/watch/watcher.d.ts.map +1 -0
- package/dist/watch/watcher.js +238 -0
- package/dist/watch/watcher.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `withPlanGuard` — centralised plan-vs-write guard middleware (M1-13,
|
|
3
|
+
* DRO-239).
|
|
4
|
+
*
|
|
5
|
+
* BEFORE this file, every plan-gated verb (`write_files`, `delete_files`) had
|
|
6
|
+
* to reimplement the same four checks — planId presence, planId existence,
|
|
7
|
+
* planId expiry, and per-path glob membership. When they drifted (as they
|
|
8
|
+
* already had: `write_files` split path-outside-plan into `"glob"` vs
|
|
9
|
+
* `"escapesLocalDir"` sub-reasons while `delete_files` used a single message,
|
|
10
|
+
* and only `plan.ts` emitted a structured audit log), a request that would be
|
|
11
|
+
* rejected by one verb could slip through another with the same shape.
|
|
12
|
+
*
|
|
13
|
+
* This middleware makes the guard one seam: every plan-gated tool wraps its
|
|
14
|
+
* handler in `withPlanGuard(...)`, and the four checks (AC2 (a)–(d)) live
|
|
15
|
+
* here, produce one canonical JSON-RPC `-32602 InvalidParams`-shaped payload
|
|
16
|
+
* (AC3), and emit one canonical `plan.guard.reject` audit line (AC6). The
|
|
17
|
+
* refactored write/delete tools stop re-implementing these checks entirely.
|
|
18
|
+
*
|
|
19
|
+
* Tool-side error taxonomies (e.g. `write_files`' `PayloadTooLargeError`,
|
|
20
|
+
* `delete_files`' `DeleteFailed`) stay in the tools — this middleware only
|
|
21
|
+
* owns the plan-boundary check, matching the issue's scope ("one middleware
|
|
22
|
+
* so every write/delete/register/unregister call funnels through identical
|
|
23
|
+
* validation"), and stops at that boundary so tools remain responsible for
|
|
24
|
+
* their own downstream error shapes.
|
|
25
|
+
*/
|
|
26
|
+
import { getPlan, pathMatchesGlobs, PlanNotFoundError } from "../plans/index.js";
|
|
27
|
+
// ─── AC3: canonical JSON-RPC-shaped rejection payload ────────────────────
|
|
28
|
+
/**
|
|
29
|
+
* Build the structured tool-result for a rejection. Mirrors the JSON-RPC
|
|
30
|
+
* `-32602 InvalidParams` error object (`{ code, message, data }`) exactly, so
|
|
31
|
+
* an MCP client can parse the tool response as JSON and branch on
|
|
32
|
+
* `code`/`data.reason` the same way it would branch on a wire-level error —
|
|
33
|
+
* without needing to know which specific tool it called.
|
|
34
|
+
*
|
|
35
|
+
* `isError: true` makes the SDK surface this as a tool failure (not a
|
|
36
|
+
* regular result) so the client's `isError` check trips.
|
|
37
|
+
*/
|
|
38
|
+
function guardErrorResult(message, reason, extra = {}) {
|
|
39
|
+
const data = { reason };
|
|
40
|
+
if (extra.planId !== undefined)
|
|
41
|
+
data.planId = extra.planId;
|
|
42
|
+
if (extra.path !== undefined)
|
|
43
|
+
data.path = extra.path;
|
|
44
|
+
const payload = { code: -32602, message, data };
|
|
45
|
+
return {
|
|
46
|
+
isError: true,
|
|
47
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// ─── AC6: structured plan.guard.reject audit log ─────────────────────────
|
|
51
|
+
/**
|
|
52
|
+
* Emit an audit-log line to stderr (never stdout — on the stdio MCP
|
|
53
|
+
* transport, stdout IS the protocol stream and a stray line there corrupts
|
|
54
|
+
* every client's message framing; `plan.ts` follows the same convention for
|
|
55
|
+
* its `plan.created` event, see the comment there).
|
|
56
|
+
*
|
|
57
|
+
* Only the guard's own metadata is logged: `event`, `planId` (when known),
|
|
58
|
+
* `reason`, `path` (when path-specific). Never the file contents, `data`
|
|
59
|
+
* payloads, `localPath` bytes, or anything else that came off the wire — a
|
|
60
|
+
* plan-boundary rejection is a security-relevant event, and the log is what
|
|
61
|
+
* an operator inspects afterwards; leaking the rejected payload defeats the
|
|
62
|
+
* point.
|
|
63
|
+
*/
|
|
64
|
+
function logGuardReject(planId, reason, path) {
|
|
65
|
+
const line = { event: "plan.guard.reject", reason };
|
|
66
|
+
if (planId !== undefined)
|
|
67
|
+
line.planId = planId;
|
|
68
|
+
if (path !== undefined)
|
|
69
|
+
line.path = path;
|
|
70
|
+
process.stderr.write(JSON.stringify(line) + "\n");
|
|
71
|
+
}
|
|
72
|
+
// ─── Path extraction ─────────────────────────────────────────────────────
|
|
73
|
+
/**
|
|
74
|
+
* Default path extractor for the two shipping tool shapes:
|
|
75
|
+
* - `mode: "writes"` (write_files) → `args.files[].path`
|
|
76
|
+
* - `mode: "deletes"` (delete_files) → `args[pathsKey ?? "paths"]` as a `string[]`
|
|
77
|
+
*
|
|
78
|
+
* A malformed shape (e.g. `files` isn't an array) is treated as "no paths to
|
|
79
|
+
* check": the guard will then delegate to the wrapped handler, which owns
|
|
80
|
+
* shape validation via its Zod input schema (see `deleteFilesArgsSchema` in
|
|
81
|
+
* delete_files.ts, `fileInputSchema` in write_files.ts). This split matches
|
|
82
|
+
* the wrapped tools' own precedent — plan checks answer "is this authorised?",
|
|
83
|
+
* shape checks answer "is this well-formed?" — and keeps the guard from
|
|
84
|
+
* needing to know each tool's input schema.
|
|
85
|
+
*/
|
|
86
|
+
function defaultExtractPaths(args, mode, pathsKey) {
|
|
87
|
+
if (typeof args !== "object" || args === null)
|
|
88
|
+
return [];
|
|
89
|
+
if (mode === "writes") {
|
|
90
|
+
const files = args.files;
|
|
91
|
+
if (!Array.isArray(files))
|
|
92
|
+
return [];
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
if (typeof file === "object" &&
|
|
96
|
+
file !== null &&
|
|
97
|
+
typeof file.path === "string") {
|
|
98
|
+
out.push(file.path);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
// mode === "deletes"
|
|
104
|
+
const key = pathsKey ?? "paths";
|
|
105
|
+
const paths = args[key];
|
|
106
|
+
if (!Array.isArray(paths))
|
|
107
|
+
return [];
|
|
108
|
+
return paths.filter((p) => typeof p === "string");
|
|
109
|
+
}
|
|
110
|
+
// ─── The guard itself ────────────────────────────────────────────────────
|
|
111
|
+
/**
|
|
112
|
+
* Wrap a plan-gated tool handler so every call funnels through one plan-vs-
|
|
113
|
+
* write validation seam.
|
|
114
|
+
*
|
|
115
|
+
* Order of checks — matches AC2 (a)→(d), and is intentionally the same order
|
|
116
|
+
* the tool would fail-fast on today:
|
|
117
|
+
*
|
|
118
|
+
* 1. **planId present** (AC2(a)) — string, non-empty. Missing values are
|
|
119
|
+
* rejected before touching the plan registry: a `getPlan("")` would
|
|
120
|
+
* throw as "not found", which would lose the distinction the reason
|
|
121
|
+
* enum draws between "you forgot to send a planId" and "the planId you
|
|
122
|
+
* sent doesn't match any plan we have." Clients act on those
|
|
123
|
+
* differently (the first is a client bug; the second may be an expired
|
|
124
|
+
* capability grant that needs re-issuing).
|
|
125
|
+
*
|
|
126
|
+
* 2. **planId exists + not expired** (AC2(b)+(c)) — `getPlan` throws
|
|
127
|
+
* `PlanNotFoundError` for both, so they surface as one `planNotFound`
|
|
128
|
+
* reason (see the enum comment above for why).
|
|
129
|
+
*
|
|
130
|
+
* 3. **path matches plan globs** (AC2(d)) — every extracted path is
|
|
131
|
+
* checked against `plan.writes` or `plan.deletes` per `options.mode`.
|
|
132
|
+
* Fails on the FIRST offending path: the exact offending path is
|
|
133
|
+
* surfaced (both in the returned payload and the audit log), so an
|
|
134
|
+
* operator investigating a rejection knows which specific path
|
|
135
|
+
* tripped the guard without having to diff the whole request against
|
|
136
|
+
* the plan's glob list.
|
|
137
|
+
*
|
|
138
|
+
* Only if all three pass does the wrapped handler run — with the resolved
|
|
139
|
+
* plan handed through in `ctx.plan`, so it doesn't re-fetch (see
|
|
140
|
+
* `PlanGuardContext`).
|
|
141
|
+
*
|
|
142
|
+
* Non-guard errors (a `getPlan` throw that isn't `PlanNotFoundError`, or
|
|
143
|
+
* a handler throw) are NOT swallowed — they propagate as-is, so a
|
|
144
|
+
* disk-corrupt plan snapshot or an internal handler bug is still visible to
|
|
145
|
+
* the transport layer (as an MCP internal error), rather than being masked
|
|
146
|
+
* as a plan-boundary rejection.
|
|
147
|
+
*/
|
|
148
|
+
export function withPlanGuard(options, handler) {
|
|
149
|
+
return async (args) => {
|
|
150
|
+
// AC2(a) — planId presence.
|
|
151
|
+
const planIdRaw = args?.planId;
|
|
152
|
+
if (typeof planIdRaw !== "string" || planIdRaw.length === 0) {
|
|
153
|
+
logGuardReject(undefined, "planIdMissing");
|
|
154
|
+
return guardErrorResult("planId is required: every plan-gated call must present a planId returned by mcp__genie__plan.", "planIdMissing");
|
|
155
|
+
}
|
|
156
|
+
const planId = planIdRaw;
|
|
157
|
+
// AC2(b)+(c) — planId exists + not expired. `getPlan` collapses both into
|
|
158
|
+
// one PlanNotFoundError; anything else (disk read failure, corrupt JSON,
|
|
159
|
+
// …) is genuinely internal and should propagate rather than masquerade
|
|
160
|
+
// as a plan-boundary rejection.
|
|
161
|
+
let plan;
|
|
162
|
+
try {
|
|
163
|
+
plan = await getPlan(planId);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (error instanceof PlanNotFoundError) {
|
|
167
|
+
logGuardReject(planId, "planNotFound");
|
|
168
|
+
return guardErrorResult(`Plan "${planId}" not found or expired. Plans expire after 1h of inactivity — request a new one via mcp__genie__plan.`, "planNotFound", { planId });
|
|
169
|
+
}
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
// AC2(d) — path membership.
|
|
173
|
+
//
|
|
174
|
+
// `extractPaths` is opt-in (for non-standard input shapes); otherwise
|
|
175
|
+
// fall back to the default extractor per mode.
|
|
176
|
+
const paths = options.extractPaths
|
|
177
|
+
? options.extractPaths(args)
|
|
178
|
+
: defaultExtractPaths(args, options.mode, options.pathsKey);
|
|
179
|
+
const globs = options.mode === "writes" ? plan.writes : plan.deletes;
|
|
180
|
+
for (const path of paths) {
|
|
181
|
+
if (!pathMatchesGlobs(path, globs)) {
|
|
182
|
+
logGuardReject(planId, "pathOutsidePlan", path);
|
|
183
|
+
return guardErrorResult(`Path "${path}" is not covered by the plan's ${options.mode}.`, "pathOutsidePlan", { planId, path });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Passthrough — hand the resolved plan to the wrapped handler.
|
|
187
|
+
return handler(args, { plan });
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=plan-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-guard.js","sourceRoot":"","sources":["../../src/middleware/plan-guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan registry and state management.
|
|
3
|
+
*
|
|
4
|
+
* A plan is the single user-visible permission grant that locks `writes`,
|
|
5
|
+
* `deletes`, and `localDir`. Plans persist to disk at
|
|
6
|
+
* `${GENIE_HOME}/plans/<planId>.json` so they survive server restarts.
|
|
7
|
+
*
|
|
8
|
+
* Plans expire after 1 hour of inactivity (configurable via GENIE_PLAN_TTL).
|
|
9
|
+
*/
|
|
10
|
+
/** Max number of write patterns allowed per plan. */
|
|
11
|
+
export declare const MAX_WRITES = 256;
|
|
12
|
+
/** Max number of wildcards per glob pattern. */
|
|
13
|
+
export declare const MAX_WILDCARDS = 3;
|
|
14
|
+
/** Default plan TTL in milliseconds (1 hour). */
|
|
15
|
+
export declare const DEFAULT_PLAN_TTL: number;
|
|
16
|
+
/** Environment variable to override plan TTL. */
|
|
17
|
+
export declare const PLAN_TTL_ENV = "GENIE_PLAN_TTL";
|
|
18
|
+
/** True if `planId` has the UUID shape every real plan uses. */
|
|
19
|
+
export declare function isValidPlanId(planId: string): boolean;
|
|
20
|
+
/** Plan state persisted to disk. */
|
|
21
|
+
export interface PlanState {
|
|
22
|
+
planId: string;
|
|
23
|
+
kitId: string;
|
|
24
|
+
writes: string[];
|
|
25
|
+
deletes: string[];
|
|
26
|
+
localDir: string;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
lastAccessedAt: string;
|
|
29
|
+
}
|
|
30
|
+
/** Error thrown when too many write patterns are provided. */
|
|
31
|
+
export declare class TooManyWritesError extends Error {
|
|
32
|
+
readonly count: number;
|
|
33
|
+
constructor(count: number);
|
|
34
|
+
}
|
|
35
|
+
/** Error thrown when a glob pattern has too many wildcards. */
|
|
36
|
+
export declare class TooComplexGlobError extends Error {
|
|
37
|
+
readonly pattern: string;
|
|
38
|
+
readonly wildcardCount: number;
|
|
39
|
+
constructor(pattern: string, wildcardCount: number);
|
|
40
|
+
}
|
|
41
|
+
/** Error thrown when a plan is not found or has expired. */
|
|
42
|
+
export declare class PlanNotFoundError extends Error {
|
|
43
|
+
readonly planId: string;
|
|
44
|
+
constructor(planId: string);
|
|
45
|
+
}
|
|
46
|
+
/** Validate glob patterns against complexity limits. */
|
|
47
|
+
export declare function validateGlobPatterns(patterns: string[]): void;
|
|
48
|
+
/** Get the plan TTL in milliseconds. */
|
|
49
|
+
export declare function getPlanTTL(): number;
|
|
50
|
+
/** Create a new plan and persist it. */
|
|
51
|
+
export declare function createPlan(kitId: string, writes: string[], deletes: string[], localDir: string): Promise<PlanState>;
|
|
52
|
+
/** Retrieve a plan by ID, checking expiry. */
|
|
53
|
+
export declare function getPlan(planId: string): Promise<PlanState>;
|
|
54
|
+
/** Check if a path matches any glob pattern in the plan. */
|
|
55
|
+
export declare function pathMatchesGlobs(path: string, globs: string[]): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Validate that a path is inside the plan's localDir.
|
|
58
|
+
*
|
|
59
|
+
* Uses `path.relative`/`path.isAbsolute` rather than a string check against a
|
|
60
|
+
* hard-coded "/" separator, so containment is correct across platforms (POSIX
|
|
61
|
+
* and Windows) — mirrors `safePath` in `store/local.ts` and `read_file.ts`.
|
|
62
|
+
*
|
|
63
|
+
* A relative `path` is resolved against `localDir` (the RFC's base for
|
|
64
|
+
* resolving `localPath` in `write_files`), NOT against `process.cwd()` — so
|
|
65
|
+
* containment stays correct even when the server's cwd differs from localDir.
|
|
66
|
+
* An absolute `path` is checked as-is.
|
|
67
|
+
*/
|
|
68
|
+
export declare function isPathInsideLocalDir(path: string, localDir: string): boolean;
|
|
69
|
+
/** Clear expired plans from the registry and disk (housekeeping). */
|
|
70
|
+
export declare function pruneExpiredPlans(): Promise<number>;
|
|
71
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plans/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgBH,qDAAqD;AACrD,eAAO,MAAM,UAAU,MAAM,CAAC;AAE9B,gDAAgD;AAChD,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B,iDAAiD;AACjD,eAAO,MAAM,gBAAgB,QAAiB,CAAC;AAE/C,iDAAiD;AACjD,eAAO,MAAM,YAAY,mBAAmB,CAAC;AAU7C,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,oCAAoC;AACpC,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,8DAA8D;AAC9D,qBAAa,kBAAmB,SAAQ,KAAK;aACf,KAAK,EAAE,MAAM;gBAAb,KAAK,EAAE,MAAM;CAI1C;AAED,+DAA+D;AAC/D,qBAAa,mBAAoB,SAAQ,KAAK;aAE1B,OAAO,EAAE,MAAM;aACf,aAAa,EAAE,MAAM;gBADrB,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM;CAQxC;AAED,4DAA4D;AAC5D,qBAAa,iBAAkB,SAAQ,KAAK;aACd,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM;CAI3C;AASD,wDAAwD;AACxD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAO7D;AAWD,wCAAwC;AACxC,wBAAgB,UAAU,IAAI,MAAM,CASnC;AA4CD,wCAAwC;AACxC,wBAAsB,UAAU,CAC9B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,MAAM,EAAE,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,SAAS,CAAC,CA6BpB;AAED,8CAA8C;AAC9C,wBAAsB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CA6ChE;AAED,4DAA4D;AAC5D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAEvE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAa5E;AAED,qEAAqE;AACrE,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAezD"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan registry and state management.
|
|
3
|
+
*
|
|
4
|
+
* A plan is the single user-visible permission grant that locks `writes`,
|
|
5
|
+
* `deletes`, and `localDir`. Plans persist to disk at
|
|
6
|
+
* `${GENIE_HOME}/plans/<planId>.json` so they survive server restarts.
|
|
7
|
+
*
|
|
8
|
+
* Plans expire after 1 hour of inactivity (configurable via GENIE_PLAN_TTL).
|
|
9
|
+
*/
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
12
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
13
|
+
// micromatch is CJS-only and assigns its named helpers (isMatch, braces, …)
|
|
14
|
+
// as properties on the exported function itself, rather than as separate
|
|
15
|
+
// `module.exports.foo = ...` assignments. Node's cjs-module-lexer can't
|
|
16
|
+
// statically see those, so both a namespace import (`import * as
|
|
17
|
+
// micromatch`) and a named import (`import { isMatch }`) fail at runtime
|
|
18
|
+
// under real Node ESM (`import * as` silently yields `{default, "module.exports"}`
|
|
19
|
+
// with no `isMatch`; a named import throws SyntaxError at load time) even
|
|
20
|
+
// though Vitest's esbuild-based transform papers over it in tests. A default
|
|
21
|
+
// import is the one shape that works both under esbuild and real Node ESM.
|
|
22
|
+
import micromatch from "micromatch";
|
|
23
|
+
/** Max number of write patterns allowed per plan. */
|
|
24
|
+
export const MAX_WRITES = 256;
|
|
25
|
+
/** Max number of wildcards per glob pattern. */
|
|
26
|
+
export const MAX_WILDCARDS = 3;
|
|
27
|
+
/** Default plan TTL in milliseconds (1 hour). */
|
|
28
|
+
export const DEFAULT_PLAN_TTL = 60 * 60 * 1000;
|
|
29
|
+
/** Environment variable to override plan TTL. */
|
|
30
|
+
export const PLAN_TTL_ENV = "GENIE_PLAN_TTL";
|
|
31
|
+
/**
|
|
32
|
+
* Plan IDs are always UUIDs (see `createPlan` → `randomUUID`). Validating the
|
|
33
|
+
* shape up front lets `getPlan` reject a malformed/hostile `planId` — e.g. a
|
|
34
|
+
* path-traversal value like `"../../x"` — before it is ever interpolated into a
|
|
35
|
+
* `${GENIE_HOME}/plans/<planId>.json` disk path.
|
|
36
|
+
*/
|
|
37
|
+
const PLAN_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
38
|
+
/** True if `planId` has the UUID shape every real plan uses. */
|
|
39
|
+
export function isValidPlanId(planId) {
|
|
40
|
+
return PLAN_ID_PATTERN.test(planId);
|
|
41
|
+
}
|
|
42
|
+
/** Error thrown when too many write patterns are provided. */
|
|
43
|
+
export class TooManyWritesError extends Error {
|
|
44
|
+
count;
|
|
45
|
+
constructor(count) {
|
|
46
|
+
super(`Too many write patterns: ${count}. Maximum allowed is ${MAX_WRITES}.`);
|
|
47
|
+
this.count = count;
|
|
48
|
+
this.name = "TooManyWritesError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Error thrown when a glob pattern has too many wildcards. */
|
|
52
|
+
export class TooComplexGlobError extends Error {
|
|
53
|
+
pattern;
|
|
54
|
+
wildcardCount;
|
|
55
|
+
constructor(pattern, wildcardCount) {
|
|
56
|
+
super(`Glob pattern "${pattern}" has ${wildcardCount} wildcards. ` +
|
|
57
|
+
`Maximum allowed is ${MAX_WILDCARDS}.`);
|
|
58
|
+
this.pattern = pattern;
|
|
59
|
+
this.wildcardCount = wildcardCount;
|
|
60
|
+
this.name = "TooComplexGlobError";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Error thrown when a plan is not found or has expired. */
|
|
64
|
+
export class PlanNotFoundError extends Error {
|
|
65
|
+
planId;
|
|
66
|
+
constructor(planId) {
|
|
67
|
+
super(`Plan "${planId}" not found or expired.`);
|
|
68
|
+
this.planId = planId;
|
|
69
|
+
this.name = "PlanNotFoundError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Count wildcards in a glob pattern. */
|
|
73
|
+
function countWildcards(pattern) {
|
|
74
|
+
// Count * and ** (double-asterisk counts as one wildcard for this rule)
|
|
75
|
+
const stars = pattern.match(/\*+/g) || [];
|
|
76
|
+
return stars.length;
|
|
77
|
+
}
|
|
78
|
+
/** Validate glob patterns against complexity limits. */
|
|
79
|
+
export function validateGlobPatterns(patterns) {
|
|
80
|
+
for (const pattern of patterns) {
|
|
81
|
+
const wildcardCount = countWildcards(pattern);
|
|
82
|
+
if (wildcardCount > MAX_WILDCARDS) {
|
|
83
|
+
throw new TooComplexGlobError(pattern, wildcardCount);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** In-memory plan registry. */
|
|
88
|
+
const planRegistry = new Map();
|
|
89
|
+
/** Get the plans directory path from GENIE_HOME. */
|
|
90
|
+
function getPlansDir() {
|
|
91
|
+
const home = process.env.GENIE_HOME || resolve(process.cwd(), ".genie");
|
|
92
|
+
return resolve(home, "plans");
|
|
93
|
+
}
|
|
94
|
+
/** Get the plan TTL in milliseconds. */
|
|
95
|
+
export function getPlanTTL() {
|
|
96
|
+
const envTTL = process.env[PLAN_TTL_ENV];
|
|
97
|
+
if (envTTL) {
|
|
98
|
+
const parsed = parseInt(envTTL, 10);
|
|
99
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
100
|
+
return parsed;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return DEFAULT_PLAN_TTL;
|
|
104
|
+
}
|
|
105
|
+
/** Load a plan from disk. Returns null if missing or unreadable/corrupt. */
|
|
106
|
+
async function loadPlanFromDisk(planId) {
|
|
107
|
+
const plansDir = getPlansDir();
|
|
108
|
+
const planPath = resolve(plansDir, `${planId}.json`);
|
|
109
|
+
try {
|
|
110
|
+
const content = await readFile(planPath, "utf-8");
|
|
111
|
+
return JSON.parse(content);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Save a plan to disk. */
|
|
118
|
+
async function savePlanToDisk(state) {
|
|
119
|
+
const plansDir = getPlansDir();
|
|
120
|
+
await mkdir(plansDir, { recursive: true });
|
|
121
|
+
const planPath = resolve(plansDir, `${state.planId}.json`);
|
|
122
|
+
await writeFile(planPath, JSON.stringify(state, null, 2), "utf-8");
|
|
123
|
+
}
|
|
124
|
+
/** Delete a plan's disk snapshot, if any. Missing file is not an error. */
|
|
125
|
+
async function deletePlanFromDisk(planId) {
|
|
126
|
+
const plansDir = getPlansDir();
|
|
127
|
+
const planPath = resolve(plansDir, `${planId}.json`);
|
|
128
|
+
try {
|
|
129
|
+
await unlink(planPath);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Already gone (or never persisted) — nothing to clean up.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Check if a plan has expired based on TTL. */
|
|
136
|
+
function isPlanExpired(state) {
|
|
137
|
+
const ttl = getPlanTTL();
|
|
138
|
+
const lastAccessed = new Date(state.lastAccessedAt).getTime();
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
return now - lastAccessed > ttl;
|
|
141
|
+
}
|
|
142
|
+
/** Create a new plan and persist it. */
|
|
143
|
+
export async function createPlan(kitId, writes, deletes, localDir) {
|
|
144
|
+
// Validate write count
|
|
145
|
+
if (writes.length > MAX_WRITES) {
|
|
146
|
+
throw new TooManyWritesError(writes.length);
|
|
147
|
+
}
|
|
148
|
+
// Validate glob complexity
|
|
149
|
+
validateGlobPatterns(writes);
|
|
150
|
+
validateGlobPatterns(deletes);
|
|
151
|
+
// Create plan state
|
|
152
|
+
const planId = randomUUID();
|
|
153
|
+
const now = new Date().toISOString();
|
|
154
|
+
const state = {
|
|
155
|
+
planId,
|
|
156
|
+
kitId,
|
|
157
|
+
writes,
|
|
158
|
+
deletes,
|
|
159
|
+
localDir,
|
|
160
|
+
createdAt: now,
|
|
161
|
+
lastAccessedAt: now,
|
|
162
|
+
};
|
|
163
|
+
// Store in registry and persist
|
|
164
|
+
planRegistry.set(planId, state);
|
|
165
|
+
await savePlanToDisk(state);
|
|
166
|
+
return state;
|
|
167
|
+
}
|
|
168
|
+
/** Retrieve a plan by ID, checking expiry. */
|
|
169
|
+
export async function getPlan(planId) {
|
|
170
|
+
// Reject a malformed/hostile planId before it ever touches a disk path.
|
|
171
|
+
// Every real plan id is a UUID; anything else (e.g. a "../../x" traversal
|
|
172
|
+
// value) cannot correspond to a plan we created, so treat it as not-found.
|
|
173
|
+
if (!isValidPlanId(planId)) {
|
|
174
|
+
throw new PlanNotFoundError(planId);
|
|
175
|
+
}
|
|
176
|
+
// Check in-memory registry first
|
|
177
|
+
let state = planRegistry.get(planId);
|
|
178
|
+
// Fall back to disk if not in memory
|
|
179
|
+
if (!state) {
|
|
180
|
+
state = await loadPlanFromDisk(planId);
|
|
181
|
+
if (state) {
|
|
182
|
+
// Defense in depth: the on-disk JSON is untrusted (a tampered snapshot
|
|
183
|
+
// could carry a `planId` that differs from the file it was read from,
|
|
184
|
+
// e.g. a traversal value that `savePlanToDisk` would later write outside
|
|
185
|
+
// the plans dir). Only trust a snapshot whose embedded id matches the id
|
|
186
|
+
// we looked up.
|
|
187
|
+
if (state.planId !== planId) {
|
|
188
|
+
throw new PlanNotFoundError(planId);
|
|
189
|
+
}
|
|
190
|
+
planRegistry.set(planId, state);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (!state) {
|
|
194
|
+
throw new PlanNotFoundError(planId);
|
|
195
|
+
}
|
|
196
|
+
// Check expiry
|
|
197
|
+
if (isPlanExpired(state)) {
|
|
198
|
+
planRegistry.delete(planId);
|
|
199
|
+
// Clean up the on-disk snapshot too, so expired plans don't accumulate
|
|
200
|
+
// indefinitely under `${GENIE_HOME}/plans/`.
|
|
201
|
+
await deletePlanFromDisk(planId);
|
|
202
|
+
throw new PlanNotFoundError(planId);
|
|
203
|
+
}
|
|
204
|
+
// Update last accessed time
|
|
205
|
+
state.lastAccessedAt = new Date().toISOString();
|
|
206
|
+
await savePlanToDisk(state);
|
|
207
|
+
return state;
|
|
208
|
+
}
|
|
209
|
+
/** Check if a path matches any glob pattern in the plan. */
|
|
210
|
+
export function pathMatchesGlobs(path, globs) {
|
|
211
|
+
return micromatch.isMatch(path, globs, { dot: true });
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Validate that a path is inside the plan's localDir.
|
|
215
|
+
*
|
|
216
|
+
* Uses `path.relative`/`path.isAbsolute` rather than a string check against a
|
|
217
|
+
* hard-coded "/" separator, so containment is correct across platforms (POSIX
|
|
218
|
+
* and Windows) — mirrors `safePath` in `store/local.ts` and `read_file.ts`.
|
|
219
|
+
*
|
|
220
|
+
* A relative `path` is resolved against `localDir` (the RFC's base for
|
|
221
|
+
* resolving `localPath` in `write_files`), NOT against `process.cwd()` — so
|
|
222
|
+
* containment stays correct even when the server's cwd differs from localDir.
|
|
223
|
+
* An absolute `path` is checked as-is.
|
|
224
|
+
*/
|
|
225
|
+
export function isPathInsideLocalDir(path, localDir) {
|
|
226
|
+
const resolvedLocalDir = resolve(localDir);
|
|
227
|
+
const resolvedPath = resolve(resolvedLocalDir, path);
|
|
228
|
+
// Identical paths are trivially "inside".
|
|
229
|
+
if (resolvedPath === resolvedLocalDir) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
const rel = relative(resolvedLocalDir, resolvedPath);
|
|
233
|
+
// `rel` escapes localDir if it's ".." itself, starts with a ".." segment,
|
|
234
|
+
// or is absolute (e.g. a different drive on Windows).
|
|
235
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute(rel);
|
|
236
|
+
}
|
|
237
|
+
/** Clear expired plans from the registry and disk (housekeeping). */
|
|
238
|
+
export async function pruneExpiredPlans() {
|
|
239
|
+
let pruned = 0;
|
|
240
|
+
const now = Date.now();
|
|
241
|
+
const ttl = getPlanTTL();
|
|
242
|
+
for (const [planId, state] of planRegistry.entries()) {
|
|
243
|
+
const lastAccessed = new Date(state.lastAccessedAt).getTime();
|
|
244
|
+
if (now - lastAccessed > ttl) {
|
|
245
|
+
planRegistry.delete(planId);
|
|
246
|
+
await deletePlanFromDisk(planId);
|
|
247
|
+
pruned++;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return pruned;
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/plans/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://genie.dev/schema/component.schema.json",
|
|
4
|
+
"title": "GenieComponent",
|
|
5
|
+
"description": "The file set + manifest metadata conjure/refine demand from the configured LLM endpoint via response_format.json_schema (M2-02).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"componentName",
|
|
10
|
+
"group",
|
|
11
|
+
"files",
|
|
12
|
+
"manifestEntry"
|
|
13
|
+
],
|
|
14
|
+
"definitions": {
|
|
15
|
+
"Viewport": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"additionalProperties": false,
|
|
18
|
+
"required": [
|
|
19
|
+
"width",
|
|
20
|
+
"height"
|
|
21
|
+
],
|
|
22
|
+
"properties": {
|
|
23
|
+
"width": {
|
|
24
|
+
"type": "integer",
|
|
25
|
+
"minimum": 1,
|
|
26
|
+
"maximum": 4096
|
|
27
|
+
},
|
|
28
|
+
"height": {
|
|
29
|
+
"type": "integer",
|
|
30
|
+
"minimum": 1,
|
|
31
|
+
"maximum": 4096
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"properties": {
|
|
37
|
+
"componentName": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "PascalCase component name, e.g. \"Button\".",
|
|
40
|
+
"pattern": "^[A-Z][A-Za-z0-9]{1,63}$"
|
|
41
|
+
},
|
|
42
|
+
"group": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "Kebab-case UI-kit group/category, e.g. \"actions\".",
|
|
45
|
+
"pattern": "^[a-z0-9-]{1,32}$"
|
|
46
|
+
},
|
|
47
|
+
"files": {
|
|
48
|
+
"type": "array",
|
|
49
|
+
"description": "The component's file set (e.g. <Name>.jsx, <Name>.tsx, <Name>.d.ts, <Name>.prompt.md, <Name>.html, meta.json) — at least one must be a <Name>.html preview file (AC5).",
|
|
50
|
+
"minItems": 1,
|
|
51
|
+
"maxItems": 12,
|
|
52
|
+
"contains": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": {
|
|
55
|
+
"path": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"pattern": "^components/[a-z0-9-]+/([A-Z][A-Za-z0-9]{1,63})/\\1\\.html$"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"items": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"additionalProperties": false,
|
|
64
|
+
"required": [
|
|
65
|
+
"path",
|
|
66
|
+
"content",
|
|
67
|
+
"mimeType"
|
|
68
|
+
],
|
|
69
|
+
"properties": {
|
|
70
|
+
"path": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "Kit-relative path under components/<group>/<Name>/.",
|
|
73
|
+
"pattern": "^components/[a-z0-9-]+/[A-Z][A-Za-z0-9]+/[A-Za-z0-9._-]+$"
|
|
74
|
+
},
|
|
75
|
+
"content": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Full file contents (AC5: an <Name>.html entry's content must begin with the @genie marker — checked post-hoc by M3-01/M2-07, not by this schema).",
|
|
78
|
+
"minLength": 1,
|
|
79
|
+
"maxLength": 65536
|
|
80
|
+
},
|
|
81
|
+
"mimeType": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"description": "MIME type of `content`, e.g. \"text/html\".",
|
|
84
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"manifestEntry": {
|
|
90
|
+
"type": "object",
|
|
91
|
+
"description": "Metadata the manifest compiler (M3-03) needs to add this card to .genie/manifest.json without re-deriving it from the files themselves.",
|
|
92
|
+
"additionalProperties": false,
|
|
93
|
+
"required": [
|
|
94
|
+
"viewport"
|
|
95
|
+
],
|
|
96
|
+
"properties": {
|
|
97
|
+
"viewport": {
|
|
98
|
+
"$ref": "#/definitions/Viewport"
|
|
99
|
+
},
|
|
100
|
+
"subtitle": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"maxLength": 256
|
|
103
|
+
},
|
|
104
|
+
"tags": {
|
|
105
|
+
"type": "array",
|
|
106
|
+
"items": {
|
|
107
|
+
"type": "string"
|
|
108
|
+
},
|
|
109
|
+
"maxItems": 16
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|