@try-works/dsh-model-as-tool 0.1.2 → 0.3.6

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 (267) hide show
  1. package/bin/dsh-tool-mode-credentials.mjs +20 -0
  2. package/bin/dsh-tool-mode.mjs +202 -0
  3. package/catalog/desert-ant-models.json +693 -693
  4. package/client/client.model-tools.js +592 -0
  5. package/client/index.js +1679 -0
  6. package/lib/adapters/index.d.ts.map +1 -1
  7. package/lib/adapters/index.js +4 -4
  8. package/lib/bindings.d.ts.map +1 -1
  9. package/lib/bindings.js +26 -25
  10. package/lib/capability.d.ts.map +1 -1
  11. package/lib/capability.js +17 -0
  12. package/lib/capture-path.d.ts +97 -0
  13. package/lib/capture-path.d.ts.map +1 -0
  14. package/lib/capture-path.js +152 -0
  15. package/lib/diagnostics.d.ts +166 -0
  16. package/lib/diagnostics.d.ts.map +1 -0
  17. package/lib/diagnostics.js +135 -0
  18. package/lib/extension-dependencies.d.ts +197 -0
  19. package/lib/extension-dependencies.d.ts.map +1 -0
  20. package/lib/extension-dependencies.js +369 -0
  21. package/lib/extension-dispatch.d.ts +117 -0
  22. package/lib/extension-dispatch.d.ts.map +1 -0
  23. package/lib/extension-dispatch.js +124 -0
  24. package/lib/extension-registrar.d.ts +100 -0
  25. package/lib/extension-registrar.d.ts.map +1 -0
  26. package/lib/extension-registrar.js +134 -0
  27. package/lib/extensions-api.d.ts +217 -0
  28. package/lib/extensions-api.d.ts.map +1 -0
  29. package/lib/extensions-api.js +17 -0
  30. package/lib/extensions-remote.d.ts +54 -0
  31. package/lib/extensions-remote.d.ts.map +1 -0
  32. package/lib/extensions-remote.js +66 -0
  33. package/lib/extensions-service.d.ts +334 -0
  34. package/lib/extensions-service.d.ts.map +1 -0
  35. package/lib/extensions-service.js +1351 -0
  36. package/lib/harness-llm.d.ts +138 -0
  37. package/lib/harness-llm.d.ts.map +1 -0
  38. package/lib/harness-llm.js +218 -0
  39. package/lib/harness-serving.d.ts +95 -0
  40. package/lib/harness-serving.d.ts.map +1 -0
  41. package/lib/harness-serving.js +157 -0
  42. package/lib/headless.d.ts +97 -0
  43. package/lib/headless.d.ts.map +1 -0
  44. package/lib/headless.js +349 -0
  45. package/lib/http-acquisition.d.ts +27 -0
  46. package/lib/http-acquisition.d.ts.map +1 -0
  47. package/lib/http-acquisition.js +136 -0
  48. package/lib/index.d.ts +250 -7
  49. package/lib/index.d.ts.map +1 -1
  50. package/lib/index.js +2625 -70
  51. package/lib/invocation-accounting.d.ts +169 -0
  52. package/lib/invocation-accounting.d.ts.map +1 -0
  53. package/lib/invocation-accounting.js +148 -0
  54. package/lib/legacy-migration.d.ts +95 -0
  55. package/lib/legacy-migration.d.ts.map +1 -0
  56. package/lib/legacy-migration.js +429 -0
  57. package/lib/managed-workers.d.ts +151 -0
  58. package/lib/managed-workers.d.ts.map +1 -0
  59. package/lib/managed-workers.js +249 -0
  60. package/lib/mcp/client-grant.d.ts +74 -0
  61. package/lib/mcp/client-grant.d.ts.map +1 -0
  62. package/lib/mcp/client-grant.js +60 -0
  63. package/lib/mcp/credential-cli.d.ts +38 -0
  64. package/lib/mcp/credential-cli.d.ts.map +1 -0
  65. package/lib/mcp/credential-cli.js +148 -0
  66. package/lib/mcp/credential-store.d.ts +143 -0
  67. package/lib/mcp/credential-store.d.ts.map +1 -0
  68. package/lib/mcp/credential-store.js +454 -0
  69. package/lib/mcp/host-binding.d.ts +25 -0
  70. package/lib/mcp/host-binding.d.ts.map +1 -1
  71. package/lib/mcp/host-binding.js +107 -14
  72. package/lib/mcp/http.d.ts +68 -18
  73. package/lib/mcp/http.d.ts.map +1 -1
  74. package/lib/mcp/http.js +390 -62
  75. package/lib/mcp/oauth.d.ts +149 -0
  76. package/lib/mcp/oauth.d.ts.map +1 -0
  77. package/lib/mcp/oauth.js +396 -0
  78. package/lib/mcp/protocol.d.ts +46 -7
  79. package/lib/mcp/protocol.d.ts.map +1 -1
  80. package/lib/mcp/protocol.js +63 -16
  81. package/lib/mcp/rate-limit.d.ts +68 -0
  82. package/lib/mcp/rate-limit.d.ts.map +1 -0
  83. package/lib/mcp/rate-limit.js +114 -0
  84. package/lib/mcp/sdk-v2.d.ts +17 -0
  85. package/lib/mcp/sdk-v2.d.ts.map +1 -0
  86. package/lib/mcp/sdk-v2.js +181 -0
  87. package/lib/mcp/server.d.ts +11 -1
  88. package/lib/mcp/server.d.ts.map +1 -1
  89. package/lib/mcp/server.js +80 -13
  90. package/lib/mcp/stdio.d.ts.map +1 -1
  91. package/lib/mcp/stdio.js +9 -1
  92. package/lib/mcp/subscriptions.d.ts +43 -1
  93. package/lib/mcp/subscriptions.d.ts.map +1 -1
  94. package/lib/mcp/subscriptions.js +90 -9
  95. package/lib/model-qualification.d.ts +192 -0
  96. package/lib/model-qualification.d.ts.map +1 -0
  97. package/lib/model-qualification.js +178 -0
  98. package/lib/planning.d.ts +246 -0
  99. package/lib/planning.d.ts.map +1 -0
  100. package/lib/planning.js +457 -0
  101. package/lib/prepared-assets.d.ts +178 -0
  102. package/lib/prepared-assets.d.ts.map +1 -0
  103. package/lib/prepared-assets.js +286 -0
  104. package/lib/provider-bridge.d.ts +37 -0
  105. package/lib/provider-bridge.d.ts.map +1 -1
  106. package/lib/provider-bridge.js +189 -12
  107. package/lib/ptc-authority.d.ts +140 -0
  108. package/lib/ptc-authority.d.ts.map +1 -0
  109. package/lib/ptc-authority.js +216 -0
  110. package/lib/ptc-runtime.d.ts +156 -0
  111. package/lib/ptc-runtime.d.ts.map +1 -0
  112. package/lib/ptc-runtime.js +191 -0
  113. package/lib/publication.d.ts +2 -1
  114. package/lib/publication.d.ts.map +1 -1
  115. package/lib/publication.js +13 -5
  116. package/lib/receipt.d.ts.map +1 -1
  117. package/lib/receipt.js +8 -0
  118. package/lib/title-proposal.d.ts +194 -0
  119. package/lib/title-proposal.d.ts.map +1 -0
  120. package/lib/title-proposal.js +244 -0
  121. package/lib/tool-mode-run.d.ts +235 -0
  122. package/lib/tool-mode-run.d.ts.map +1 -0
  123. package/lib/tool-mode-run.js +283 -0
  124. package/lib/types.d.ts +3 -1
  125. package/lib/types.d.ts.map +1 -1
  126. package/lib/update-surface.d.ts +79 -0
  127. package/lib/update-surface.d.ts.map +1 -0
  128. package/lib/update-surface.js +224 -0
  129. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts +16 -0
  130. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts.map +1 -0
  131. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.js +85 -0
  132. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts +38 -0
  133. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts.map +1 -0
  134. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.js +445 -0
  135. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts +20 -0
  136. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts.map +1 -0
  137. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.js +112 -0
  138. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts +14 -0
  139. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts.map +1 -0
  140. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.js +63 -0
  141. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts +14 -0
  142. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts.map +1 -0
  143. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.js +76 -0
  144. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts +82 -0
  145. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts.map +1 -0
  146. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.js +274 -0
  147. package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts +26 -0
  148. package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts.map +1 -0
  149. package/node_modules/@try-works/tool-mode-contracts/lib/index.js +48 -0
  150. package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts +104 -0
  151. package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts.map +1 -0
  152. package/node_modules/@try-works/tool-mode-contracts/lib/meta.js +55 -0
  153. package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +107 -0
  154. package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts.map +1 -0
  155. package/node_modules/@try-works/tool-mode-contracts/lib/registry.js +246 -0
  156. package/node_modules/@try-works/tool-mode-contracts/package.json +34 -0
  157. package/node_modules/@try-works/tool-mode-extension-sdk/bin/dsh-tool-mode.mjs +20 -0
  158. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts +80 -0
  159. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts.map +1 -0
  160. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.js +533 -0
  161. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts +83 -0
  162. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts.map +1 -0
  163. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.js +125 -0
  164. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts +70 -0
  165. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts.map +1 -0
  166. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.js +201 -0
  167. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts +57 -0
  168. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts.map +1 -0
  169. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.js +83 -0
  170. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts +31 -0
  171. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts.map +1 -0
  172. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.js +31 -0
  173. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts +86 -0
  174. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts.map +1 -0
  175. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.js +185 -0
  176. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts +126 -0
  177. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts.map +1 -0
  178. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.js +297 -0
  179. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts +216 -0
  180. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts.map +1 -0
  181. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.js +800 -0
  182. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts +210 -0
  183. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts.map +1 -0
  184. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.js +368 -0
  185. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts +63 -0
  186. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts.map +1 -0
  187. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.js +149 -0
  188. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts +35 -0
  189. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts.map +1 -0
  190. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.js +179 -0
  191. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts +179 -0
  192. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts.map +1 -0
  193. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.js +235 -0
  194. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts +367 -0
  195. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts.map +1 -0
  196. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.js +807 -0
  197. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts +185 -0
  198. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts.map +1 -0
  199. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.js +504 -0
  200. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts +17 -0
  201. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts.map +1 -0
  202. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.js +80 -0
  203. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts +52 -0
  204. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts.map +1 -0
  205. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.js +102 -0
  206. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts +171 -0
  207. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts.map +1 -0
  208. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.js +271 -0
  209. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts +242 -0
  210. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts.map +1 -0
  211. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.js +58 -0
  212. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts +118 -0
  213. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts.map +1 -0
  214. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.js +113 -0
  215. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts +2 -0
  216. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts.map +1 -0
  217. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.js +351 -0
  218. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts +150 -0
  219. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts.map +1 -0
  220. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.js +670 -0
  221. package/node_modules/@try-works/tool-mode-extension-sdk/package.json +53 -0
  222. package/node_modules/@try-works/tool-mode-extension-sdk/schema/extension-manifest-1.1.0.schema.json +253 -0
  223. package/node_modules/@try-works/tool-mode-recipes/bin/tool-mode-recipe.mjs +15 -0
  224. package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts +427 -0
  225. package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts.map +1 -0
  226. package/node_modules/@try-works/tool-mode-recipes/lib/broker.js +592 -0
  227. package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts +60 -0
  228. package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts.map +1 -0
  229. package/node_modules/@try-works/tool-mode-recipes/lib/cli.js +936 -0
  230. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts +237 -0
  231. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts.map +1 -0
  232. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.js +200 -0
  233. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts +85 -0
  234. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts.map +1 -0
  235. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.js +176 -0
  236. package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts +105 -0
  237. package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts.map +1 -0
  238. package/node_modules/@try-works/tool-mode-recipes/lib/images.js +129 -0
  239. package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts +25 -0
  240. package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts.map +1 -0
  241. package/node_modules/@try-works/tool-mode-recipes/lib/index.js +25 -0
  242. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts +325 -0
  243. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts.map +1 -0
  244. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.js +1310 -0
  245. package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts +51 -0
  246. package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts.map +1 -0
  247. package/node_modules/@try-works/tool-mode-recipes/lib/templates.js +310 -0
  248. package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts +227 -0
  249. package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts.map +1 -0
  250. package/node_modules/@try-works/tool-mode-recipes/lib/titles.js +407 -0
  251. package/node_modules/@try-works/tool-mode-recipes/package.json +56 -0
  252. package/node_modules/@try-works/tool-mode-recipes/recipes/describe-image.json +38 -0
  253. package/node_modules/@try-works/tool-mode-recipes/recipes/text-title.json +45 -0
  254. package/node_modules/@try-works/tool-mode-recipes/schema/harness-model-recipe.schema.json +273 -0
  255. package/package.json +50 -17
  256. package/remote/spec.mjs +439 -0
  257. package/remote/typert.host.mjs +14 -0
  258. package/remote/typert.remote-client.mjs +16 -0
  259. package/lib/adapters/tongue.d.ts +0 -31
  260. package/lib/adapters/tongue.d.ts.map +0 -1
  261. package/lib/adapters/tongue.js +0 -93
  262. package/lib/adapters/voz.d.ts +0 -28
  263. package/lib/adapters/voz.d.ts.map +0 -1
  264. package/lib/adapters/voz.js +0 -102
  265. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts +0 -0
  266. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts.map +0 -0
  267. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.js +0 -0
@@ -0,0 +1,936 @@
1
+ /**
2
+ * The recipe authoring CLI (RCP-08).
3
+ *
4
+ * Three commands, each a required new tool rather than an existing npm/DSH
5
+ * command: `validate` checks a draft against BOTH authorities (the published
6
+ * `harness-model-recipe.schema.json` shipped beside this module AND the code
7
+ * validator `parseRecipe`), `describe` prints the effective bounds an operator
8
+ * reads before deciding, and `pack` builds a declarative-extension package
9
+ * (manifest + recipes, NO executable code) consumable by the extension install
10
+ * path.
11
+ *
12
+ * Guardrails built in rather than documented:
13
+ *
14
+ * - The schema is read from THIS PACKAGE'S SHIPPED FILE, resolved relative to
15
+ * the module (so a packed install validates against the packed artifact, not
16
+ * some source checkout).
17
+ * - Packing validates the exact production artifact and runs NO lifecycle
18
+ * scripts: the output is written directly (directory + a store-method zip),
19
+ * never through `npm pack`.
20
+ * - Nothing here runs inference, installs or activates anything, or touches
21
+ * the network (RCP-08's creation rules).
22
+ * - Exit codes are a contract: 0 valid, 1 invalid (every refusal named),
23
+ * 2 usage or IO failure.
24
+ *
25
+ * RCP-09 (agent-assisted creation with an authoring grant) is NOT claimed by
26
+ * this module: the CLI is a plain argv/stdio tool, which an agent CAN drive,
27
+ * but the authoring-grant and draft-directory policy belong to the host and are
28
+ * left as the recorded remainder.
29
+ *
30
+ * @module @try-works/tool-mode-recipes/cli
31
+ */
32
+ import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
33
+ import { fileURLToPath } from 'node:url';
34
+ import { dirname, join, parse, resolve, sep } from 'node:path';
35
+ import Ajv2020 from 'ajv/dist/2020.js';
36
+ import { INITIAL_MAX_IMAGE_INPUTS, INITIAL_MAX_OUTPUT_TOKENS, parseRecipe, toCanonicalRecipe, } from "./recipe-format.js";
37
+ import { EDITABLE_FIELDS, policyEditRefusal, templateFiles, validateCreateArguments, } from "./templates.js";
38
+ import { runFixtureSuite, validateFixtureDocument, } from "./fixture-provider.js";
39
+ import { INITIAL_HOST_POLICY } from "./broker.js";
40
+ /** The published schema, shipped beside this module (RCP-02's artifact). */
41
+ const SHIPPED_SCHEMA_URL = new URL('../schema/harness-model-recipe.schema.json', import.meta.url);
42
+ /** Load and compile the shipped schema, once per invocation. */
43
+ function schemaValidator() {
44
+ let text;
45
+ try {
46
+ text = readFileSync(fileURLToPath(SHIPPED_SCHEMA_URL), 'utf8');
47
+ }
48
+ catch (error) {
49
+ return { validate: () => [], loadError: `the shipped schema could not be read: ${messageOf(error)}` };
50
+ }
51
+ let schema;
52
+ try {
53
+ schema = JSON.parse(text);
54
+ }
55
+ catch (error) {
56
+ return { validate: () => [], loadError: `the shipped schema is not valid JSON: ${messageOf(error)}` };
57
+ }
58
+ try {
59
+ const validate = new Ajv2020({ strict: true, allErrors: true }).compile(schema);
60
+ return {
61
+ validate: value => (validate(value) ? [] : (validate.errors ?? []).map(error => `${error.instancePath === '' ? '(root)' : error.instancePath} ${error.message ?? 'is invalid'}`)),
62
+ };
63
+ }
64
+ catch (error) {
65
+ return { validate: () => [], loadError: `the shipped schema does not compile: ${messageOf(error)}` };
66
+ }
67
+ }
68
+ /** Validate one decoded document against BOTH authorities (RCP-02, RCP-08). */
69
+ export function judgeRecipe(document) {
70
+ const { validate, loadError } = schemaValidator();
71
+ const parsed = parseRecipe(document);
72
+ const schemaViolations = loadError !== undefined ? [loadError] : validate(document);
73
+ return {
74
+ schemaOk: loadError === undefined && schemaViolations.length === 0,
75
+ schemaViolations,
76
+ codeOk: parsed.ok,
77
+ codeViolations: parsed.ok ? [] : parsed.errors,
78
+ ...parsed.ok ? { recipe: parsed.recipe } : {},
79
+ };
80
+ }
81
+ /** Read and decode a recipe file; IO problems are the CALLER's to report as usage failures. */
82
+ function readRecipeFile(path) {
83
+ let text;
84
+ try {
85
+ text = readFileSync(path, 'utf8');
86
+ }
87
+ catch (error) {
88
+ return { ioError: `cannot read ${path}: ${messageOf(error)}` };
89
+ }
90
+ try {
91
+ return { document: JSON.parse(text) };
92
+ }
93
+ catch (error) {
94
+ return { ioError: `${path} is not valid JSON: ${messageOf(error)}` };
95
+ }
96
+ }
97
+ /** Print a verdict; returns whether it was fully clean. */
98
+ function printVerdict(io, subject, verdict) {
99
+ io.out(`schema: ${verdict.schemaOk ? 'OK' : `${verdict.schemaViolations.length} violation(s)`}`);
100
+ for (const violation of verdict.schemaViolations)
101
+ io.err(` ${subject}: ${violation}`);
102
+ io.out(`code: ${verdict.codeOk ? 'OK' : `${verdict.codeViolations.length} violation(s)`}`);
103
+ for (const violation of verdict.codeViolations)
104
+ io.err(` ${subject}: ${violation}`);
105
+ return verdict.schemaOk && verdict.codeOk;
106
+ }
107
+ const USAGE = `tool-mode-recipe — author declarative harness-model recipes (RCP-08)
108
+
109
+ validate <recipe.json> validate a draft against the published
110
+ harness-model-recipe.schema.json AND the
111
+ code validator; every refusal is named
112
+ describe <recipe.json> print the effective bounds: tool/contract,
113
+ inputs, output and image ceilings,
114
+ destinations, recording, context
115
+ create --template <image-description|text-title>
116
+ [--name <recipe-id>] [--description <text>]
117
+ [--out <dir>] emit a STATIC draft project (recipe, input/
118
+ output schemas, a starter skill note, a
119
+ starter fixtures file, a README) into a
120
+ NEW directory; nothing is installed,
121
+ activated, downloaded or inferred, and an
122
+ existing target is refused
123
+ edit-draft <recipe.json>
124
+ --set <field> --value <text> a bounded static mutation over the safe
125
+ fields (description, maxOutputTokens,
126
+ imageInputs.max); policy fields are
127
+ refused by name and the result must
128
+ validate before it is written
129
+ test <project-dir>
130
+ --fixtures <fixtures.json> run the recipe(s) through the REAL broker
131
+ against a SCRIPTED fake provider (never a
132
+ real model): binding, budget, failure
133
+ vocabulary and the provider-call ceiling
134
+ pack <recipe.json | project-dir> build a declarative-extension package
135
+ [--name <pkg>] [--version <x.y.z>]
136
+ [--description <text>] [--out <dir>]
137
+ (default --out ./dist) — manifest +
138
+ recipes JSON, NO executable code, no
139
+ lifecycle scripts run
140
+
141
+ --live is NOT provided by this build: a live run requires an explicit deployment
142
+ executor profile with consent and limits (recorded remainder, RCP-08).
143
+
144
+ Exit codes: 0 valid / packed / passed, 1 invalid recipe or fixture failure, 2 usage or IO failure.`;
145
+ /**
146
+ * Run one CLI invocation.
147
+ * @param argv - arguments after the executable name.
148
+ * @param io - the output sinks.
149
+ * @returns a promise of the process exit code.
150
+ */
151
+ export async function runRecipeCli(argv, io) {
152
+ try {
153
+ return await dispatchRecipeCli(argv, io);
154
+ }
155
+ catch (error) {
156
+ // The exit-2 contract: an UNEXPECTED failure (IO, filesystem, decode) is a
157
+ // named usage/IO refusal — never a raw stack, never exit 1.
158
+ io.err(`IO failure: ${error instanceof Error ? error.message : String(error)}`);
159
+ return 2;
160
+ }
161
+ }
162
+ async function dispatchRecipeCli(argv, io) {
163
+ // A NULL-PROTOTYPE flags map: `--__proto__ x` must not touch the object's
164
+ // prototype chain (the name is still refused by the per-command flag set).
165
+ const positional = [];
166
+ const flags = Object.create(null);
167
+ const booleanFlags = new Set(['help', 'live']);
168
+ for (let index = 0; index < argv.length; index += 1) {
169
+ const token = argv[index];
170
+ if (!token.startsWith('--')) {
171
+ positional.push(token);
172
+ continue;
173
+ }
174
+ const name = token.slice(2);
175
+ if (name === 'help') {
176
+ io.out(USAGE);
177
+ return 0;
178
+ }
179
+ if (booleanFlags.has(name)) {
180
+ // A boolean flag REFUSES a following value token: `--live <value>` must
181
+ // not silently consume the value and downgrade the live request.
182
+ const next = argv[index + 1];
183
+ if (next !== undefined && !next.startsWith('--')) {
184
+ io.err(`the flag --${name} takes no value; ${JSON.stringify(next)} is not a flag`);
185
+ io.err(USAGE);
186
+ return 2;
187
+ }
188
+ flags[name] = true;
189
+ continue;
190
+ }
191
+ const value = argv[index + 1];
192
+ if (value === undefined || value.startsWith('--')) {
193
+ flags[name] = true;
194
+ continue;
195
+ }
196
+ flags[name] = value;
197
+ index += 1;
198
+ }
199
+ const [command, ...rest] = positional;
200
+ if (command === undefined) {
201
+ io.err(USAGE);
202
+ return 2;
203
+ }
204
+ // A mistyped flag must be refused, never silently ignored (the contract the
205
+ // original parser pinned): each command names its accepted flags.
206
+ const acceptedFlags = {
207
+ validate: [], describe: [],
208
+ pack: ['out', 'name', 'version', 'description'],
209
+ create: ['template', 'name', 'description', 'out'],
210
+ 'edit-draft': ['set', 'value'],
211
+ test: ['fixtures', 'live'],
212
+ };
213
+ const allowed = acceptedFlags[command];
214
+ if (allowed === undefined) {
215
+ io.err(`unknown command ${JSON.stringify(command)}`);
216
+ io.err(USAGE);
217
+ return 2;
218
+ }
219
+ for (const name of Object.keys(flags)) {
220
+ if (!allowed.includes(name)) {
221
+ io.err(`unknown option --${name} for ${JSON.stringify(command)}`);
222
+ io.err(USAGE);
223
+ return 2;
224
+ }
225
+ }
226
+ // Flag-style commands: create / edit-draft / test.
227
+ if (command === 'create') {
228
+ return createCommand(flags, io);
229
+ }
230
+ if (command === 'edit-draft') {
231
+ return editDraftCommand(rest, flags, io);
232
+ }
233
+ if (command === 'test') {
234
+ return await testCommand(rest, flags, io);
235
+ }
236
+ // The original positional commands keep their exact contract.
237
+ if (rest.length !== 1) {
238
+ io.err(USAGE);
239
+ return 2;
240
+ }
241
+ const subject = rest[0];
242
+ if (command === 'validate' || command === 'describe') {
243
+ const { document, ioError } = readRecipeFile(subject);
244
+ if (ioError !== undefined) {
245
+ io.err(ioError);
246
+ return 2;
247
+ }
248
+ const verdict = judgeRecipe(document);
249
+ const clean = printVerdict(io, subject, verdict);
250
+ if (!clean)
251
+ return 1;
252
+ if (command === 'describe') {
253
+ describeTo(io, verdict.recipe);
254
+ }
255
+ return 0;
256
+ }
257
+ if (command === 'pack') {
258
+ return packCommand(subject, flags, io);
259
+ }
260
+ io.err(`unknown command ${JSON.stringify(command)}`);
261
+ io.err(USAGE);
262
+ return 2;
263
+ }
264
+ /**
265
+ * Refuse a target whose ancestor chain carries a reparse point (symlink or
266
+ * Windows junction) or a `tool-mode.extension.json` (an installed or packed
267
+ * artifact is immutable). `create`/`edit-draft` never write through either.
268
+ * @param target - the absolute target path (a directory for create, a file for edit-draft).
269
+ * @returns a refusal reason, or undefined when the target is writable-by-construction.
270
+ */
271
+ function writabilityRefusal(target) {
272
+ const absolute = resolve(target);
273
+ const parsed = parse(absolute);
274
+ // Walk from the FILESYSTEM ROOT, not from cwd: a target on another drive (or
275
+ // outside cwd) must be walked too — the old cwd-relative slice heuristic
276
+ // silently skipped it. Every EXISTING component is lstat'ed (a Windows
277
+ // junction reports as a symlink) and every ancestor is checked for the
278
+ // installed-artifact manifest.
279
+ const components = absolute.slice(parsed.root.length).split(sep).filter(Boolean);
280
+ let cursor = parsed.root;
281
+ // The root itself participates: a drive root that were a reparse point is as
282
+ // much an escape as any deeper component.
283
+ for (const component of components) {
284
+ cursor = join(cursor, component);
285
+ try {
286
+ if (lstatSync(cursor).isSymbolicLink()) {
287
+ return `the target path ${target} passes through a symbolic link or junction at ${cursor}; refused`;
288
+ }
289
+ }
290
+ catch {
291
+ // The component does not exist yet: no deeper component can exist either.
292
+ break;
293
+ }
294
+ }
295
+ // The installed-artifact rule applies to EVERY ancestor directory, not only
296
+ // the immediate parent (the reviewer's grandparent-manifest case): the target
297
+ // must not lie inside a directory tree that carries tool-mode.extension.json.
298
+ let ancestor = dirname(absolute);
299
+ for (;;) {
300
+ if (existsSync(join(ancestor, 'tool-mode.extension.json'))) {
301
+ return `the target path ${target} lies inside an installed or packed artifact at ${ancestor}; those are immutable and refused`;
302
+ }
303
+ const parent = dirname(ancestor);
304
+ if (parent === ancestor)
305
+ break;
306
+ ancestor = parent;
307
+ }
308
+ return undefined;
309
+ }
310
+ /**
311
+ * Verify a directory chain is still reparse-free IMMEDIATELY before a write.
312
+ * The TOCTOU residual: a junction flipped between this call and the following
313
+ * open() can still redirect that ONE open; the post-write verification below
314
+ * detects it and no FURTHER file is created (the window is a single sub-
315
+ * millisecond open per file, and the created project is refused and removed).
316
+ */
317
+ function chainStillClean(dir) {
318
+ return writabilityRefusal(join(dir, '.sentinel'));
319
+ }
320
+ /**
321
+ * Create one file EXCLUSIVELY ('wx') in a chain-verified directory, verifying
322
+ * the chain again after the write. A reparse point observed at any point
323
+ * aborts the operation (the caller refuses the whole command).
324
+ */
325
+ function safeWriteFile(target, data) {
326
+ const parent = dirname(target);
327
+ const before = chainStillClean(parent);
328
+ if (before !== undefined)
329
+ return before;
330
+ let descriptor;
331
+ try {
332
+ descriptor = openSync(target, 'wx');
333
+ writeFileSync(descriptor, data, 'utf8');
334
+ fsyncSync(descriptor);
335
+ }
336
+ finally {
337
+ if (descriptor !== undefined)
338
+ closeSync(descriptor);
339
+ }
340
+ const after = chainStillClean(parent);
341
+ if (after !== undefined)
342
+ return after;
343
+ if (lstatSync(target).isSymbolicLink()) {
344
+ return `the written file ${target} is a reparse point; refused`;
345
+ }
346
+ return undefined;
347
+ }
348
+ /** `create`: emit the static project files, refusing anything unsafe. */
349
+ function createCommand(flags, io) {
350
+ const template = typeof flags.template === 'string' ? flags.template : undefined;
351
+ const out = resolve(typeof flags.out === 'string' ? flags.out : './draft');
352
+ const name = typeof flags.name === 'string' ? flags.name : basenameOf(out);
353
+ const description = typeof flags.description === 'string' ? flags.description : undefined;
354
+ const validated = validateCreateArguments(template, name, description);
355
+ if (validated.error !== undefined) {
356
+ io.err(validated.error);
357
+ io.err(USAGE);
358
+ return 2;
359
+ }
360
+ const refused = writabilityRefusal(out);
361
+ if (refused !== undefined) {
362
+ io.err(refused);
363
+ return 2;
364
+ }
365
+ if (existsSync(out)) {
366
+ io.err(`create refused: the target ${out} already exists; create never overwrites an existing path`);
367
+ return 2;
368
+ }
369
+ const files = templateFiles(validated.template, name, validated.description);
370
+ // The draft must validate under the SHARED validator BEFORE any file lands.
371
+ const draft = files.find(file => file.path === `recipes/${name}.json`);
372
+ const draftDocument = JSON.parse(draft?.data ?? '{}');
373
+ const verdict = judgeRecipe(draftDocument);
374
+ if (!verdict.schemaOk || !verdict.codeOk) {
375
+ io.err('create refused: the generated draft does not validate — this is a template bug, not your input');
376
+ for (const violation of [...verdict.schemaViolations, ...verdict.codeViolations])
377
+ io.err(` ${violation}`);
378
+ return 1;
379
+ }
380
+ const created = [];
381
+ // Cleanup runs ONLY when the chain re-verifies clean at refusal time:
382
+ // deleting through a now-junctioned path could remove VICTIM files.
383
+ const cleanupOwned = () => {
384
+ try {
385
+ if (chainStillClean(out) !== undefined)
386
+ return;
387
+ for (const path of created.reverse())
388
+ rmSync(path, { recursive: true, force: true });
389
+ }
390
+ catch {
391
+ // Best-effort: the refusal already stands; a stuck file reports honestly
392
+ // on the next run instead of being silently deleted through a link.
393
+ }
394
+ };
395
+ try {
396
+ mkdirSync(out, { recursive: true });
397
+ created.push(out);
398
+ const postMkdir = chainStillClean(out);
399
+ if (postMkdir !== undefined) {
400
+ cleanupOwned();
401
+ io.err(`create refused after creation: ${postMkdir}`);
402
+ return 2;
403
+ }
404
+ for (const file of files) {
405
+ const target = join(out, file.path);
406
+ mkdirSync(dirname(target), { recursive: true });
407
+ const writeRefusal = safeWriteFile(target, file.data);
408
+ if (writeRefusal !== undefined) {
409
+ cleanupOwned();
410
+ io.err(`create refused mid-write: ${writeRefusal}`);
411
+ return 2;
412
+ }
413
+ created.push(target);
414
+ }
415
+ // Post-write verification: if ANY reparse point appeared anywhere in the
416
+ // chain during the run, the command refuses and removes the project it
417
+ // created — but ONLY along a chain that is still verified clean.
418
+ const finalCheck = writabilityRefusal(out);
419
+ if (finalCheck !== undefined) {
420
+ cleanupOwned();
421
+ io.err(`create refused after verification: ${finalCheck}`);
422
+ return 2;
423
+ }
424
+ }
425
+ catch (error) {
426
+ cleanupOwned();
427
+ io.err(`create failed: ${messageOf(error)}`);
428
+ return 2;
429
+ }
430
+ io.out(`created draft project ${name} at ${out} (template ${validated.template})`);
431
+ io.out(` files: ${files.map(file => file.path).join(', ')}`);
432
+ io.out(' nothing was installed, activated, downloaded or inferred (RCP-08)');
433
+ io.out(` next: tool-mode-recipe validate recipes/${name}.json, then test ., then pack .`);
434
+ return 0;
435
+ }
436
+ /** The last path component of an absolute or relative directory path. */
437
+ function basenameOf(path) {
438
+ const parts = path.replace(/[\\/]+$/, '').split(/[\\/]/);
439
+ return parts[parts.length - 1] ?? path;
440
+ }
441
+ /** `edit-draft`: a bounded static mutation that must validate before it is written. */
442
+ function editDraftCommand(positional, flags, io) {
443
+ if (positional.length !== 1) {
444
+ io.err('edit-draft needs exactly one recipe file');
445
+ io.err(USAGE);
446
+ return 2;
447
+ }
448
+ const subject = resolve(positional[0]);
449
+ const field = typeof flags.set === 'string' ? flags.set : undefined;
450
+ const value = flags.value;
451
+ if (field === undefined || typeof value !== 'string') {
452
+ io.err('edit-draft needs --set <field> --value <text>');
453
+ io.err(USAGE);
454
+ return 2;
455
+ }
456
+ if (field.startsWith('policy.')) {
457
+ io.err(policyEditRefusal(field));
458
+ return 2;
459
+ }
460
+ // The whitelist lives on a null-prototype map, so prototype-key lookups
461
+ // ('__proto__', 'constructor', 'toString', …) resolve to undefined and hit
462
+ // this refusal instead of traversing Object.prototype.
463
+ const editable = EDITABLE_FIELDS[field];
464
+ if (editable === undefined) {
465
+ io.err(`editing ${JSON.stringify(field)} is refused: only the safe fields are mutable (`
466
+ + `${Object.keys(EDITABLE_FIELDS).join(', ')}); rename the draft instead of reshaping its identity`);
467
+ return 2;
468
+ }
469
+ const valueError = editable.validate(value);
470
+ if (valueError !== undefined) {
471
+ io.err(`--set ${field} refused: ${valueError}`);
472
+ return 2;
473
+ }
474
+ const refused = writabilityRefusal(subject);
475
+ if (refused !== undefined) {
476
+ io.err(refused);
477
+ return 2;
478
+ }
479
+ const { document, ioError } = readRecipeFile(subject);
480
+ if (ioError !== undefined) {
481
+ io.err(ioError);
482
+ return 2;
483
+ }
484
+ if (typeof document !== 'object' || document === null || Array.isArray(document)) {
485
+ io.err(`${subject} is not a recipe object`);
486
+ return 2;
487
+ }
488
+ const mutated = { ...document };
489
+ // The DRAFT's intermediate shape is validated BEFORE any mutation: a draft
490
+ // whose imageInputs is a primitive refuses by name instead of crashing.
491
+ const shapeRefusal = editable.precondition(mutated);
492
+ if (shapeRefusal !== undefined) {
493
+ io.err(`--set ${field} refused: ${shapeRefusal}; ${subject} is unchanged`);
494
+ return 2;
495
+ }
496
+ editable.apply(mutated, value);
497
+ const verdict = judgeRecipe(mutated);
498
+ const clean = printVerdict(io, subject, verdict);
499
+ if (!clean) {
500
+ io.err(`edit-draft refused: the edited recipe would not validate; ${subject} is unchanged`);
501
+ return 1;
502
+ }
503
+ // Atomic write: chain-verified exclusive temp + rename, so a crash never
504
+ // leaves a truncated draft and a flipped junction cannot redirect the temp.
505
+ const temporary = `${subject}.tmp-${process.pid}-${Date.now().toString(36)}`;
506
+ const tempRefusal = safeWriteFile(temporary, `${JSON.stringify(mutated, null, 2)}\n`);
507
+ if (tempRefusal !== undefined) {
508
+ rmSync(temporary, { force: true });
509
+ io.err(`cannot write ${subject}: ${tempRefusal}`);
510
+ return 2;
511
+ }
512
+ try {
513
+ renameSync(temporary, subject);
514
+ }
515
+ catch (error) {
516
+ rmSync(temporary, { force: true });
517
+ io.err(`cannot write ${subject}: ${messageOf(error)}`);
518
+ return 2;
519
+ }
520
+ io.out(`edited ${field} of ${subject}; the result validates`);
521
+ return 0;
522
+ }
523
+ /** `test`: the REAL broker over the scripted fixture provider. */
524
+ async function testCommand(positional, flags, io) {
525
+ if (flags.live === true) {
526
+ io.err('--live is not provided by this CLI build (RCP-08 remainder): a live run requires an explicit deployment '
527
+ + 'executor profile with consent and limits. This CLI never invokes a real model; use the fixture suite instead.');
528
+ return 2;
529
+ }
530
+ if (positional.length !== 1) {
531
+ io.err('test needs exactly one project directory');
532
+ io.err(USAGE);
533
+ return 2;
534
+ }
535
+ const project = resolve(positional[0]);
536
+ const fixturesPath = resolve(typeof flags.fixtures === 'string' ? flags.fixtures : join(project, 'tests', 'fixtures.json'));
537
+ const recipesDir = join(project, 'recipes');
538
+ if (!existsSync(recipesDir)) {
539
+ io.err(`no recipes directory at ${recipesDir}; create a project first (tool-mode-recipe create)`);
540
+ return 2;
541
+ }
542
+ const recipePaths = readdirSync(recipesDir).filter(entry => entry.endsWith('.json')).sort()
543
+ .map(entry => join(recipesDir, entry));
544
+ if (recipePaths.length === 0) {
545
+ io.err(`no recipe JSON files in ${recipesDir}`);
546
+ return 2;
547
+ }
548
+ if (!existsSync(fixturesPath)) {
549
+ io.err(`no fixtures file at ${fixturesPath}; pass --fixtures or create a project first`);
550
+ return 2;
551
+ }
552
+ let fixture;
553
+ try {
554
+ const parsed = JSON.parse(readFileSync(fixturesPath, 'utf8'));
555
+ const validated = validateFixtureDocument(parsed);
556
+ if (validated.error !== undefined || validated.document === undefined) {
557
+ io.err(`the fixtures file is not usable: ${validated.error ?? 'unknown shape'}`);
558
+ return 2;
559
+ }
560
+ fixture = validated.document;
561
+ }
562
+ catch (error) {
563
+ io.err(`the fixtures file is not valid JSON: ${messageOf(error)}`);
564
+ return 2;
565
+ }
566
+ let allPassed = true;
567
+ const failedCases = [];
568
+ for (const path of recipePaths) {
569
+ const { document, ioError } = readRecipeFile(path);
570
+ if (ioError !== undefined) {
571
+ io.err(ioError);
572
+ return 2;
573
+ }
574
+ const verdict = judgeRecipe(document);
575
+ if (!verdict.schemaOk || !verdict.codeOk) {
576
+ io.err(`${path} does not validate; the fixture suite runs only validated recipes`);
577
+ allPassed = false;
578
+ continue;
579
+ }
580
+ io.out(`testing ${path} against ${fixturesPath}`);
581
+ const verdicts = await runFixtureSuite(verdict.recipe, fixture, io, INITIAL_HOST_POLICY);
582
+ for (const entry of verdicts) {
583
+ if (!entry.pass) {
584
+ allPassed = false;
585
+ failedCases.push(entry.caseId);
586
+ }
587
+ }
588
+ }
589
+ if (!allPassed) {
590
+ io.err(`test failed: fixture case(s) ${failedCases.join(', ')} did not match their expected outcomes`);
591
+ return 1;
592
+ }
593
+ io.out('test passed: every fixture case matched its expected outcome against the scripted provider (not a real model)');
594
+ return 0;
595
+ }
596
+ /**
597
+ * Print the effective bounds for a recipe (RCP-08's describe).
598
+ *
599
+ * The recipe's own ceilings are RES-10's initial maxima by construction (the
600
+ * validator refuses anything above them); the host may narrow further at
601
+ * runtime, which the output says rather than hides.
602
+ */
603
+ function describeTo(io, recipe) {
604
+ const names = Object.keys(recipe.inputs);
605
+ const required = names.filter(name => recipe.inputs[name]?.required === true);
606
+ const images = names.filter(name => recipe.inputs[name]?.type === 'image');
607
+ io.out(`${recipe.tool.name} (contract ${recipe.tool.contractId}) — recipe ${recipe.id}@${recipe.version}`);
608
+ io.out(` description: ${recipe.description}`);
609
+ io.out(` inputs: ${names.length} (${required.length} required: ${required.join(', ') || '(none)'}`
610
+ + `${images.length > 0 ? `; ${images.length} image: ${images.join(', ')}` : ''})`);
611
+ io.out(` output ceiling: at most ${recipe.maxOutputTokens} tokens `
612
+ + `(RES-10 initial maximum ${INITIAL_MAX_OUTPUT_TOKENS}; the host policy may narrow this)`);
613
+ io.out(` image ceiling: at most ${recipe.imageInputs.max} image input(s) `
614
+ + `(RES-10 initial maximum ${INITIAL_MAX_IMAGE_INPUTS})`);
615
+ io.out(` recording: ${recipe.policy.record ? 'requested' : 'not requested'} — a host policy that `
616
+ + `requires recording will refuse a ${recipe.policy.record ? 'recorded' : 'record: false'} recipe rather than silently drop it`);
617
+ io.out(` destinations: ${recipe.policy.allowRemote ? 'local or remote, by the host policy' : 'local only, by the recipe'}`);
618
+ io.out(' context: none supplied — a recipe is data; no conversation, tools, attachments or memory are inherited (HLM-05)');
619
+ if (recipe.modelSelection !== undefined) {
620
+ io.out(` executor: approved-profile "${recipe.modelSelection.profile}" (RCP-05) — the operator binds the profile; `
621
+ + 'an unresolved binding keeps the tool unavailable with EXECUTOR_NOT_CONFIGURED');
622
+ }
623
+ else {
624
+ io.out(' executor: caller-model (RCP-05) — the invoking agent\'s authorized harness model, resolved once per invocation');
625
+ }
626
+ if (recipe.response !== undefined) {
627
+ io.out(` response: ${recipe.response.mode} into payload field "${recipe.response.field}" (RCP-04)`);
628
+ if (recipe.response.mode === 'json') {
629
+ io.out(' disclosure: json mode is prompt-enforced and locally validated in this build — the provider\'s '
630
+ + 'verified structured-output feature is NOT negotiated (RCP-04\'s disclosure requirement)');
631
+ }
632
+ }
633
+ else {
634
+ io.out(' response: plain text payload (RCP-04)');
635
+ }
636
+ }
637
+ /** The declarative-manifest template, mirroring the shipped declarative package. */
638
+ function manifestFor(name, version, description, recipes) {
639
+ return {
640
+ protocolVersion: '1.1.0',
641
+ id: name,
642
+ version,
643
+ displayName: name,
644
+ description,
645
+ license: 'MIT',
646
+ activation: { policy: 'operator', deactivatable: true },
647
+ runtimeApi: 1,
648
+ compatibility: {
649
+ hostApi: '^1.1.0',
650
+ requiredFeatures: ['declarative-recipes-v1'],
651
+ },
652
+ execution: { kind: 'declarative' },
653
+ permissions: {
654
+ networkOrigins: [],
655
+ inputFiles: false,
656
+ storage: 'none',
657
+ secrets: [],
658
+ nativeCode: false,
659
+ },
660
+ models: [],
661
+ tools: [],
662
+ implementations: recipes.map(recipe => ({
663
+ id: recipe.id,
664
+ contractId: recipe.tool.contractId,
665
+ contractVersion: '1.0.0',
666
+ recipeVersion: recipe.version,
667
+ })),
668
+ skills: [],
669
+ licenses: [
670
+ {
671
+ layer: 'code',
672
+ name: 'MIT',
673
+ url: 'https://opensource.org/license/mit',
674
+ openSource: true,
675
+ redistribution: 'permitted',
676
+ attributionRequired: false,
677
+ obligations: [],
678
+ },
679
+ {
680
+ layer: 'weights',
681
+ name: 'none: the package carries no weights',
682
+ url: 'https://example.invalid/none',
683
+ openSource: false,
684
+ redistribution: 'not-permitted',
685
+ attributionRequired: false,
686
+ obligations: ['the recipes reference a harness model supplied by the deployment, so no weights are distributed here'],
687
+ },
688
+ {
689
+ layer: 'engine',
690
+ name: 'none: the host broker is the engine',
691
+ url: 'https://example.invalid/none',
692
+ openSource: false,
693
+ redistribution: 'not-permitted',
694
+ attributionRequired: false,
695
+ obligations: ['execution happens in the host broker, which is the point of a declarative package'],
696
+ },
697
+ {
698
+ layer: 'sdk',
699
+ name: 'none: a declarative package links no SDK at runtime',
700
+ url: 'https://example.invalid/none',
701
+ openSource: false,
702
+ redistribution: 'not-permitted',
703
+ attributionRequired: false,
704
+ obligations: ['the host needs no runtime link for a package that ships no module'],
705
+ },
706
+ {
707
+ layer: 'conversion-artifacts',
708
+ name: 'none: no conversion is performed',
709
+ url: 'https://example.invalid/none',
710
+ openSource: false,
711
+ redistribution: 'permitted',
712
+ attributionRequired: false,
713
+ obligations: [],
714
+ },
715
+ ],
716
+ network: { requiredTelemetry: false, otherNetworkUse: false },
717
+ };
718
+ }
719
+ /** Run `pack`, emitting a package directory and a store-method zip. */
720
+ function packCommand(subject, options, io) {
721
+ const out = resolve(options.out ?? './dist');
722
+ let name = options.name;
723
+ let version = options.version;
724
+ const recipePaths = [];
725
+ const isFile = existsSync(subject) && statSync(subject).isFile();
726
+ if (isFile) {
727
+ recipePaths.push(resolve(subject));
728
+ }
729
+ else {
730
+ const dir = resolve(subject);
731
+ if (!existsSync(dir)) {
732
+ io.err(`no recipe or project directory at ${subject}`);
733
+ return 2;
734
+ }
735
+ const packagePath = join(dir, 'package.json');
736
+ if (existsSync(packagePath)) {
737
+ let manifest = {};
738
+ try {
739
+ manifest = JSON.parse(readFileSync(packagePath, 'utf8'));
740
+ }
741
+ catch (error) {
742
+ io.err(`the project package.json is not valid JSON: ${messageOf(error)}`);
743
+ return 2;
744
+ }
745
+ if (name === undefined && typeof manifest.name === 'string')
746
+ name = manifest.name;
747
+ if (version === undefined && typeof manifest.version === 'string')
748
+ version = manifest.version;
749
+ }
750
+ const recipesDir = join(dir, 'recipes');
751
+ const source = existsSync(recipesDir) ? recipesDir : dir;
752
+ for (const entry of readdirSync(source).sort()) {
753
+ if (!entry.endsWith('.json'))
754
+ continue;
755
+ if (source === dir && (entry === 'package.json' || entry === 'tool-mode.extension.json'))
756
+ continue;
757
+ recipePaths.push(join(source, entry));
758
+ }
759
+ }
760
+ if (name === undefined || !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(name)) {
761
+ io.err('pack needs a package name: pass --name <pkg> or pack a project directory with a package.json');
762
+ return 2;
763
+ }
764
+ if (recipePaths.length === 0) {
765
+ io.err('no recipe JSON files found to pack');
766
+ return 2;
767
+ }
768
+ const recipes = [];
769
+ let clean = true;
770
+ for (const path of recipePaths) {
771
+ const { document, ioError } = readRecipeFile(path);
772
+ if (ioError !== undefined) {
773
+ io.err(ioError);
774
+ clean = false;
775
+ continue;
776
+ }
777
+ const verdict = judgeRecipe(document);
778
+ const fileClean = printVerdict(io, path, verdict);
779
+ if (!fileClean) {
780
+ clean = false;
781
+ continue;
782
+ }
783
+ // Two recipes claiming one tool name would publish two implementations of
784
+ // one contract under one manifest; the packer refuses rather than guessing.
785
+ const recipe = verdict.recipe;
786
+ const clash = recipes.find(existing => existing.tool.name === recipe.tool.name);
787
+ if (clash !== undefined) {
788
+ io.err(`${path} claims the tool "${recipe.tool.name}" already claimed by recipe ${clash.id}; `
789
+ + 'one package serves one implementation per tool');
790
+ clean = false;
791
+ continue;
792
+ }
793
+ recipes.push(recipe);
794
+ }
795
+ if (!clean) {
796
+ io.err('pack refused: the production artifact is only packed from recipes that validate');
797
+ return 1;
798
+ }
799
+ const packageVersion = version ?? (recipes.length === 1 ? recipes[0]?.version : undefined) ?? '0.1.0';
800
+ const description = options.description
801
+ ?? `Declarative recipe package ${name}: ${recipes.map(recipe => recipe.tool.name).join(', ')}.`;
802
+ const files = [];
803
+ files.push({
804
+ path: 'package.json',
805
+ data: `${JSON.stringify({
806
+ name,
807
+ version: packageVersion,
808
+ type: 'module',
809
+ license: 'MIT',
810
+ description,
811
+ files: ['tool-mode.extension.json', 'recipes'],
812
+ toolMode: { manifest: './tool-mode.extension.json' },
813
+ }, null, 2)}\n`,
814
+ });
815
+ files.push({
816
+ path: 'tool-mode.extension.json',
817
+ data: `${JSON.stringify(manifestFor(name, packageVersion, description, recipes), null, 2)}\n`,
818
+ });
819
+ for (const recipe of recipes) {
820
+ // Pack emits the CANONICAL RCP-02 shape: the packaged artifact is the
821
+ // vocabulary the spec publishes, and the lossless binding constraints keep
822
+ // the canonical round trip identical to the frozen recipe.
823
+ files.push({ path: `recipes/${recipe.id}.json`, data: `${JSON.stringify(toCanonicalRecipe(recipe), null, 2)}\n` });
824
+ }
825
+ const packageDir = join(out, `${name.replaceAll('/', '__')}-${packageVersion}`);
826
+ mkdirSync(packageDir, { recursive: true });
827
+ for (const file of files) {
828
+ const target = join(packageDir, file.path);
829
+ mkdirSync(dirname(target), { recursive: true });
830
+ writeFileSync(target, file.data, 'utf8');
831
+ }
832
+ const zipPath = `${packageDir}.zip`;
833
+ writeFileSync(zipPath, writeZip(files.map(file => ({
834
+ path: file.path,
835
+ data: Buffer.from(file.data, 'utf8'),
836
+ }))));
837
+ io.out(`packed ${name}@${packageVersion}`);
838
+ io.out(` package: ${packageDir}`);
839
+ io.out(` archive: ${zipPath} (store-method zip, consumable by the extension install path)`);
840
+ io.out(` contents: ${files.map(file => file.path).join(', ')}`);
841
+ io.out(' NO executable code ships, and no lifecycle scripts were run (RCP-08)');
842
+ return 0;
843
+ }
844
+ /** Best-effort message from an arbitrary thrown value. */
845
+ function messageOf(error) {
846
+ try {
847
+ return error instanceof Error ? error.message : String(error);
848
+ }
849
+ catch {
850
+ return '<unrenderable>';
851
+ }
852
+ }
853
+ // ---------------------------------------------------------------------------
854
+ // A minimal, dependency-free ZIP writer (STORE method, deterministic order).
855
+ // ---------------------------------------------------------------------------
856
+ /** The CRC-32 table and update, for the store-method entries. */
857
+ const CRC_TABLE = (() => {
858
+ const table = new Uint32Array(256);
859
+ for (let n = 0; n < 256; n += 1) {
860
+ let c = n;
861
+ for (let k = 0; k < 8; k += 1)
862
+ c = (c & 1) !== 0 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
863
+ table[n] = c >>> 0;
864
+ }
865
+ return table;
866
+ })();
867
+ function crc32(bytes) {
868
+ let crc = 0xffffffff;
869
+ for (const byte of bytes)
870
+ crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
871
+ return (crc ^ 0xffffffff) >>> 0;
872
+ }
873
+ /** Write a STORE-method zip over the given files, in the given order. */
874
+ function writeZip(files) {
875
+ const chunks = [];
876
+ const central = [];
877
+ let offset = 0;
878
+ const put = (buffer) => {
879
+ chunks.push(buffer);
880
+ offset += buffer.byteLength;
881
+ };
882
+ for (const file of files) {
883
+ const name = Buffer.from(file.path, 'utf8');
884
+ const crc = crc32(new Uint8Array(file.data));
885
+ const header = Buffer.alloc(30);
886
+ header.writeUInt32LE(0x04034b50, 0);
887
+ header.writeUInt16LE(20, 4); // version needed
888
+ header.writeUInt16LE(0, 6); // flags
889
+ header.writeUInt16LE(0, 8); // STORE
890
+ header.writeUInt16LE(0, 10); // time
891
+ header.writeUInt16LE(0x21, 12); // date (1980-01-01, deterministic)
892
+ header.writeUInt32LE(crc, 14);
893
+ header.writeUInt32LE(file.data.byteLength, 18);
894
+ header.writeUInt32LE(file.data.byteLength, 22);
895
+ header.writeUInt16LE(name.byteLength, 26);
896
+ header.writeUInt16LE(0, 28); // extra length
897
+ put(header);
898
+ put(name);
899
+ put(file.data);
900
+ const entry = Buffer.alloc(46);
901
+ entry.writeUInt32LE(0x02014b50, 0);
902
+ entry.writeUInt16LE(20, 4); // version made by
903
+ entry.writeUInt16LE(20, 6); // version needed
904
+ entry.writeUInt16LE(0, 8);
905
+ entry.writeUInt16LE(0, 10);
906
+ entry.writeUInt16LE(0, 12);
907
+ entry.writeUInt16LE(0x21, 14);
908
+ entry.writeUInt32LE(crc, 16);
909
+ entry.writeUInt32LE(file.data.byteLength, 20);
910
+ entry.writeUInt32LE(file.data.byteLength, 24);
911
+ entry.writeUInt16LE(name.byteLength, 28);
912
+ entry.writeUInt16LE(0, 30);
913
+ entry.writeUInt16LE(0, 32);
914
+ entry.writeUInt16LE(0, 34);
915
+ entry.writeUInt16LE(0, 36); // external attrs
916
+ entry.writeUInt32LE(offset - header.byteLength - name.byteLength - file.data.byteLength, 42); // local header offset
917
+ central.push(entry);
918
+ central.push(name);
919
+ }
920
+ const centralSize = central.reduce((sum, buffer) => sum + buffer.byteLength, 0);
921
+ const centralStart = offset;
922
+ for (const buffer of central)
923
+ put(buffer);
924
+ const end = Buffer.alloc(22);
925
+ end.writeUInt32LE(0x06054b50, 0);
926
+ end.writeUInt16LE(0, 4);
927
+ end.writeUInt16LE(0, 6);
928
+ end.writeUInt16LE(files.length, 8);
929
+ end.writeUInt16LE(files.length, 10);
930
+ end.writeUInt32LE(centralSize, 12);
931
+ end.writeUInt32LE(centralStart, 16);
932
+ end.writeUInt16LE(0, 20);
933
+ put(end);
934
+ return Buffer.concat(chunks);
935
+ }
936
+ //# sourceMappingURL=cli.js.map