@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.
Files changed (301) hide show
  1. package/README.md +15 -0
  2. package/dist/auth/bearer.d.ts +49 -0
  3. package/dist/auth/bearer.d.ts.map +1 -0
  4. package/dist/auth/bearer.js +159 -0
  5. package/dist/auth/bearer.js.map +1 -0
  6. package/dist/auth/oauth/authorize.d.ts +19 -0
  7. package/dist/auth/oauth/authorize.d.ts.map +1 -0
  8. package/dist/auth/oauth/authorize.js +111 -0
  9. package/dist/auth/oauth/authorize.js.map +1 -0
  10. package/dist/auth/oauth/config.d.ts +10 -0
  11. package/dist/auth/oauth/config.d.ts.map +1 -0
  12. package/dist/auth/oauth/config.js +16 -0
  13. package/dist/auth/oauth/config.js.map +1 -0
  14. package/dist/auth/oauth/dcr.d.ts +15 -0
  15. package/dist/auth/oauth/dcr.d.ts.map +1 -0
  16. package/dist/auth/oauth/dcr.js +38 -0
  17. package/dist/auth/oauth/dcr.js.map +1 -0
  18. package/dist/auth/oauth/jwt.d.ts +13 -0
  19. package/dist/auth/oauth/jwt.d.ts.map +1 -0
  20. package/dist/auth/oauth/jwt.js +46 -0
  21. package/dist/auth/oauth/jwt.js.map +1 -0
  22. package/dist/auth/oauth/metadata.d.ts +9 -0
  23. package/dist/auth/oauth/metadata.d.ts.map +1 -0
  24. package/dist/auth/oauth/metadata.js +19 -0
  25. package/dist/auth/oauth/metadata.js.map +1 -0
  26. package/dist/auth/oauth/router.d.ts +32 -0
  27. package/dist/auth/oauth/router.d.ts.map +1 -0
  28. package/dist/auth/oauth/router.js +159 -0
  29. package/dist/auth/oauth/router.js.map +1 -0
  30. package/dist/auth/oauth/store.d.ts +55 -0
  31. package/dist/auth/oauth/store.d.ts.map +1 -0
  32. package/dist/auth/oauth/store.js +63 -0
  33. package/dist/auth/oauth/store.js.map +1 -0
  34. package/dist/auth/oauth/token.d.ts +21 -0
  35. package/dist/auth/oauth/token.d.ts.map +1 -0
  36. package/dist/auth/oauth/token.js +77 -0
  37. package/dist/auth/oauth/token.js.map +1 -0
  38. package/dist/auth/oidc/group-policy.d.ts +43 -0
  39. package/dist/auth/oidc/group-policy.d.ts.map +1 -0
  40. package/dist/auth/oidc/group-policy.js +54 -0
  41. package/dist/auth/oidc/group-policy.js.map +1 -0
  42. package/dist/auth/oidc/verifier.d.ts +42 -0
  43. package/dist/auth/oidc/verifier.d.ts.map +1 -0
  44. package/dist/auth/oidc/verifier.js +145 -0
  45. package/dist/auth/oidc/verifier.js.map +1 -0
  46. package/dist/auth/token-cli.d.ts +9 -0
  47. package/dist/auth/token-cli.d.ts.map +1 -0
  48. package/dist/auth/token-cli.js +98 -0
  49. package/dist/auth/token-cli.js.map +1 -0
  50. package/dist/cli.d.ts +3 -0
  51. package/dist/cli.d.ts.map +1 -0
  52. package/dist/cli.js +149 -0
  53. package/dist/cli.js.map +1 -0
  54. package/dist/config/redact.d.ts +20 -0
  55. package/dist/config/redact.d.ts.map +1 -0
  56. package/dist/config/redact.js +77 -0
  57. package/dist/config/redact.js.map +1 -0
  58. package/dist/config/secrets.d.ts +100 -0
  59. package/dist/config/secrets.d.ts.map +1 -0
  60. package/dist/config/secrets.js +130 -0
  61. package/dist/config/secrets.js.map +1 -0
  62. package/dist/framework/html.d.ts +103 -0
  63. package/dist/framework/html.d.ts.map +1 -0
  64. package/dist/framework/html.js +271 -0
  65. package/dist/framework/html.js.map +1 -0
  66. package/dist/framework/interface.d.ts +130 -0
  67. package/dist/framework/interface.d.ts.map +1 -0
  68. package/dist/framework/interface.js +112 -0
  69. package/dist/framework/interface.js.map +1 -0
  70. package/dist/framework/react.d.ts +133 -0
  71. package/dist/framework/react.d.ts.map +1 -0
  72. package/dist/framework/react.js +243 -0
  73. package/dist/framework/react.js.map +1 -0
  74. package/dist/framework/vue.d.ts +99 -0
  75. package/dist/framework/vue.d.ts.map +1 -0
  76. package/dist/framework/vue.js +375 -0
  77. package/dist/framework/vue.js.map +1 -0
  78. package/dist/index.d.ts +3 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +3 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/llm/client.d.ts +81 -0
  83. package/dist/llm/client.d.ts.map +1 -0
  84. package/dist/llm/client.js +141 -0
  85. package/dist/llm/client.js.map +1 -0
  86. package/dist/llm/component-response.d.ts +111 -0
  87. package/dist/llm/component-response.d.ts.map +1 -0
  88. package/dist/llm/component-response.js +167 -0
  89. package/dist/llm/component-response.js.map +1 -0
  90. package/dist/llm/generated-files.d.ts +10 -0
  91. package/dist/llm/generated-files.d.ts.map +1 -0
  92. package/dist/llm/generated-files.js +26 -0
  93. package/dist/llm/generated-files.js.map +1 -0
  94. package/dist/llm/prompts/CHANGELOG.md +70 -0
  95. package/dist/llm/prompts/generate-component.system.md +99 -0
  96. package/dist/llm/prompts/refine-component.system.md +112 -0
  97. package/dist/llm/prompts.d.ts +66 -0
  98. package/dist/llm/prompts.d.ts.map +1 -0
  99. package/dist/llm/prompts.js +78 -0
  100. package/dist/llm/prompts.js.map +1 -0
  101. package/dist/llm/retry.d.ts +183 -0
  102. package/dist/llm/retry.d.ts.map +1 -0
  103. package/dist/llm/retry.js +475 -0
  104. package/dist/llm/retry.js.map +1 -0
  105. package/dist/llm/schema.d.ts +129 -0
  106. package/dist/llm/schema.d.ts.map +1 -0
  107. package/dist/llm/schema.js +147 -0
  108. package/dist/llm/schema.js.map +1 -0
  109. package/dist/llm/validate.d.ts +107 -0
  110. package/dist/llm/validate.d.ts.map +1 -0
  111. package/dist/llm/validate.js +174 -0
  112. package/dist/llm/validate.js.map +1 -0
  113. package/dist/manifest/compiler.d.ts +112 -0
  114. package/dist/manifest/compiler.d.ts.map +1 -0
  115. package/dist/manifest/compiler.js +306 -0
  116. package/dist/manifest/compiler.js.map +1 -0
  117. package/dist/manifest/ensure.d.ts +46 -0
  118. package/dist/manifest/ensure.d.ts.map +1 -0
  119. package/dist/manifest/ensure.js +49 -0
  120. package/dist/manifest/ensure.js.map +1 -0
  121. package/dist/manifest/index.d.ts +13 -0
  122. package/dist/manifest/index.d.ts.map +1 -0
  123. package/dist/manifest/index.js +13 -0
  124. package/dist/manifest/index.js.map +1 -0
  125. package/dist/middleware/plan-guard.d.ts +167 -0
  126. package/dist/middleware/plan-guard.d.ts.map +1 -0
  127. package/dist/middleware/plan-guard.js +190 -0
  128. package/dist/middleware/plan-guard.js.map +1 -0
  129. package/dist/plans/index.d.ts +71 -0
  130. package/dist/plans/index.d.ts.map +1 -0
  131. package/dist/plans/index.js +252 -0
  132. package/dist/plans/index.js.map +1 -0
  133. package/dist/schemas/component.schema.json +114 -0
  134. package/dist/server.d.ts +96 -0
  135. package/dist/server.d.ts.map +1 -0
  136. package/dist/server.js +258 -0
  137. package/dist/server.js.map +1 -0
  138. package/dist/store/empty-manifest.d.ts +58 -0
  139. package/dist/store/empty-manifest.d.ts.map +1 -0
  140. package/dist/store/empty-manifest.js +59 -0
  141. package/dist/store/empty-manifest.js.map +1 -0
  142. package/dist/store/git-host.d.ts +120 -0
  143. package/dist/store/git-host.d.ts.map +1 -0
  144. package/dist/store/git-host.js +796 -0
  145. package/dist/store/git-host.js.map +1 -0
  146. package/dist/store/index.d.ts +5 -0
  147. package/dist/store/index.d.ts.map +1 -0
  148. package/dist/store/index.js +4 -0
  149. package/dist/store/index.js.map +1 -0
  150. package/dist/store/interface.d.ts +322 -0
  151. package/dist/store/interface.d.ts.map +1 -0
  152. package/dist/store/interface.js +100 -0
  153. package/dist/store/interface.js.map +1 -0
  154. package/dist/store/kit-files.d.ts +88 -0
  155. package/dist/store/kit-files.d.ts.map +1 -0
  156. package/dist/store/kit-files.js +199 -0
  157. package/dist/store/kit-files.js.map +1 -0
  158. package/dist/store/local.d.ts +70 -0
  159. package/dist/store/local.d.ts.map +1 -0
  160. package/dist/store/local.js +651 -0
  161. package/dist/store/local.js.map +1 -0
  162. package/dist/store/manifest.d.ts +44 -0
  163. package/dist/store/manifest.d.ts.map +1 -0
  164. package/dist/store/manifest.js +116 -0
  165. package/dist/store/manifest.js.map +1 -0
  166. package/dist/store/viewer-assets.d.ts +20 -0
  167. package/dist/store/viewer-assets.d.ts.map +1 -0
  168. package/dist/store/viewer-assets.js +118 -0
  169. package/dist/store/viewer-assets.js.map +1 -0
  170. package/dist/sync/anchor.d.ts +125 -0
  171. package/dist/sync/anchor.d.ts.map +1 -0
  172. package/dist/sync/anchor.js +210 -0
  173. package/dist/sync/anchor.js.map +1 -0
  174. package/dist/sync/index.d.ts +15 -0
  175. package/dist/sync/index.d.ts.map +1 -0
  176. package/dist/sync/index.js +15 -0
  177. package/dist/sync/index.js.map +1 -0
  178. package/dist/sync/orchestrator.d.ts +160 -0
  179. package/dist/sync/orchestrator.d.ts.map +1 -0
  180. package/dist/sync/orchestrator.js +288 -0
  181. package/dist/sync/orchestrator.js.map +1 -0
  182. package/dist/tools/bind_kit.d.ts +29 -0
  183. package/dist/tools/bind_kit.d.ts.map +1 -0
  184. package/dist/tools/bind_kit.js +70 -0
  185. package/dist/tools/bind_kit.js.map +1 -0
  186. package/dist/tools/conjure.d.ts +141 -0
  187. package/dist/tools/conjure.d.ts.map +1 -0
  188. package/dist/tools/conjure.js +655 -0
  189. package/dist/tools/conjure.js.map +1 -0
  190. package/dist/tools/conjure_screen.d.ts +174 -0
  191. package/dist/tools/conjure_screen.d.ts.map +1 -0
  192. package/dist/tools/conjure_screen.js +458 -0
  193. package/dist/tools/conjure_screen.js.map +1 -0
  194. package/dist/tools/create_kit.d.ts +32 -0
  195. package/dist/tools/create_kit.d.ts.map +1 -0
  196. package/dist/tools/create_kit.js +131 -0
  197. package/dist/tools/create_kit.js.map +1 -0
  198. package/dist/tools/create_project.d.ts +267 -0
  199. package/dist/tools/create_project.d.ts.map +1 -0
  200. package/dist/tools/create_project.js +546 -0
  201. package/dist/tools/create_project.js.map +1 -0
  202. package/dist/tools/delete_files.d.ts +93 -0
  203. package/dist/tools/delete_files.d.ts.map +1 -0
  204. package/dist/tools/delete_files.js +276 -0
  205. package/dist/tools/delete_files.js.map +1 -0
  206. package/dist/tools/delete_project.d.ts +43 -0
  207. package/dist/tools/delete_project.d.ts.map +1 -0
  208. package/dist/tools/delete_project.js +118 -0
  209. package/dist/tools/delete_project.js.map +1 -0
  210. package/dist/tools/get_kit.d.ts +28 -0
  211. package/dist/tools/get_kit.d.ts.map +1 -0
  212. package/dist/tools/get_kit.js +95 -0
  213. package/dist/tools/get_kit.js.map +1 -0
  214. package/dist/tools/get_project.d.ts +37 -0
  215. package/dist/tools/get_project.d.ts.map +1 -0
  216. package/dist/tools/get_project.js +71 -0
  217. package/dist/tools/get_project.js.map +1 -0
  218. package/dist/tools/list_components.d.ts +42 -0
  219. package/dist/tools/list_components.d.ts.map +1 -0
  220. package/dist/tools/list_components.js +125 -0
  221. package/dist/tools/list_components.js.map +1 -0
  222. package/dist/tools/list_files.d.ts +32 -0
  223. package/dist/tools/list_files.d.ts.map +1 -0
  224. package/dist/tools/list_files.js +109 -0
  225. package/dist/tools/list_files.js.map +1 -0
  226. package/dist/tools/list_kits.d.ts +14 -0
  227. package/dist/tools/list_kits.d.ts.map +1 -0
  228. package/dist/tools/list_kits.js +41 -0
  229. package/dist/tools/list_kits.js.map +1 -0
  230. package/dist/tools/list_projects.d.ts +23 -0
  231. package/dist/tools/list_projects.d.ts.map +1 -0
  232. package/dist/tools/list_projects.js +66 -0
  233. package/dist/tools/list_projects.js.map +1 -0
  234. package/dist/tools/plan.d.ts +16 -0
  235. package/dist/tools/plan.d.ts.map +1 -0
  236. package/dist/tools/plan.js +146 -0
  237. package/dist/tools/plan.js.map +1 -0
  238. package/dist/tools/preview.d.ts +289 -0
  239. package/dist/tools/preview.d.ts.map +1 -0
  240. package/dist/tools/preview.js +672 -0
  241. package/dist/tools/preview.js.map +1 -0
  242. package/dist/tools/read_file.d.ts +40 -0
  243. package/dist/tools/read_file.d.ts.map +1 -0
  244. package/dist/tools/read_file.js +114 -0
  245. package/dist/tools/read_file.js.map +1 -0
  246. package/dist/tools/refine.d.ts +220 -0
  247. package/dist/tools/refine.d.ts.map +1 -0
  248. package/dist/tools/refine.js +501 -0
  249. package/dist/tools/refine.js.map +1 -0
  250. package/dist/tools/validate.d.ts +74 -0
  251. package/dist/tools/validate.d.ts.map +1 -0
  252. package/dist/tools/validate.js +224 -0
  253. package/dist/tools/validate.js.map +1 -0
  254. package/dist/tools/write_files.d.ts +158 -0
  255. package/dist/tools/write_files.d.ts.map +1 -0
  256. package/dist/tools/write_files.js +541 -0
  257. package/dist/tools/write_files.js.map +1 -0
  258. package/dist/transport.d.ts +77 -0
  259. package/dist/transport.d.ts.map +1 -0
  260. package/dist/transport.js +448 -0
  261. package/dist/transport.js.map +1 -0
  262. package/dist/ui/card-asset-broker.d.ts +42 -0
  263. package/dist/ui/card-asset-broker.d.ts.map +1 -0
  264. package/dist/ui/card-asset-broker.js +479 -0
  265. package/dist/ui/card-asset-broker.js.map +1 -0
  266. package/dist/ui/grid-resource.d.ts +252 -0
  267. package/dist/ui/grid-resource.d.ts.map +1 -0
  268. package/dist/ui/grid-resource.js +680 -0
  269. package/dist/ui/grid-resource.js.map +1 -0
  270. package/dist/ui/viewer-static/index.html +95 -0
  271. package/dist/ui/viewer-static/viewer.css +451 -0
  272. package/dist/ui/viewer-static/viewer.js +1405 -0
  273. package/dist/validate/full-scan.d.ts +87 -0
  274. package/dist/validate/full-scan.d.ts.map +1 -0
  275. package/dist/validate/full-scan.js +299 -0
  276. package/dist/validate/full-scan.js.map +1 -0
  277. package/dist/validate/index.d.ts +15 -0
  278. package/dist/validate/index.d.ts.map +1 -0
  279. package/dist/validate/index.js +18 -0
  280. package/dist/validate/index.js.map +1 -0
  281. package/dist/validate/marker.d.ts +89 -0
  282. package/dist/validate/marker.d.ts.map +1 -0
  283. package/dist/validate/marker.js +87 -0
  284. package/dist/validate/marker.js.map +1 -0
  285. package/dist/validate/phash.d.ts +135 -0
  286. package/dist/validate/phash.d.ts.map +1 -0
  287. package/dist/validate/phash.js +307 -0
  288. package/dist/validate/phash.js.map +1 -0
  289. package/dist/validate/render.d.ts +38 -0
  290. package/dist/validate/render.d.ts.map +1 -0
  291. package/dist/validate/render.js +155 -0
  292. package/dist/validate/render.js.map +1 -0
  293. package/dist/watch/index.d.ts +12 -0
  294. package/dist/watch/index.d.ts.map +1 -0
  295. package/dist/watch/index.js +12 -0
  296. package/dist/watch/index.js.map +1 -0
  297. package/dist/watch/watcher.d.ts +65 -0
  298. package/dist/watch/watcher.d.ts.map +1 -0
  299. package/dist/watch/watcher.js +238 -0
  300. package/dist/watch/watcher.js.map +1 -0
  301. 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
+ }