@try-works/dsh-tool-mode 0.0.0-stage → 0.5.44

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 (425) hide show
  1. package/README.md +299 -2
  2. package/bin/dsh-tool-mode-credentials.mjs +20 -0
  3. package/bin/dsh-tool-mode-mcp-stdio.mjs +80 -0
  4. package/bin/dsh-tool-mode.mjs +211 -0
  5. package/builtins.lock.json +26 -0
  6. package/catalog/desert-ant-models.json +693 -0
  7. package/catalog/playbooks.json +531 -0
  8. package/client/client.model-tools.js +1910 -0
  9. package/client/index.js +4039 -0
  10. package/cordis.patch.yml +39 -0
  11. package/lib/adapters/index.d.ts +61 -0
  12. package/lib/adapters/index.d.ts.map +1 -0
  13. package/lib/adapters/index.js +82 -0
  14. package/lib/adapters/types.d.ts +106 -0
  15. package/lib/adapters/types.d.ts.map +1 -0
  16. package/lib/adapters/types.js +85 -0
  17. package/lib/approval.d.ts +189 -0
  18. package/lib/approval.d.ts.map +1 -0
  19. package/lib/approval.js +510 -0
  20. package/lib/asset-acquisition.d.ts +149 -0
  21. package/lib/asset-acquisition.d.ts.map +1 -0
  22. package/lib/asset-acquisition.js +175 -0
  23. package/lib/authority.d.ts +185 -0
  24. package/lib/authority.d.ts.map +1 -0
  25. package/lib/authority.js +223 -0
  26. package/lib/bindings.d.ts +144 -0
  27. package/lib/bindings.d.ts.map +1 -0
  28. package/lib/bindings.js +314 -0
  29. package/lib/builtins.d.ts +27 -0
  30. package/lib/builtins.d.ts.map +1 -0
  31. package/lib/builtins.js +37 -0
  32. package/lib/capability.d.ts +58 -0
  33. package/lib/capability.d.ts.map +1 -0
  34. package/lib/capability.js +192 -0
  35. package/lib/capture-path.d.ts +97 -0
  36. package/lib/capture-path.d.ts.map +1 -0
  37. package/lib/capture-path.js +152 -0
  38. package/lib/catalog.d.ts +37 -0
  39. package/lib/catalog.d.ts.map +1 -0
  40. package/lib/catalog.js +274 -0
  41. package/lib/contract-payload.d.ts +39 -0
  42. package/lib/contract-payload.d.ts.map +1 -0
  43. package/lib/contract-payload.js +56 -0
  44. package/lib/custom-tools/compile.d.ts +25 -0
  45. package/lib/custom-tools/compile.d.ts.map +1 -0
  46. package/lib/custom-tools/compile.js +61 -0
  47. package/lib/custom-tools/composite-executor.d.ts +42 -0
  48. package/lib/custom-tools/composite-executor.d.ts.map +1 -0
  49. package/lib/custom-tools/composite-executor.js +113 -0
  50. package/lib/custom-tools/dispatch.d.ts +28 -0
  51. package/lib/custom-tools/dispatch.d.ts.map +1 -0
  52. package/lib/custom-tools/dispatch.js +34 -0
  53. package/lib/custom-tools/http-executor.d.ts +48 -0
  54. package/lib/custom-tools/http-executor.d.ts.map +1 -0
  55. package/lib/custom-tools/http-executor.js +241 -0
  56. package/lib/custom-tools/index.d.ts +12 -0
  57. package/lib/custom-tools/index.d.ts.map +1 -0
  58. package/lib/custom-tools/index.js +12 -0
  59. package/lib/custom-tools/mcp-surface.d.ts +14 -0
  60. package/lib/custom-tools/mcp-surface.d.ts.map +1 -0
  61. package/lib/custom-tools/mcp-surface.js +41 -0
  62. package/lib/custom-tools/prompt-executor.d.ts +39 -0
  63. package/lib/custom-tools/prompt-executor.d.ts.map +1 -0
  64. package/lib/custom-tools/prompt-executor.js +59 -0
  65. package/lib/custom-tools/registry.d.ts +69 -0
  66. package/lib/custom-tools/registry.d.ts.map +1 -0
  67. package/lib/custom-tools/registry.js +130 -0
  68. package/lib/custom-tools/schema.d.ts +26 -0
  69. package/lib/custom-tools/schema.d.ts.map +1 -0
  70. package/lib/custom-tools/schema.js +375 -0
  71. package/lib/custom-tools/service.d.ts +90 -0
  72. package/lib/custom-tools/service.d.ts.map +1 -0
  73. package/lib/custom-tools/service.js +203 -0
  74. package/lib/custom-tools/store.d.ts +61 -0
  75. package/lib/custom-tools/store.d.ts.map +1 -0
  76. package/lib/custom-tools/store.js +240 -0
  77. package/lib/custom-tools/types.d.ts +131 -0
  78. package/lib/custom-tools/types.d.ts.map +1 -0
  79. package/lib/custom-tools/types.js +11 -0
  80. package/lib/decision-combine.d.ts +113 -0
  81. package/lib/decision-combine.d.ts.map +1 -0
  82. package/lib/decision-combine.js +156 -0
  83. package/lib/decision-evaluate.d.ts +185 -0
  84. package/lib/decision-evaluate.d.ts.map +1 -0
  85. package/lib/decision-evaluate.js +227 -0
  86. package/lib/deployment-coordinator.d.ts +213 -0
  87. package/lib/deployment-coordinator.d.ts.map +1 -0
  88. package/lib/deployment-coordinator.js +274 -0
  89. package/lib/diagnostics.d.ts +169 -0
  90. package/lib/diagnostics.d.ts.map +1 -0
  91. package/lib/diagnostics.js +136 -0
  92. package/lib/discovery-contracts.d.ts +14 -0
  93. package/lib/discovery-contracts.d.ts.map +1 -0
  94. package/lib/discovery-contracts.js +90 -0
  95. package/lib/discovery-describe.d.ts +35 -0
  96. package/lib/discovery-describe.d.ts.map +1 -0
  97. package/lib/discovery-describe.js +99 -0
  98. package/lib/embedding-index.d.ts +55 -0
  99. package/lib/embedding-index.d.ts.map +1 -0
  100. package/lib/embedding-index.js +126 -0
  101. package/lib/extension-dependencies.d.ts +355 -0
  102. package/lib/extension-dependencies.d.ts.map +1 -0
  103. package/lib/extension-dependencies.js +987 -0
  104. package/lib/extension-dispatch.d.ts +150 -0
  105. package/lib/extension-dispatch.d.ts.map +1 -0
  106. package/lib/extension-dispatch.js +178 -0
  107. package/lib/extension-page-state.d.ts +69 -0
  108. package/lib/extension-page-state.d.ts.map +1 -0
  109. package/lib/extension-page-state.js +82 -0
  110. package/lib/extension-registrar.d.ts +116 -0
  111. package/lib/extension-registrar.d.ts.map +1 -0
  112. package/lib/extension-registrar.js +144 -0
  113. package/lib/extensions-api.d.ts +346 -0
  114. package/lib/extensions-api.d.ts.map +1 -0
  115. package/lib/extensions-api.js +17 -0
  116. package/lib/extensions-remote.d.ts +57 -0
  117. package/lib/extensions-remote.d.ts.map +1 -0
  118. package/lib/extensions-remote.js +69 -0
  119. package/lib/extensions-service.d.ts +568 -0
  120. package/lib/extensions-service.d.ts.map +1 -0
  121. package/lib/extensions-service.js +2507 -0
  122. package/lib/harness-budgets.d.ts +158 -0
  123. package/lib/harness-budgets.d.ts.map +1 -0
  124. package/lib/harness-budgets.js +152 -0
  125. package/lib/harness-llm.d.ts +138 -0
  126. package/lib/harness-llm.d.ts.map +1 -0
  127. package/lib/harness-llm.js +218 -0
  128. package/lib/harness-serving.d.ts +115 -0
  129. package/lib/harness-serving.d.ts.map +1 -0
  130. package/lib/harness-serving.js +176 -0
  131. package/lib/headless.d.ts +97 -0
  132. package/lib/headless.d.ts.map +1 -0
  133. package/lib/headless.js +407 -0
  134. package/lib/http-acquisition.d.ts +27 -0
  135. package/lib/http-acquisition.d.ts.map +1 -0
  136. package/lib/http-acquisition.js +140 -0
  137. package/lib/index.d.ts +546 -0
  138. package/lib/index.d.ts.map +1 -0
  139. package/lib/index.js +5197 -0
  140. package/lib/invocation-accounting.d.ts +169 -0
  141. package/lib/invocation-accounting.d.ts.map +1 -0
  142. package/lib/invocation-accounting.js +148 -0
  143. package/lib/jev.d.ts +218 -0
  144. package/lib/jev.d.ts.map +1 -0
  145. package/lib/jev.js +366 -0
  146. package/lib/legacy-migration.d.ts +95 -0
  147. package/lib/legacy-migration.d.ts.map +1 -0
  148. package/lib/legacy-migration.js +429 -0
  149. package/lib/license-basis.d.ts +29 -0
  150. package/lib/license-basis.d.ts.map +1 -0
  151. package/lib/license-basis.js +58 -0
  152. package/lib/managed-workers.d.ts +158 -0
  153. package/lib/managed-workers.d.ts.map +1 -0
  154. package/lib/managed-workers.js +269 -0
  155. package/lib/mcp/auth-selection.d.ts +35 -0
  156. package/lib/mcp/auth-selection.d.ts.map +1 -0
  157. package/lib/mcp/auth-selection.js +64 -0
  158. package/lib/mcp/client-grant.d.ts +89 -0
  159. package/lib/mcp/client-grant.d.ts.map +1 -0
  160. package/lib/mcp/client-grant.js +63 -0
  161. package/lib/mcp/credential-cli.d.ts +38 -0
  162. package/lib/mcp/credential-cli.d.ts.map +1 -0
  163. package/lib/mcp/credential-cli.js +148 -0
  164. package/lib/mcp/credential-store.d.ts +143 -0
  165. package/lib/mcp/credential-store.d.ts.map +1 -0
  166. package/lib/mcp/credential-store.js +461 -0
  167. package/lib/mcp/export-policy.d.ts +25 -0
  168. package/lib/mcp/export-policy.d.ts.map +1 -0
  169. package/lib/mcp/export-policy.js +31 -0
  170. package/lib/mcp/host-binding.d.ts +144 -0
  171. package/lib/mcp/host-binding.d.ts.map +1 -0
  172. package/lib/mcp/host-binding.js +280 -0
  173. package/lib/mcp/http.d.ts +180 -0
  174. package/lib/mcp/http.d.ts.map +1 -0
  175. package/lib/mcp/http.js +602 -0
  176. package/lib/mcp/oauth.d.ts +149 -0
  177. package/lib/mcp/oauth.d.ts.map +1 -0
  178. package/lib/mcp/oauth.js +396 -0
  179. package/lib/mcp/protocol.d.ts +329 -0
  180. package/lib/mcp/protocol.d.ts.map +1 -0
  181. package/lib/mcp/protocol.js +459 -0
  182. package/lib/mcp/rate-limit.d.ts +68 -0
  183. package/lib/mcp/rate-limit.d.ts.map +1 -0
  184. package/lib/mcp/rate-limit.js +114 -0
  185. package/lib/mcp/sdk-v2.d.ts +17 -0
  186. package/lib/mcp/sdk-v2.d.ts.map +1 -0
  187. package/lib/mcp/sdk-v2.js +204 -0
  188. package/lib/mcp/server.d.ts +149 -0
  189. package/lib/mcp/server.d.ts.map +1 -0
  190. package/lib/mcp/server.js +302 -0
  191. package/lib/mcp/stdio.d.ts +80 -0
  192. package/lib/mcp/stdio.d.ts.map +1 -0
  193. package/lib/mcp/stdio.js +184 -0
  194. package/lib/mcp/subscriptions.d.ts +177 -0
  195. package/lib/mcp/subscriptions.d.ts.map +1 -0
  196. package/lib/mcp/subscriptions.js +288 -0
  197. package/lib/mcp-status.d.ts +96 -0
  198. package/lib/mcp-status.d.ts.map +1 -0
  199. package/lib/mcp-status.js +70 -0
  200. package/lib/mcp-usage.d.ts +110 -0
  201. package/lib/mcp-usage.d.ts.map +1 -0
  202. package/lib/mcp-usage.js +189 -0
  203. package/lib/model-qualification.d.ts +192 -0
  204. package/lib/model-qualification.d.ts.map +1 -0
  205. package/lib/model-qualification.js +178 -0
  206. package/lib/planning.d.ts +256 -0
  207. package/lib/planning.d.ts.map +1 -0
  208. package/lib/planning.js +471 -0
  209. package/lib/playbook.d.ts +42 -0
  210. package/lib/playbook.d.ts.map +1 -0
  211. package/lib/playbook.js +65 -0
  212. package/lib/prepared-assets.d.ts +269 -0
  213. package/lib/prepared-assets.d.ts.map +1 -0
  214. package/lib/prepared-assets.js +471 -0
  215. package/lib/provider-bridge.d.ts +107 -0
  216. package/lib/provider-bridge.d.ts.map +1 -0
  217. package/lib/provider-bridge.js +423 -0
  218. package/lib/ptc-authority.d.ts +154 -0
  219. package/lib/ptc-authority.d.ts.map +1 -0
  220. package/lib/ptc-authority.js +242 -0
  221. package/lib/ptc-confinement.d.ts +60 -0
  222. package/lib/ptc-confinement.d.ts.map +1 -0
  223. package/lib/ptc-confinement.js +212 -0
  224. package/lib/ptc-runtime.d.ts +166 -0
  225. package/lib/ptc-runtime.d.ts.map +1 -0
  226. package/lib/ptc-runtime.js +231 -0
  227. package/lib/publication.d.ts +161 -0
  228. package/lib/publication.d.ts.map +1 -0
  229. package/lib/publication.js +181 -0
  230. package/lib/receipt-store.d.ts +57 -0
  231. package/lib/receipt-store.d.ts.map +1 -0
  232. package/lib/receipt-store.js +143 -0
  233. package/lib/receipt.d.ts +231 -0
  234. package/lib/receipt.d.ts.map +1 -0
  235. package/lib/receipt.js +165 -0
  236. package/lib/registrar.d.ts +75 -0
  237. package/lib/registrar.d.ts.map +1 -0
  238. package/lib/registrar.js +152 -0
  239. package/lib/resource-governance.d.ts +204 -0
  240. package/lib/resource-governance.d.ts.map +1 -0
  241. package/lib/resource-governance.js +213 -0
  242. package/lib/server-lifecycle.d.ts +175 -0
  243. package/lib/server-lifecycle.d.ts.map +1 -0
  244. package/lib/server-lifecycle.js +269 -0
  245. package/lib/skills.d.ts +31 -0
  246. package/lib/skills.d.ts.map +1 -0
  247. package/lib/skills.js +170 -0
  248. package/lib/speech-bridge.d.ts +74 -0
  249. package/lib/speech-bridge.d.ts.map +1 -0
  250. package/lib/speech-bridge.js +115 -0
  251. package/lib/state.d.ts +100 -0
  252. package/lib/state.d.ts.map +1 -0
  253. package/lib/state.js +160 -0
  254. package/lib/title-proposal.d.ts +194 -0
  255. package/lib/title-proposal.d.ts.map +1 -0
  256. package/lib/title-proposal.js +244 -0
  257. package/lib/tool-inventory.d.ts +90 -0
  258. package/lib/tool-inventory.d.ts.map +1 -0
  259. package/lib/tool-inventory.js +102 -0
  260. package/lib/tool-mode-run.d.ts +310 -0
  261. package/lib/tool-mode-run.d.ts.map +1 -0
  262. package/lib/tool-mode-run.js +411 -0
  263. package/lib/tool-sources.d.ts +80 -0
  264. package/lib/tool-sources.d.ts.map +1 -0
  265. package/lib/tool-sources.js +123 -0
  266. package/lib/tool-usage-projection.d.ts +390 -0
  267. package/lib/tool-usage-projection.d.ts.map +1 -0
  268. package/lib/tool-usage-projection.js +1038 -0
  269. package/lib/tool-usage-report.d.ts +153 -0
  270. package/lib/tool-usage-report.d.ts.map +1 -0
  271. package/lib/tool-usage-report.js +274 -0
  272. package/lib/transport.d.ts +158 -0
  273. package/lib/transport.d.ts.map +1 -0
  274. package/lib/transport.js +332 -0
  275. package/lib/types.d.ts +134 -0
  276. package/lib/types.d.ts.map +1 -0
  277. package/lib/types.js +8 -0
  278. package/lib/update-surface.d.ts +79 -0
  279. package/lib/update-surface.d.ts.map +1 -0
  280. package/lib/update-surface.js +224 -0
  281. package/lib/version.d.ts +3 -0
  282. package/lib/version.d.ts.map +1 -0
  283. package/lib/version.js +27 -0
  284. package/node_modules/@try-works/tool-mode-contracts/lib/contract-metadata.d.ts +13 -0
  285. package/node_modules/@try-works/tool-mode-contracts/lib/contract-metadata.d.ts.map +1 -0
  286. package/node_modules/@try-works/tool-mode-contracts/lib/contract-metadata.js +85 -0
  287. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts +16 -0
  288. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts.map +1 -0
  289. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.js +87 -0
  290. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts +38 -0
  291. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts.map +1 -0
  292. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.js +445 -0
  293. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts +20 -0
  294. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts.map +1 -0
  295. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.js +112 -0
  296. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts +14 -0
  297. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts.map +1 -0
  298. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.js +63 -0
  299. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts +14 -0
  300. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts.map +1 -0
  301. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.js +76 -0
  302. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts +82 -0
  303. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts.map +1 -0
  304. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.js +274 -0
  305. package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts +26 -0
  306. package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts.map +1 -0
  307. package/node_modules/@try-works/tool-mode-contracts/lib/index.js +53 -0
  308. package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts +133 -0
  309. package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts.map +1 -0
  310. package/node_modules/@try-works/tool-mode-contracts/lib/meta.js +64 -0
  311. package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +121 -0
  312. package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts.map +1 -0
  313. package/node_modules/@try-works/tool-mode-contracts/lib/registry.js +246 -0
  314. package/node_modules/@try-works/tool-mode-contracts/package.json +35 -0
  315. package/node_modules/@try-works/tool-mode-extension-sdk/bin/dsh-tool-mode.mjs +20 -0
  316. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts +80 -0
  317. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts.map +1 -0
  318. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.js +533 -0
  319. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts +86 -0
  320. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts.map +1 -0
  321. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.js +145 -0
  322. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts +70 -0
  323. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts.map +1 -0
  324. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.js +201 -0
  325. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts +57 -0
  326. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts.map +1 -0
  327. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.js +83 -0
  328. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts +33 -0
  329. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts.map +1 -0
  330. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.js +33 -0
  331. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts +86 -0
  332. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts.map +1 -0
  333. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.js +191 -0
  334. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts +137 -0
  335. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts.map +1 -0
  336. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.js +349 -0
  337. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts +274 -0
  338. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts.map +1 -0
  339. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.js +932 -0
  340. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts +210 -0
  341. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts.map +1 -0
  342. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.js +368 -0
  343. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts +63 -0
  344. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts.map +1 -0
  345. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.js +149 -0
  346. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts +35 -0
  347. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts.map +1 -0
  348. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.js +179 -0
  349. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts +179 -0
  350. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts.map +1 -0
  351. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.js +299 -0
  352. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts +395 -0
  353. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts.map +1 -0
  354. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.js +839 -0
  355. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.d.ts +44 -0
  356. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.d.ts.map +1 -0
  357. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.js +255 -0
  358. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts +185 -0
  359. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts.map +1 -0
  360. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.js +504 -0
  361. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts +17 -0
  362. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts.map +1 -0
  363. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.js +80 -0
  364. package/node_modules/@try-works/tool-mode-extension-sdk/lib/licensing.d.ts +221 -0
  365. package/node_modules/@try-works/tool-mode-extension-sdk/lib/licensing.d.ts.map +1 -0
  366. package/node_modules/@try-works/tool-mode-extension-sdk/lib/licensing.js +188 -0
  367. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts +52 -0
  368. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts.map +1 -0
  369. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.js +102 -0
  370. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts +171 -0
  371. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts.map +1 -0
  372. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.js +271 -0
  373. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts +242 -0
  374. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts.map +1 -0
  375. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.js +58 -0
  376. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts +128 -0
  377. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts.map +1 -0
  378. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.js +113 -0
  379. package/node_modules/@try-works/tool-mode-extension-sdk/lib/test-host.d.ts +41 -0
  380. package/node_modules/@try-works/tool-mode-extension-sdk/lib/test-host.d.ts.map +1 -0
  381. package/node_modules/@try-works/tool-mode-extension-sdk/lib/test-host.js +52 -0
  382. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts +2 -0
  383. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts.map +1 -0
  384. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.js +351 -0
  385. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts +157 -0
  386. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts.map +1 -0
  387. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.js +688 -0
  388. package/node_modules/@try-works/tool-mode-extension-sdk/package.json +54 -0
  389. package/node_modules/@try-works/tool-mode-extension-sdk/schema/extension-manifest-1.1.0.schema.json +253 -0
  390. package/node_modules/@try-works/tool-mode-recipes/bin/tool-mode-recipe.mjs +15 -0
  391. package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts +460 -0
  392. package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts.map +1 -0
  393. package/node_modules/@try-works/tool-mode-recipes/lib/broker.js +647 -0
  394. package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts +61 -0
  395. package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts.map +1 -0
  396. package/node_modules/@try-works/tool-mode-recipes/lib/cli.js +1106 -0
  397. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts +237 -0
  398. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts.map +1 -0
  399. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.js +205 -0
  400. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts +85 -0
  401. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts.map +1 -0
  402. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.js +176 -0
  403. package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts +119 -0
  404. package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts.map +1 -0
  405. package/node_modules/@try-works/tool-mode-recipes/lib/images.js +201 -0
  406. package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts +25 -0
  407. package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts.map +1 -0
  408. package/node_modules/@try-works/tool-mode-recipes/lib/index.js +25 -0
  409. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts +345 -0
  410. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts.map +1 -0
  411. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.js +1465 -0
  412. package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts +51 -0
  413. package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts.map +1 -0
  414. package/node_modules/@try-works/tool-mode-recipes/lib/templates.js +310 -0
  415. package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts +231 -0
  416. package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts.map +1 -0
  417. package/node_modules/@try-works/tool-mode-recipes/lib/titles.js +435 -0
  418. package/node_modules/@try-works/tool-mode-recipes/package.json +56 -0
  419. package/node_modules/@try-works/tool-mode-recipes/recipes/describe-image.json +38 -0
  420. package/node_modules/@try-works/tool-mode-recipes/recipes/text-title.json +45 -0
  421. package/node_modules/@try-works/tool-mode-recipes/schema/harness-model-recipe.schema.json +273 -0
  422. package/package.json +127 -4
  423. package/remote/spec.mjs +993 -0
  424. package/remote/typert.host.mjs +14 -0
  425. package/remote/typert.remote-client.mjs +16 -0
@@ -0,0 +1,1465 @@
1
+ /**
2
+ * The declarative recipe package format (RCP-01, RCP-02, RCP-03, RCP-05, RCP-06).
3
+ *
4
+ * A recipe is TRUSTED, DATA-ONLY: a static instruction string plus typed input
5
+ * bindings. It is deliberately neither a PTC program nor an autonomous subagent. It has
6
+ * no statements, no expressions, no imports and no filesystem access, because the moment
7
+ * a recipe can COMPUTE something it needs an interpreter with a threat model instead of
8
+ * a validator with an allow-list — and the threat model is the expensive part.
9
+ *
10
+ * Every rule below exists to keep that true against a recipe that is trying not to be
11
+ * data, and each one names what it prevents:
12
+ *
13
+ * - **Unknown keys are refused, not ignored.** A key this host does not implement is
14
+ * either a typo or a field that means something elsewhere; silently dropping it runs a
15
+ * recipe that is not the one the author wrote (RCP-02, RCP-06).
16
+ * - **`tools`, `messages`, `history` and `conversation` are refused BY NAME**, with a
17
+ * reason that says a recipe is data and not a program. HLM-05 forbids inheriting the
18
+ * parent's conversation, system prompt, tools, skills, attachments or memory, so a
19
+ * recipe that asks for any of them is refused here rather than quietly starved of them
20
+ * at dispatch time.
21
+ * - **Executable template syntax is refused.** `{{name}}` is a binding placeholder and
22
+ * nothing else; `${...}` and `<%...%>` are template CODE, and RCP-02 forbids it.
23
+ * - **A placeholder that names no declared input is refused** (RCP-03: an unbound
24
+ * pointer), because a template referring to something the caller can never supply is a
25
+ * recipe that cannot be satisfied.
26
+ * - **The limits are RES-10's initial per-leaf maxima**, asserted individually: 1024
27
+ * output tokens and two image inputs. A recipe may lower a limit, never raise it.
28
+ * - **The parsed recipe is deep-frozen**, so a caller holding it cannot widen the limits
29
+ * or rewrite the instructions it was authorized with (RCP-05, RCP-06).
30
+ *
31
+ * Bindings are a NAME-KEYED map rather than the specification's JSON Pointers on
32
+ * purpose. A pointer is a small language — `/a/b/0`, `~0` escapes, `-`, traversal past a
33
+ * reserved member — and RCP-03 has to reject all of it. A closed map of identifier-named
34
+ * inputs has no pointer to traverse, so the whole class of pointer abuse is absent by
35
+ * construction rather than by check.
36
+ *
37
+ * @module @try-works/tool-mode-recipes/recipe-format
38
+ */
39
+ /**
40
+ * RES-10's initial maximum output tokens for one leaf invocation.
41
+ *
42
+ * Restated here rather than imported: this package must not depend on the host package,
43
+ * because a declarative recipe package is loaded by the host and not the other way
44
+ * around (EXT-02, ARCH-02). The number is asserted in the tests so a drift between the
45
+ * two copies is a failing test rather than a silent budget change.
46
+ */
47
+ export const INITIAL_MAX_OUTPUT_TOKENS = 1024;
48
+ /** RES-10's initial maximum image inputs for one leaf invocation. */
49
+ export const INITIAL_MAX_IMAGE_INPUTS = 2;
50
+ /** The image formats this recipe format actually validates (HLM-11). */
51
+ const SUPPORTED_IMAGE_MEDIA_TYPES = ['image/png', 'image/jpeg', 'image/webp'];
52
+ /** RES-10's initial pixel ceiling for one image (16 MP). */
53
+ const MAX_IMAGE_PIXELS = 16_000_000;
54
+ /** The only keys a recipe may carry at the top level. */
55
+ export const RECIPE_TOP_LEVEL_KEYS = [
56
+ 'id',
57
+ 'version',
58
+ 'description',
59
+ 'tool',
60
+ 'inputs',
61
+ 'promptTemplate',
62
+ 'maxOutputTokens',
63
+ 'imageInputs',
64
+ 'policy',
65
+ ];
66
+ /**
67
+ * Key names that are refused wherever they appear, at any depth.
68
+ *
69
+ * These are the fields through which a recipe would stop being data and start being a
70
+ * program with access to the parent agent's context. They are checked BEFORE the
71
+ * allow-list so the failure names the real problem — "a recipe is data, not a program" —
72
+ * instead of reporting a generic unknown key.
73
+ */
74
+ export const FORBIDDEN_RECIPE_KEYS = [
75
+ 'tools',
76
+ 'tool_calls',
77
+ 'toolCalls',
78
+ 'messages',
79
+ 'history',
80
+ 'conversation',
81
+ 'attachments',
82
+ 'memory',
83
+ 'skills',
84
+ 'systemPrompt',
85
+ // RCP-02's own context flags. HLM-05 says a flag meaning "take the entire
86
+ // conversation" is not supported in this release, so the flag is refused rather than
87
+ // accepted and then ignored — an ignored flag is a promise the host does not keep.
88
+ 'inheritConversation',
89
+ 'includeTools',
90
+ ];
91
+ /**
92
+ * Property names that must never appear, because they reach past data into the object
93
+ * model (RCP-03: reserved prototype/member abuse).
94
+ */
95
+ export const RESERVED_RECIPE_KEYS = ['__proto__', 'constructor', 'prototype'];
96
+ /** A legal input name, a legal placeholder and a legal JSON Schema property. */
97
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
98
+ /**
99
+ * A legal contract identity.
100
+ *
101
+ * Two spellings, because the specification uses two: a STANDARD contract is a dotted
102
+ * lower-case identity (`text.title`), which is the form `tool-mode-contracts` publishes,
103
+ * and a THIRD-PARTY contract is scoped to the package that owns it
104
+ * (`@acme/tool-mode-image-review/review`), which is the form the TMX manifest example
105
+ * uses. Refusing the second would reject a spec-conformant third-party recipe, so both
106
+ * are admitted and the identity comparison below is what actually protects against a
107
+ * recipe claiming another tool.
108
+ */
109
+ const CONTRACT_IDENTITY = /^(?:[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+|@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)+)$/;
110
+ /** The keys a recipe's `tool` object may carry. */
111
+ const TOOL_KEYS = ['name', 'contractId'];
112
+ /** The keys a recipe's `imageInputs` object may carry. */
113
+ const IMAGE_INPUTS_KEYS = ['max'];
114
+ /** The keys a recipe's `policy` object may carry. */
115
+ const POLICY_KEYS = ['record', 'allowRemote'];
116
+ /** The keys one input declaration may carry. */
117
+ const INPUT_KEYS = [
118
+ 'type',
119
+ 'required',
120
+ 'description',
121
+ 'minLength',
122
+ 'maxLength',
123
+ 'minimum',
124
+ 'maximum',
125
+ 'enum',
126
+ 'default',
127
+ 'mediaTypes',
128
+ 'maxBytes',
129
+ 'jsonSerialized',
130
+ ];
131
+ /** The kinds this format understands, for validation and error messages. */
132
+ const INPUT_KINDS = ['string', 'integer', 'number', 'boolean', 'image'];
133
+ /**
134
+ * Render one named error.
135
+ *
136
+ * The code prefix is what a caller switches on; the sentence after it is what a human
137
+ * reads. Both are needed: a code alone does not say which key was wrong, and a sentence
138
+ * alone cannot be matched on without matching prose.
139
+ * @param code - the error code.
140
+ * @param message - the specific problem, naming the offending key or field.
141
+ * @returns the rendered error.
142
+ */
143
+ export function formatRecipeError(code, message) {
144
+ return `${code}: ${message}`;
145
+ }
146
+ /**
147
+ * The code of a rendered error.
148
+ * @param error - a rendered error.
149
+ * @returns the code, or the whole string when it carries no prefix.
150
+ */
151
+ export function errorCodeOf(error) {
152
+ const separator = error.indexOf(':');
153
+ return separator === -1 ? error : error.slice(0, separator);
154
+ }
155
+ /** Whether a value is a plain JSON-style object, as opposed to an array or a class. */
156
+ function isPlainObject(value) {
157
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
158
+ return false;
159
+ const prototype = Object.getPrototypeOf(value);
160
+ // A class instance, a Date, a Map or a RegExp is not data; a null-prototype object is.
161
+ return prototype === Object.prototype || prototype === null;
162
+ }
163
+ /** Name a value's kind for an error message. */
164
+ function describeKind(value) {
165
+ if (value === null)
166
+ return 'null';
167
+ if (Array.isArray(value))
168
+ return 'an array';
169
+ const kind = typeof value;
170
+ if (kind !== 'object')
171
+ return `a ${kind}`;
172
+ return isPlainObject(value) ? 'an object' : 'a non-plain object (a class instance is not data)';
173
+ }
174
+ /** Freeze a recipe in place, at every depth. */
175
+ function deepFreeze(value) {
176
+ if (typeof value === 'object' && value !== null) {
177
+ for (const child of Object.values(value))
178
+ deepFreeze(child);
179
+ Object.freeze(value);
180
+ }
181
+ return value;
182
+ }
183
+ /**
184
+ * Walk the raw recipe, refusing anything that is not data.
185
+ *
186
+ * Two rules run together here because both are about a value's POSITION, not its shape:
187
+ * a key that names context the request must not carry, and a key that reaches into the
188
+ * object model. Doing this as a walk rather than as a top-level check is what catches
189
+ * `{ tool: { history: [] } }`, which a top-level allow-list would report as a mere
190
+ * unknown nested key.
191
+ * @param node - the value being walked.
192
+ * @param path - the dotted path, for the error message.
193
+ * @param errors - collected errors.
194
+ */
195
+ /** Deepest JSON value accepted in a recipe or a JSON-serialized binding. */
196
+ const MAX_DATA_DEPTH = 32;
197
+ /** Largest JSON-serialized binding accepted, in characters. */
198
+ const MAX_JSON_BINDING_BYTES = 1_000_000;
199
+ function checkDataOnly(node, path, errors, depth = 0) {
200
+ if (depth > MAX_DATA_DEPTH) {
201
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path} is nested deeper than ${MAX_DATA_DEPTH} levels; a bounded data value is required (RCP-03)`));
202
+ return;
203
+ }
204
+ if (node === null || typeof node === 'string' || typeof node === 'boolean')
205
+ return;
206
+ if (typeof node === 'number') {
207
+ if (!Number.isFinite(node)) {
208
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path} is not a finite number; NaN and Infinity are not JSON data`));
209
+ }
210
+ return;
211
+ }
212
+ if (Array.isArray(node)) {
213
+ for (const [index, item] of node.entries())
214
+ checkDataOnly(item, `${path}[${index}]`, errors, depth + 1);
215
+ return;
216
+ }
217
+ if (!isPlainObject(node)) {
218
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path} is ${describeKind(node)}, which is not data. A recipe may contain only JSON values — no functions, `
219
+ + 'no classes, no executable template code (RCP-02)'));
220
+ return;
221
+ }
222
+ for (const [key, value] of Object.entries(node)) {
223
+ const child = path === '' ? key : `${path}.${key}`;
224
+ if (FORBIDDEN_RECIPE_KEYS.includes(key)) {
225
+ errors.push(formatRecipeError('INVALID_RECIPE', `${child} is refused by name. A recipe is DATA, not a program: it cannot supply tools, messages, `
226
+ + 'conversation history, attachments, memory or skills, because the broker builds the provider request from '
227
+ + 'the recipe and the bound inputs alone (HLM-05, HLM-06). Remove the key; something that needs tools is a '
228
+ + 'PTC program, which is a different artifact'));
229
+ continue;
230
+ }
231
+ if (RESERVED_RECIPE_KEYS.includes(key)) {
232
+ errors.push(formatRecipeError('INVALID_RECIPE', `${child} uses a reserved property name; RCP-03 rejects reserved prototype/member abuse`));
233
+ continue;
234
+ }
235
+ checkDataOnly(value, child, errors, depth + 1);
236
+ }
237
+ }
238
+ /** The unknown keys in an object, against an allow-list. */
239
+ function unknownKeys(value, allowed) {
240
+ return Object.keys(value).filter(key => !allowed.includes(key));
241
+ }
242
+ /** Render an unknown-key refusal. */
243
+ function unknownKeyError(path, key, allowed) {
244
+ return formatRecipeError('INVALID_RECIPE', `${path}${path === '' ? '' : '.'}${key} is not a key this format has. Allowed here: ${allowed.join(', ')}. `
245
+ + 'A recipe\'s shape is closed: an unknown key is a typo or a field this host does not implement, and ignoring it '
246
+ + 'would run a recipe other than the one the author wrote (RCP-02, RCP-06)');
247
+ }
248
+ /** Read a finite integer, or report why not. */
249
+ function readInteger(value, path, errors) {
250
+ if (typeof value !== 'number' || !Number.isInteger(value)) {
251
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path} must be an integer; received ${describeKind(value)}`));
252
+ return undefined;
253
+ }
254
+ return value;
255
+ }
256
+ /** Read a non-empty string, or report why not. */
257
+ function readNonEmptyString(value, path, errors) {
258
+ if (typeof value !== 'string') {
259
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path} must be a string; received ${describeKind(value)}`));
260
+ return undefined;
261
+ }
262
+ if (value.trim().length === 0) {
263
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path} must not be empty`));
264
+ return undefined;
265
+ }
266
+ return value;
267
+ }
268
+ /**
269
+ * Validate one input declaration.
270
+ * @param name - the input name.
271
+ * @param raw - the raw declaration.
272
+ * @param errors - collected errors.
273
+ * @returns the declaration, when it is valid enough to keep.
274
+ */
275
+ function parseInputDeclaration(name, raw, errors) {
276
+ const path = `inputs.${name}`;
277
+ if (!IDENTIFIER.test(name)) {
278
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path} is not a legal input name. A name is an identifier because it is also the placeholder the template `
279
+ + 'binds, and a name that cannot be written as `{{name}}` cannot be bound at all'));
280
+ return undefined;
281
+ }
282
+ if (!isPlainObject(raw)) {
283
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path} must be an object; received ${describeKind(raw)}`));
284
+ return undefined;
285
+ }
286
+ for (const key of unknownKeys(raw, INPUT_KEYS))
287
+ errors.push(unknownKeyError(path, key, INPUT_KEYS));
288
+ const declaredType = raw['type'];
289
+ if (typeof declaredType !== 'string' || !INPUT_KINDS.includes(declaredType)) {
290
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path}.type must be one of ${INPUT_KINDS.join(', ')}; received ${JSON.stringify(declaredType)}`));
291
+ return undefined;
292
+ }
293
+ const type = declaredType;
294
+ const requiredRaw = raw['required'];
295
+ if (requiredRaw !== undefined && typeof requiredRaw !== 'boolean') {
296
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path}.required must be a boolean`));
297
+ }
298
+ const required = requiredRaw === true;
299
+ const declaration = { type, required };
300
+ // A canonical `inputBindings` kind "json" binding arrives as a string
301
+ // declaration carrying this marker; it is read through so the canonical
302
+ // round trip stays lossless.
303
+ if (raw['jsonSerialized'] !== undefined) {
304
+ if (raw['jsonSerialized'] !== true || type !== 'string') {
305
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path}.jsonSerialized must be true and only applies to a string binding`));
306
+ }
307
+ else {
308
+ declaration.jsonSerialized = true;
309
+ }
310
+ }
311
+ if (raw['description'] !== undefined) {
312
+ const description = readNonEmptyString(raw['description'], `${path}.description`, errors);
313
+ if (description !== undefined)
314
+ declaration.description = description;
315
+ }
316
+ /**
317
+ * Keywords are only accepted on the kind they constrain.
318
+ *
319
+ * A `maxLength` on an image, or a `minimum` on a string, is a constraint its author
320
+ * believes is enforced. Silently ignoring it would ship a bound the recipe never had,
321
+ * which is the failure BASE-04 names for schema projection.
322
+ */
323
+ const keyword = (key, kinds) => {
324
+ const value = raw[key];
325
+ if (value === undefined)
326
+ return undefined;
327
+ if (!kinds.includes(type)) {
328
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path}.${key} does not apply to an input of type "${type}"; it constrains ${kinds.join(' or ')}. A constraint `
329
+ + 'that is ignored is a bound the author believes is enforced'));
330
+ return undefined;
331
+ }
332
+ return value;
333
+ };
334
+ const minLength = keyword('minLength', ['string']);
335
+ if (minLength !== undefined) {
336
+ const value = readInteger(minLength, `${path}.minLength`, errors);
337
+ if (value !== undefined)
338
+ declaration.minLength = value;
339
+ }
340
+ const maxLength = keyword('maxLength', ['string']);
341
+ if (maxLength !== undefined) {
342
+ const value = readInteger(maxLength, `${path}.maxLength`, errors);
343
+ if (value !== undefined)
344
+ declaration.maxLength = value;
345
+ }
346
+ const minimum = keyword('minimum', ['integer', 'number']);
347
+ if (minimum !== undefined) {
348
+ const value = typeof minimum === 'number' && Number.isFinite(minimum) ? minimum : undefined;
349
+ if (value === undefined)
350
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path}.minimum must be a finite number`));
351
+ else
352
+ declaration.minimum = value;
353
+ }
354
+ const maximum = keyword('maximum', ['integer', 'number']);
355
+ if (maximum !== undefined) {
356
+ const value = typeof maximum === 'number' && Number.isFinite(maximum) ? maximum : undefined;
357
+ if (value === undefined)
358
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path}.maximum must be a finite number`));
359
+ else
360
+ declaration.maximum = value;
361
+ }
362
+ const enumerated = keyword('enum', ['string', 'integer', 'number', 'boolean']);
363
+ if (enumerated !== undefined) {
364
+ if (!Array.isArray(enumerated) || enumerated.length === 0
365
+ || !enumerated.every(member => typeof member === 'string' || typeof member === 'number' || typeof member === 'boolean')) {
366
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path}.enum must be a non-empty array of scalar values`));
367
+ }
368
+ else {
369
+ declaration.enum = enumerated;
370
+ }
371
+ }
372
+ const mediaTypes = keyword('mediaTypes', ['image']);
373
+ if (mediaTypes !== undefined) {
374
+ if (!Array.isArray(mediaTypes) || mediaTypes.length === 0
375
+ || !mediaTypes.every(member => typeof member === 'string' && member.startsWith('image/'))) {
376
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path}.mediaTypes must be a non-empty array of image media types (for example "image/png")`));
377
+ }
378
+ else {
379
+ declaration.mediaTypes = mediaTypes;
380
+ }
381
+ }
382
+ const maxBytes = keyword('maxBytes', ['image']);
383
+ if (maxBytes !== undefined) {
384
+ const value = readInteger(maxBytes, `${path}.maxBytes`, errors);
385
+ if (value !== undefined) {
386
+ if (value < 1)
387
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path}.maxBytes must be at least 1`));
388
+ else
389
+ declaration.maxBytes = value;
390
+ }
391
+ }
392
+ if (raw['default'] !== undefined) {
393
+ const fallback = raw['default'];
394
+ const scalar = typeof fallback === 'string' || typeof fallback === 'number' || typeof fallback === 'boolean';
395
+ if (!scalar || type === 'image') {
396
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path}.default must be a scalar matching the declared type; an image binding has no default, because a `
397
+ + 'default image would be a media reference the caller never supplied'));
398
+ }
399
+ else if (!matchesScalarType(type, fallback)) {
400
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path}.default does not match the declared type ${type}`));
401
+ }
402
+ else if (declaration.enum !== undefined && !declaration.enum.includes(fallback)) {
403
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path}.default is not one of the declared enum values`));
404
+ }
405
+ else {
406
+ declaration.default = fallback;
407
+ }
408
+ }
409
+ return declaration;
410
+ }
411
+ /** Whether a scalar satisfies a declared scalar kind. */
412
+ function matchesScalarType(type, value) {
413
+ switch (type) {
414
+ case 'string': return typeof value === 'string';
415
+ case 'boolean': return typeof value === 'boolean';
416
+ case 'integer': return typeof value === 'number' && Number.isInteger(value);
417
+ case 'number': return typeof value === 'number' && Number.isFinite(value);
418
+ case 'image': return false;
419
+ }
420
+ }
421
+ /** Whether a string is a JSON Pointer per RFC 6901 (escapes must be `~0` or `~1`). */
422
+ function isRfc6901Pointer(value) {
423
+ if (!value.startsWith('/'))
424
+ return false;
425
+ return value.slice(1).split('/').every(segment => !/~(?![01])/.test(segment));
426
+ }
427
+ /** Every `{{name}}` placeholder in a template, in order of appearance. */
428
+ function placeholdersIn(template) {
429
+ const found = [];
430
+ for (const match of template.matchAll(/\{\{([^{}]*)\}\}/g)) {
431
+ found.push((match[1] ?? '').trim());
432
+ }
433
+ return found;
434
+ }
435
+ /**
436
+ * Normalize a CANONICAL RCP-02 document into the legacy raw shape the parser
437
+ * below reads, collecting every canonical-key violation as a named error.
438
+ *
439
+ * The mapping is: `instructions`→`promptTemplate`; `inputBindings`→`inputs`
440
+ * (pointer→name, kind text|image|json, required, label→description);
441
+ * `limits.maxOutputTokens`→`maxOutputTokens`; `limits.maxImages`→
442
+ * `imageInputs.max`; `policy` defaults to `{record:true, allowRemote:true}`
443
+ * (recording defaults ON per HLM-09). `modelSelection.strategy:"caller-model"`
444
+ * is the default and is validated then dropped; `"approved-profile"` is kept.
445
+ * `context` must be `{inheritConversation:false, includeTools:false}` — the
446
+ * "take the entire conversation" flag is not supported in this release
447
+ * (HLM-05) and is refused. `response` is validated and kept. `schemaVersion`
448
+ * and `kind:"harness-model"` are constants. `limits.timeoutMs`,
449
+ * `maxInputTextBytes`, `maxAttempts`, `repairAttempts` are validated and
450
+ * dropped: the broker enforces the RES-10 ceilings and the caller owns the
451
+ * wall-clock deadline (RES-10's 60 s figure is the caller's to pass).
452
+ *
453
+ * @param raw - the decoded recipe document.
454
+ * @param errors - the error sink.
455
+ * @returns the legacy-shaped document plus the canonical-only extras, or
456
+ * @param raw - the decoded recipe document.
457
+ * @param errors - the error sink.
458
+ * @returns the legacy-shaped document plus the canonical-only extras.
459
+ */
460
+ function normalizeRecipeDocument(raw, errors) {
461
+ const normalized = {};
462
+ const extras = {};
463
+ // Every canonical sub-object is CLOSED: unknown keys are refused, and the
464
+ // forbidden/reserved names are refused by name — nothing is silently dropped.
465
+ const refuseUnsafeKeys = (container, path, allowed) => {
466
+ for (const key of Object.keys(container)) {
467
+ if (allowed.includes(key))
468
+ continue;
469
+ if (FORBIDDEN_RECIPE_KEYS.includes(key)) {
470
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path}.${key} is refused by name. A recipe is DATA, not a program: it cannot supply tools, messages, `
471
+ + 'conversation history, attachments, memory or skills (HLM-05, HLM-06)'));
472
+ }
473
+ else if (RESERVED_RECIPE_KEYS.includes(key)) {
474
+ errors.push(formatRecipeError('INVALID_RECIPE', `${path}.${key} is a reserved prototype member and is refused by name (RCP-03)`));
475
+ }
476
+ else {
477
+ errors.push(unknownKeyError(path, key, allowed));
478
+ }
479
+ }
480
+ };
481
+ // Constants: schemaVersion is accepted and pinned; kind must be harness-model.
482
+ if (raw['schemaVersion'] !== undefined) {
483
+ if (typeof raw['schemaVersion'] !== 'string' || raw['schemaVersion'].length === 0) {
484
+ errors.push(formatRecipeError('INVALID_RECIPE', 'schemaVersion must be a version string (for example "1.0.0")'));
485
+ }
486
+ }
487
+ if (raw['kind'] !== undefined && raw['kind'] !== 'harness-model') {
488
+ errors.push(formatRecipeError('INVALID_RECIPE', `kind must be "harness-model" when declared; received ${describeKind(raw['kind'])}`));
489
+ }
490
+ for (const key of ['id', 'version', 'description', 'tool']) {
491
+ if (raw[key] !== undefined)
492
+ normalized[key] = raw[key];
493
+ }
494
+ // --- instructions -----------------------------------------------------------
495
+ const instructions = raw['instructions'];
496
+ if (typeof instructions !== 'string' || instructions.length === 0) {
497
+ errors.push(formatRecipeError('INVALID_RECIPE', `instructions must be a non-empty string; received ${describeKind(instructions)}`));
498
+ }
499
+ // --- modelSelection (RCP-05) ------------------------------------------------
500
+ const modelSelection = raw['modelSelection'];
501
+ const requires = { inputModalities: [], outputModalities: [] };
502
+ let strategy;
503
+ if (!isPlainObject(modelSelection)) {
504
+ errors.push(formatRecipeError('INVALID_RECIPE', `modelSelection must be an object carrying "strategy" and "requires"; received ${describeKind(modelSelection)}`));
505
+ }
506
+ else {
507
+ refuseUnsafeKeys(modelSelection, 'modelSelection', ['strategy', 'requires', 'profile']);
508
+ strategy = typeof modelSelection['strategy'] === 'string' ? modelSelection['strategy'] : undefined;
509
+ if (strategy !== 'caller-model' && strategy !== 'approved-profile') {
510
+ errors.push(formatRecipeError('INVALID_RECIPE', `modelSelection.strategy must be "caller-model" or "approved-profile"; received ${describeKind(modelSelection['strategy'])}`));
511
+ }
512
+ const rawRequires = modelSelection['requires'];
513
+ if (!isPlainObject(rawRequires)) {
514
+ errors.push(formatRecipeError('INVALID_RECIPE', 'modelSelection.requires must carry inputModalities and outputModalities'));
515
+ }
516
+ else {
517
+ refuseUnsafeKeys(rawRequires, 'modelSelection.requires', ['inputModalities', 'outputModalities']);
518
+ for (const [modalityKey, destination] of [['inputModalities', requires.inputModalities], ['outputModalities', requires.outputModalities]]) {
519
+ const value = rawRequires[modalityKey];
520
+ if (!Array.isArray(value) || value.length === 0 || !value.every(entry => entry === 'text' || entry === 'image' || entry === 'audio' || entry === 'video')) {
521
+ errors.push(formatRecipeError('INVALID_RECIPE', `modelSelection.requires.${modalityKey} must be a non-empty array of the known modalities (text, image, audio, video); received ${describeKind(value)}`));
522
+ }
523
+ else {
524
+ for (const entry of value)
525
+ destination.push(entry);
526
+ }
527
+ }
528
+ }
529
+ const profile = modelSelection['profile'];
530
+ if (strategy === 'approved-profile') {
531
+ if (typeof profile !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(profile)) {
532
+ errors.push(formatRecipeError('INVALID_RECIPE', 'modelSelection.strategy "approved-profile" requires a profile name of letters, digits, dot, underscore or dash'));
533
+ }
534
+ else {
535
+ extras.modelSelection = { strategy: 'approved-profile', profile };
536
+ }
537
+ }
538
+ else if (profile !== undefined) {
539
+ errors.push(formatRecipeError('INVALID_RECIPE', 'modelSelection.profile is only valid with strategy "approved-profile"'));
540
+ }
541
+ }
542
+ // --- context (HLM-05's frozen invariants) ------------------------------------
543
+ const context = raw['context'];
544
+ if (context !== undefined) {
545
+ if (!isPlainObject(context)) {
546
+ errors.push(formatRecipeError('INVALID_RECIPE', `context must be the object {inheritConversation:false, includeTools:false}; received ${describeKind(context)}`));
547
+ }
548
+ else {
549
+ refuseUnsafeKeys(context, 'context', ['inheritConversation', 'includeTools']);
550
+ if (context['inheritConversation'] !== false
551
+ || context['includeTools'] !== false) {
552
+ errors.push(formatRecipeError('INVALID_RECIPE', 'context must be exactly {inheritConversation:false, includeTools:false}: a flag meaning "take the entire '
553
+ + 'conversation" is not supported in this release (HLM-05)'));
554
+ }
555
+ }
556
+ }
557
+ // --- response (RCP-04) -------------------------------------------------------
558
+ const response = raw['response'];
559
+ if (response !== undefined) {
560
+ if (!isPlainObject(response)) {
561
+ errors.push(formatRecipeError('INVALID_RECIPE', `response must be an object carrying "mode" and "field"; received ${describeKind(response)}`));
562
+ }
563
+ else {
564
+ refuseUnsafeKeys(response, 'response', ['mode', 'field']);
565
+ const mode = response['mode'];
566
+ const field = response['field'];
567
+ if (mode !== 'text' && mode !== 'json') {
568
+ errors.push(formatRecipeError('INVALID_RECIPE', `response.mode must be "text" or "json"; received ${describeKind(mode)}`));
569
+ }
570
+ if (typeof field !== 'string' || field.length === 0) {
571
+ errors.push(formatRecipeError('INVALID_RECIPE', 'response.field must be a non-empty payload field name'));
572
+ }
573
+ if (mode === 'text' || mode === 'json') {
574
+ if (typeof field === 'string' && field.length > 0) {
575
+ extras.response = { mode, field };
576
+ }
577
+ }
578
+ }
579
+ }
580
+ // --- limits ------------------------------------------------------------------
581
+ const limits = raw['limits'];
582
+ let maxOutputTokens;
583
+ let maxImages = 0;
584
+ if (!isPlainObject(limits)) {
585
+ errors.push(formatRecipeError('INVALID_RECIPE', 'limits must be an object carrying at least maxOutputTokens and maxImages; received '
586
+ + `${describeKind(limits)}`));
587
+ }
588
+ else {
589
+ refuseUnsafeKeys(limits, 'limits', ['timeoutMs', 'maxInputTextBytes', 'maxImages', 'maxOutputTokens', 'maxAttempts', 'repairAttempts']);
590
+ maxOutputTokens = readInteger(limits['maxOutputTokens'], 'limits.maxOutputTokens', errors) ?? undefined;
591
+ if (maxOutputTokens !== undefined && (maxOutputTokens < 1 || maxOutputTokens > INITIAL_MAX_OUTPUT_TOKENS)) {
592
+ errors.push(formatRecipeError('INVALID_RECIPE', `limits.maxOutputTokens must be between 1 and ${INITIAL_MAX_OUTPUT_TOKENS} (RES-10)`));
593
+ }
594
+ const rawMaxImages = readInteger(limits['maxImages'], 'limits.maxImages', errors);
595
+ if (rawMaxImages !== undefined && (rawMaxImages < 0 || rawMaxImages > INITIAL_MAX_IMAGE_INPUTS)) {
596
+ errors.push(formatRecipeError('INVALID_RECIPE', `limits.maxImages must be between 0 and ${INITIAL_MAX_IMAGE_INPUTS} (RES-10)`));
597
+ }
598
+ else if (rawMaxImages !== undefined) {
599
+ maxImages = rawMaxImages;
600
+ }
601
+ // Accepted, validated, dropped (see the docstring above): the caller owns
602
+ // the deadline and the broker enforces the RES-10 ceilings.
603
+ for (const key of ['timeoutMs', 'maxInputTextBytes', 'maxAttempts', 'repairAttempts']) {
604
+ if (limits[key] !== undefined)
605
+ readInteger(limits[key], `limits.${key}`, errors);
606
+ }
607
+ }
608
+ normalized.maxOutputTokens = maxOutputTokens;
609
+ normalized.imageInputs = { max: maxImages };
610
+ // Recording defaults ON; destinations default to the author's acceptance.
611
+ normalized.policy = { record: true, allowRemote: true };
612
+ // --- inputBindings ------------------------------------------------------------
613
+ const rawBindings = raw['inputBindings'];
614
+ const inputs = Object.create(null);
615
+ const bindingNames = new Set();
616
+ if (!Array.isArray(rawBindings) || rawBindings.length === 0) {
617
+ errors.push(formatRecipeError('INVALID_RECIPE', `inputBindings must be a non-empty array of {kind, pointer, required, label}; received ${describeKind(rawBindings)}`));
618
+ }
619
+ else {
620
+ for (const entry of rawBindings) {
621
+ if (!isPlainObject(entry)) {
622
+ errors.push(formatRecipeError('INVALID_RECIPE', `inputBindings entries must be objects; received ${describeKind(entry)}`));
623
+ continue;
624
+ }
625
+ refuseUnsafeKeys(entry, 'inputBindings[]', ['kind', 'pointer', 'required', 'label', 'minLength', 'maxLength', 'minimum', 'maximum', 'enum', 'default', 'mediaTypes', 'maxBytes']);
626
+ const kind = entry['kind'];
627
+ const pointer = entry['pointer'];
628
+ if (kind !== 'text' && kind !== 'image' && kind !== 'json') {
629
+ errors.push(formatRecipeError('INVALID_RECIPE', `inputBindings[].kind must be "text", "image" or "json"; received ${describeKind(kind)}`));
630
+ continue;
631
+ }
632
+ if (typeof pointer !== 'string' || !isRfc6901Pointer(pointer)) {
633
+ errors.push(formatRecipeError('INVALID_RECIPE', `inputBindings[].pointer must be a valid JSON Pointer (RFC 6901, starting with "/" and using only ~0/~1 `
634
+ + `escapes); received ${JSON.stringify(pointer)} (RCP-03)`));
635
+ continue;
636
+ }
637
+ const name = pointer.slice(1);
638
+ if (name.includes('/') || !/^[A-Za-z0-9_-]+$/.test(name)) {
639
+ errors.push(formatRecipeError('INVALID_RECIPE', `inputBindings[].pointer ${JSON.stringify(pointer)} names a nested or non-simple input. This host's arguments `
640
+ + 'are a flat map of simple names, so a pointer that is valid RFC 6901 but not a top-level simple name is '
641
+ + 'refused rather than treated as an unbound placeholder (RCP-03)'));
642
+ continue;
643
+ }
644
+ if (bindingNames.has(name)) {
645
+ errors.push(formatRecipeError('INVALID_RECIPE', `inputBindings declare the pointer ${pointer} twice`));
646
+ continue;
647
+ }
648
+ bindingNames.add(name);
649
+ const required = entry['required'] === true;
650
+ const label = typeof entry['label'] === 'string' && entry['label'].length > 0 ? entry['label'] : undefined;
651
+ // The canonical serializer re-emits the richer constraint fields so the
652
+ // canonical round trip is LOSSLESS; the legacy parser below validates
653
+ // them with its own named refusals.
654
+ const constraints = {};
655
+ for (const key of ['minLength', 'maxLength', 'minimum', 'maximum', 'enum', 'default', 'mediaTypes', 'maxBytes']) {
656
+ if (entry[key] !== undefined)
657
+ constraints[key] = entry[key];
658
+ }
659
+ if (kind === 'image') {
660
+ inputs[name] = {
661
+ type: 'image', required,
662
+ ...label === undefined ? {} : { description: label },
663
+ ...constraints,
664
+ };
665
+ }
666
+ else {
667
+ // NOTE: no implicit minLength default — the canonical round trip must
668
+ // be LOSSLESS, and a legacy declaration without minLength parses to a
669
+ // frozen recipe without minLength, so the normalization must not invent
670
+ // one.
671
+ inputs[name] = {
672
+ type: 'string', required,
673
+ ...label === undefined ? {} : { description: label },
674
+ ...constraints,
675
+ ...kind === 'json' ? { jsonSerialized: true } : {},
676
+ };
677
+ }
678
+ }
679
+ if (Object.keys(inputs).length === 0 && errors.length === 0) {
680
+ errors.push(formatRecipeError('INVALID_RECIPE', 'inputBindings must declare at least one binding'));
681
+ }
682
+ }
683
+ normalized.inputs = inputs;
684
+ // --- promptTemplate rendering (RCP-03) --------------------------------------
685
+ // The canonical `instructions` are static prose; bindings are rendered BY
686
+ // POINTER. When the instructions do NOT already bind an input with the
687
+ // legacy `{{name}}` placeholder (the spec's §19.3 example has none), the
688
+ // normalized template appends one rendered line per unbound input — label or
689
+ // name, in declaration order — so the legacy "never binds" rule never fires
690
+ // on the canonical path and the bound VALUES reach the provider request.
691
+ if (typeof instructions === 'string') {
692
+ const bound = new Set();
693
+ for (const match of instructions.matchAll(/\{\{([^{}]*)\}\}/g)) {
694
+ bound.add((match[1] ?? '').trim());
695
+ }
696
+ const unboundLines = [];
697
+ for (const [name, declaration] of Object.entries(inputs)) {
698
+ if (bound.has(name))
699
+ continue;
700
+ const label = typeof declaration['description'] === 'string' && declaration['description'].length > 0
701
+ ? declaration['description'] : name;
702
+ unboundLines.push(`- ${label}: {{${name}}}`);
703
+ }
704
+ normalized.promptTemplate = unboundLines.length === 0
705
+ ? instructions
706
+ : `${instructions}\n\nSupplied context: the recipe instructions and the validated bound inputs only — no parent `
707
+ + `conversation, system prompt, tools or memory (HLM-05)\n${unboundLines.join('\n')}`;
708
+ }
709
+ // The declared requires must be consistent with the declared bindings:
710
+ // an image binding needs "image" in inputModalities, and nothing else is
711
+ // permitted to appear (HLM-04: unknown is not supported).
712
+ const declaredImage = Object.values(inputs).some(declaration => declaration['type'] === 'image');
713
+ if (requires.inputModalities.length > 0) {
714
+ if (declaredImage && !requires.inputModalities.includes('image')) {
715
+ errors.push(formatRecipeError('INVALID_RECIPE', 'the recipe declares an image binding but modelSelection.requires.inputModalities omits "image"'));
716
+ }
717
+ if (!declaredImage && requires.inputModalities.includes('image')) {
718
+ errors.push(formatRecipeError('INVALID_RECIPE', 'modelSelection.requires.inputModalities claims "image" but no image binding is declared'));
719
+ }
720
+ }
721
+ if (requires.outputModalities.length > 0 && !requires.outputModalities.includes('text')) {
722
+ errors.push(formatRecipeError('INVALID_RECIPE', 'this build only produces text output; modelSelection.requires.outputModalities must include "text"'));
723
+ }
724
+ return { normalized, extras };
725
+ }
726
+ /**
727
+ * Parse and validate a raw recipe.
728
+ *
729
+ * Every violation is collected and reported — one error per problem — because fixing a
730
+ * recipe one error per run is how an author gives up.
731
+ * @param raw - the parsed JSON of a recipe file, or any candidate value.
732
+ * @param options - the tool identity the recipe is being loaded as.
733
+ * @returns the frozen recipe, or every reason it is not one.
734
+ */
735
+ export function parseRecipe(raw, options = {}) {
736
+ const errors = [];
737
+ if (!isPlainObject(raw)) {
738
+ return {
739
+ ok: false,
740
+ errors: [formatRecipeError('INVALID_RECIPE', `a recipe must be a JSON object; received ${describeKind(raw)}`)],
741
+ };
742
+ }
743
+ // A CANONICAL RCP-02 document is normalized into the legacy shape BEFORE the
744
+ // data-only gate below, because the canonical vocabulary's own `context`
745
+ // object carries the HLM-05 flag names (`inheritConversation`/`includeTools`)
746
+ // that the legacy gate refuses by name at any depth — the normalizer
747
+ // VALIDATES those flags are exactly false and then DROPS them, so the
748
+ // normalized document carries no forbidden keys and the legacy checks apply
749
+ // unchanged. The raw document is still name-gated: every top-level key that
750
+ // is not part of the canonical vocabulary is refused (unknown/forbidden/
751
+ // reserved) BEFORE any value is read.
752
+ const canonicalKeys = new Set([
753
+ 'schemaVersion', 'kind', 'id', 'version', 'description', 'tool',
754
+ 'modelSelection', 'instructions', 'inputBindings', 'response', 'limits', 'context',
755
+ ]);
756
+ const isCanonical = canonicalKeys.has('instructions')
757
+ && (Object.prototype.hasOwnProperty.call(raw, 'instructions')
758
+ || Object.prototype.hasOwnProperty.call(raw, 'inputBindings')
759
+ || Object.prototype.hasOwnProperty.call(raw, 'modelSelection')
760
+ || Object.prototype.hasOwnProperty.call(raw, 'kind')
761
+ || Object.prototype.hasOwnProperty.call(raw, 'schemaVersion'));
762
+ if (isCanonical) {
763
+ for (const key of Object.keys(raw)) {
764
+ if (!canonicalKeys.has(key)) {
765
+ if (FORBIDDEN_RECIPE_KEYS.includes(key)) {
766
+ errors.push(formatRecipeError('INVALID_RECIPE', `${key} is refused by name. A recipe is DATA, not a program: it cannot supply tools, messages, conversation `
767
+ + 'history, attachments, memory or skills, because the broker builds the provider request from the recipe '
768
+ + 'and the bound inputs alone (HLM-05, HLM-06). Remove the key; something that needs tools is a PTC '
769
+ + 'program, which is a different artifact'));
770
+ }
771
+ else if (RESERVED_RECIPE_KEYS.includes(key)) {
772
+ errors.push(formatRecipeError('INVALID_RECIPE', `${key} is a reserved prototype member and is refused by name (RCP-03)`));
773
+ }
774
+ else {
775
+ errors.push(unknownKeyError('', key, [...canonicalKeys]));
776
+ }
777
+ }
778
+ }
779
+ if (errors.length > 0)
780
+ return { ok: false, errors };
781
+ const canonical = normalizeRecipeDocument(raw, errors);
782
+ if (errors.length > 0)
783
+ return { ok: false, errors };
784
+ const parsed = parseRecipe(canonical.normalized, options);
785
+ if (!parsed.ok)
786
+ return parsed;
787
+ const recipe = parsed.recipe;
788
+ const frozen = deepFreeze({
789
+ ...recipe,
790
+ ...canonical.extras.modelSelection === undefined ? {} : { modelSelection: canonical.extras.modelSelection },
791
+ ...canonical.extras.response === undefined ? {} : { response: canonical.extras.response },
792
+ });
793
+ return { ok: true, recipe: frozen };
794
+ }
795
+ // Data-only first: this is where a function, a class instance or a context key is
796
+ // caught, and it must happen before any field is read, because a getter could
797
+ // otherwise run arbitrary code during validation.
798
+ checkDataOnly(raw, '', errors);
799
+ for (const key of unknownKeys(raw, RECIPE_TOP_LEVEL_KEYS)) {
800
+ errors.push(unknownKeyError('', key, RECIPE_TOP_LEVEL_KEYS));
801
+ }
802
+ const id = readNonEmptyString(raw['id'], 'id', errors);
803
+ if (id !== undefined && !/^[A-Za-z0-9@][A-Za-z0-9._@/-]*$/.test(id)) {
804
+ errors.push(formatRecipeError('INVALID_RECIPE', `id ${JSON.stringify(id)} must be a stable identity of letters, digits, dot, dash, underscore, slash or a `
805
+ + 'leading scope, with no whitespace'));
806
+ }
807
+ const version = readNonEmptyString(raw['version'], 'version', errors);
808
+ if (version !== undefined && !/^\d+\.\d+\.\d+$/.test(version)) {
809
+ errors.push(formatRecipeError('INVALID_RECIPE', `version ${JSON.stringify(version)} must be semantic (x.y.z). A prompt change is a version change, because `
810
+ + 'identical JSON shape does not mean identical semantics (RCP-06)'));
811
+ }
812
+ const description = readNonEmptyString(raw['description'], 'description', errors);
813
+ if (description !== undefined && description.trim().length < 10) {
814
+ errors.push(formatRecipeError('INVALID_RECIPE', 'description must be a real sentence, not a placeholder'));
815
+ }
816
+ const tool = parseToolIdentity(raw['tool'], errors, options);
817
+ // --- inputs -----------------------------------------------------------------
818
+ const rawInputs = raw['inputs'];
819
+ const inputs = {};
820
+ if (!isPlainObject(rawInputs)) {
821
+ errors.push(formatRecipeError('INVALID_RECIPE', `inputs must be an object keyed by input name; received ${describeKind(rawInputs)}`));
822
+ }
823
+ else if (Object.keys(rawInputs).length === 0) {
824
+ errors.push(formatRecipeError('INVALID_RECIPE', 'inputs must declare at least one input. A tool that binds nothing is a constant, and a constant does not need '
825
+ + 'a provider call to produce it'));
826
+ }
827
+ else {
828
+ for (const [name, declaration] of Object.entries(rawInputs)) {
829
+ const parsed = parseInputDeclaration(name, declaration, errors);
830
+ if (parsed !== undefined)
831
+ inputs[name] = parsed;
832
+ }
833
+ }
834
+ // --- imageInputs ------------------------------------------------------------
835
+ let imageInputMax = 0;
836
+ const rawImageInputs = raw['imageInputs'];
837
+ if (!isPlainObject(rawImageInputs)) {
838
+ errors.push(formatRecipeError('INVALID_RECIPE', `imageInputs must be an object carrying "max"; received ${describeKind(rawImageInputs)}`));
839
+ }
840
+ else {
841
+ for (const key of unknownKeys(rawImageInputs, IMAGE_INPUTS_KEYS)) {
842
+ errors.push(unknownKeyError('imageInputs', key, IMAGE_INPUTS_KEYS));
843
+ }
844
+ const max = readInteger(rawImageInputs['max'], 'imageInputs.max', errors);
845
+ if (max !== undefined) {
846
+ if (max < 0) {
847
+ errors.push(formatRecipeError('INVALID_RECIPE', 'imageInputs.max must not be negative'));
848
+ }
849
+ else if (max > INITIAL_MAX_IMAGE_INPUTS) {
850
+ errors.push(formatRecipeError('INVALID_RECIPE', `imageInputs.max is ${max}, over the ${INITIAL_MAX_IMAGE_INPUTS} that RES-10's initial per-leaf maximum `
851
+ + 'permits for one invocation. A reviewed profile may lower this; a recipe may not raise it'));
852
+ }
853
+ else {
854
+ imageInputMax = max;
855
+ }
856
+ }
857
+ }
858
+ const declaredImages = Object.entries(inputs).filter(([, declaration]) => declaration.type === 'image');
859
+ if (declaredImages.length > 0 && imageInputMax === 0) {
860
+ errors.push(formatRecipeError('INVALID_RECIPE', `the recipe declares ${declaredImages.length} image input(s) (${declaredImages.map(([name]) => name).join(', ')}) `
861
+ + 'but imageInputs.max is 0, so no image could ever be bound'));
862
+ }
863
+ if (declaredImages.length > imageInputMax) {
864
+ errors.push(formatRecipeError('INVALID_RECIPE', `the recipe declares ${declaredImages.length} image inputs but permits imageInputs.max ${imageInputMax}`));
865
+ }
866
+ // --- promptTemplate ---------------------------------------------------------
867
+ const promptTemplate = readNonEmptyString(raw['promptTemplate'], 'promptTemplate', errors);
868
+ if (promptTemplate !== undefined) {
869
+ /**
870
+ * The template is static instructions with bindings and nothing else.
871
+ *
872
+ * The check is deliberately two-sided: a placeholder must name a declared input
873
+ * (an unbound pointer cannot be satisfied, RCP-03), and template CODE must not
874
+ * appear at all, because RCP-02 admits no expressions, `eval`, shell, dynamic
875
+ * imports or remote prompt downloads.
876
+ */
877
+ const names = new Set(Object.keys(inputs));
878
+ const bound = new Set();
879
+ for (const match of promptTemplate.matchAll(/\{\{([^{}]*)\}\}/g)) {
880
+ const name = (match[1] ?? '').trim();
881
+ bound.add(name);
882
+ if (!names.has(name)) {
883
+ errors.push(formatRecipeError('INVALID_RECIPE', `promptTemplate binds {{${name}}}, which is not a declared input. The declared inputs are: `
884
+ + `${names.size === 0 ? '(none)' : [...names].join(', ')}. A binding that names nothing is an unbound `
885
+ + 'pointer and can never be satisfied (RCP-03)'));
886
+ }
887
+ }
888
+ if (![...names].every(name => bound.has(name))) {
889
+ const unused = [...names].filter(name => !bound.has(name));
890
+ errors.push(formatRecipeError('INVALID_RECIPE', `promptTemplate never binds ${unused.join(', ')}, so those inputs would be validated and then silently `
891
+ + 'dropped. Either bind them or remove them from the recipe'));
892
+ }
893
+ const leftover = promptTemplate.replace(/\{\{[^{}]*\}\}/g, '');
894
+ if (leftover.includes('{{') || leftover.includes('}}')) {
895
+ errors.push(formatRecipeError('INVALID_RECIPE', 'promptTemplate contains an unbalanced or nested brace; bindings are written exactly `{{name}}`'));
896
+ }
897
+ for (const syntax of ['${', '<%', '%>']) {
898
+ if (promptTemplate.includes(syntax)) {
899
+ errors.push(formatRecipeError('INVALID_RECIPE', `promptTemplate contains the template expression ${JSON.stringify(syntax)}. A recipe is static data: `
900
+ + 'RCP-02 admits no expressions, eval, shell, dynamic imports or remote prompt downloads'));
901
+ }
902
+ }
903
+ }
904
+ // --- maxOutputTokens --------------------------------------------------------
905
+ let maxOutputTokens = 0;
906
+ const rawMaxOutputTokens = readInteger(raw['maxOutputTokens'], 'maxOutputTokens', errors);
907
+ if (rawMaxOutputTokens !== undefined) {
908
+ if (rawMaxOutputTokens < 1) {
909
+ errors.push(formatRecipeError('INVALID_RECIPE', 'maxOutputTokens must be at least 1'));
910
+ }
911
+ else if (rawMaxOutputTokens > INITIAL_MAX_OUTPUT_TOKENS) {
912
+ errors.push(formatRecipeError('INVALID_RECIPE', `maxOutputTokens is ${rawMaxOutputTokens}, over the ${INITIAL_MAX_OUTPUT_TOKENS} that RES-10's initial `
913
+ + 'per-leaf maximum permits. A reviewed profile may lower this; a recipe may not raise it'));
914
+ }
915
+ else {
916
+ maxOutputTokens = rawMaxOutputTokens;
917
+ }
918
+ }
919
+ // --- policy -----------------------------------------------------------------
920
+ let record = true;
921
+ let allowRemote = true;
922
+ const rawPolicy = raw['policy'];
923
+ if (!isPlainObject(rawPolicy)) {
924
+ errors.push(formatRecipeError('INVALID_RECIPE', `policy must be an object carrying "record" and "allowRemote"; received ${describeKind(rawPolicy)}`));
925
+ }
926
+ else {
927
+ for (const key of unknownKeys(rawPolicy, POLICY_KEYS))
928
+ errors.push(unknownKeyError('policy', key, POLICY_KEYS));
929
+ for (const key of POLICY_KEYS) {
930
+ const value = rawPolicy[key];
931
+ if (typeof value !== 'boolean') {
932
+ errors.push(formatRecipeError('INVALID_RECIPE', `policy.${key} must be a boolean; a policy that is absent or ambiguous is one the host must guess at, and `
933
+ + 'guessing at recording is how a request goes unrecorded (HLM-09)'));
934
+ }
935
+ }
936
+ if (typeof rawPolicy['record'] === 'boolean')
937
+ record = rawPolicy['record'];
938
+ if (typeof rawPolicy['allowRemote'] === 'boolean')
939
+ allowRemote = rawPolicy['allowRemote'];
940
+ }
941
+ if (errors.length > 0 || tool === undefined || promptTemplate === undefined) {
942
+ return { ok: false, errors: errors.length > 0 ? errors : [formatRecipeError('INVALID_RECIPE', 'the recipe is incomplete')] };
943
+ }
944
+ return {
945
+ ok: true,
946
+ recipe: deepFreeze({
947
+ id: id ?? '',
948
+ version: version ?? '',
949
+ description: description ?? '',
950
+ tool,
951
+ inputs,
952
+ promptTemplate,
953
+ maxOutputTokens,
954
+ imageInputs: { max: imageInputMax },
955
+ policy: { record, allowRemote },
956
+ }),
957
+ };
958
+ }
959
+ /**
960
+ * Serialize a frozen recipe as the CANONICAL RCP-02 document.
961
+ *
962
+ * The output is what pack/create emit and what the schema's canonical branch
963
+ * validates: `schemaVersion`/`kind`/`modelSelection`/`instructions`/
964
+ * `inputBindings`/`response`/`limits`/`context` plus the identity keys
965
+ * (id/version/description/tool). The richer legacy constraint fields are
966
+ * re-emitted on the bindings, so canonical → parse → canonical is LOSSLESS.
967
+ * `modelSelection.strategy` defaults to `caller-model` with the derived
968
+ * requires; an `approved-profile` selection is preserved verbatim.
969
+ *
970
+ * @param recipe - the frozen recipe.
971
+ * @returns the canonical document.
972
+ */
973
+ export function toCanonicalRecipe(recipe) {
974
+ const declaredImage = Object.values(recipe.inputs).some(declaration => declaration.type === 'image');
975
+ return {
976
+ schemaVersion: '1.0.0',
977
+ kind: 'harness-model',
978
+ id: recipe.id,
979
+ version: recipe.version,
980
+ description: recipe.description,
981
+ tool: { name: recipe.tool.name, contractId: recipe.tool.contractId },
982
+ modelSelection: recipe.modelSelection ?? {
983
+ strategy: 'caller-model',
984
+ requires: {
985
+ inputModalities: declaredImage ? ['text', 'image'] : ['text'],
986
+ outputModalities: ['text'],
987
+ },
988
+ },
989
+ instructions: recipe.promptTemplate,
990
+ inputBindings: Object.entries(recipe.inputs).map(([name, declaration]) => ({
991
+ kind: declaration.type === 'image' ? 'image' : declaration.jsonSerialized === true ? 'json' : 'text',
992
+ pointer: `/${name}`,
993
+ required: declaration.required,
994
+ ...declaration.description === undefined ? {} : { label: declaration.description },
995
+ ...declaration.minLength === undefined ? {} : { minLength: declaration.minLength },
996
+ ...declaration.maxLength === undefined ? {} : { maxLength: declaration.maxLength },
997
+ ...declaration.minimum === undefined ? {} : { minimum: declaration.minimum },
998
+ ...declaration.maximum === undefined ? {} : { maximum: declaration.maximum },
999
+ ...declaration.enum === undefined ? {} : { enum: declaration.enum },
1000
+ ...declaration.default === undefined ? {} : { default: declaration.default },
1001
+ ...declaration.mediaTypes === undefined ? {} : { mediaTypes: declaration.mediaTypes },
1002
+ ...declaration.maxBytes === undefined ? {} : { maxBytes: declaration.maxBytes },
1003
+ })),
1004
+ ...recipe.response === undefined ? {} : { response: recipe.response },
1005
+ limits: { maxOutputTokens: recipe.maxOutputTokens, maxImages: recipe.imageInputs.max },
1006
+ context: { inheritConversation: false, includeTools: false },
1007
+ };
1008
+ }
1009
+ /** Validate and read the tool identity, including the identity-claim refusal. */
1010
+ function parseToolIdentity(raw, errors, options) {
1011
+ if (!isPlainObject(raw)) {
1012
+ errors.push(formatRecipeError('INVALID_RECIPE', `tool must be an object; received ${describeKind(raw)}`));
1013
+ return undefined;
1014
+ }
1015
+ for (const key of unknownKeys(raw, TOOL_KEYS))
1016
+ errors.push(unknownKeyError('tool', key, TOOL_KEYS));
1017
+ const name = readNonEmptyString(raw['name'], 'tool.name', errors);
1018
+ if (name !== undefined && !/^[a-z][a-z0-9_]*$/.test(name)) {
1019
+ errors.push(formatRecipeError('INVALID_RECIPE', `tool.name ${JSON.stringify(name)} must be lower snake_case`));
1020
+ }
1021
+ const contractId = readNonEmptyString(raw['contractId'], 'tool.contractId', errors);
1022
+ if (contractId !== undefined && !CONTRACT_IDENTITY.test(contractId)) {
1023
+ errors.push(formatRecipeError('INVALID_RECIPE', `tool.contractId ${JSON.stringify(contractId)} must be a dotted lower-case identity (for example "text.title") or `
1024
+ + 'a scoped third-party identity (for example "@acme/tool-mode-image-review/review")'));
1025
+ }
1026
+ /**
1027
+ * A recipe implements the tool it is loaded as, and no other.
1028
+ *
1029
+ * Without this check a recipe file could be swapped for another tool's recipe and
1030
+ * inherit that tool's authorization while running different instructions — the recipe
1031
+ * generation and the contract identity would silently disagree (RCP-06).
1032
+ */
1033
+ if (name !== undefined && options.expectedToolName !== undefined && name !== options.expectedToolName) {
1034
+ errors.push(formatRecipeError('INVALID_RECIPE', `the recipe names the tool "${name}" but it is being loaded as the implementation of "${options.expectedToolName}". `
1035
+ + 'A recipe cannot claim another tool\'s identity: the authorization, the contract and the limits all belong to '
1036
+ + 'the tool it declares (RCP-06)'));
1037
+ }
1038
+ if (contractId !== undefined && options.expectedContractId !== undefined && contractId !== options.expectedContractId) {
1039
+ errors.push(formatRecipeError('INVALID_RECIPE', `the recipe names the contract "${contractId}" but it is being loaded as the implementation of `
1040
+ + `"${options.expectedContractId}"`));
1041
+ }
1042
+ if (name === undefined || contractId === undefined)
1043
+ return undefined;
1044
+ return { name, contractId };
1045
+ }
1046
+ /**
1047
+ * Bind caller arguments to a recipe's declared inputs.
1048
+ *
1049
+ * Every problem is reported, not the first: an author fixing a binding one field per run
1050
+ * is an author who ships the field they gave up on. Nothing is coerced — a string "3" is
1051
+ * not the integer 3, because a silently coerced binding is a binding the caller did not
1052
+ * supply.
1053
+ * @param recipe - the recipe whose inputs are being bound.
1054
+ * @param args - the caller's arguments.
1055
+ * @returns the bound values, or every reason they do not satisfy the recipe.
1056
+ */
1057
+ export function validateRecipeInputs(recipe, args) {
1058
+ if (!isPlainObject(args)) {
1059
+ return {
1060
+ ok: false,
1061
+ errors: [formatRecipeError('RECIPE_BINDING_ERROR', `the arguments for ${recipe.tool.name} must be an object; received ${describeKind(args)}`)],
1062
+ };
1063
+ }
1064
+ const errors = [];
1065
+ const declaredNames = Object.keys(recipe.inputs);
1066
+ for (const supplied of Object.keys(args)) {
1067
+ if (!Object.prototype.hasOwnProperty.call(recipe.inputs, supplied)) {
1068
+ errors.push(formatRecipeError('RECIPE_BINDING_ERROR', `"${supplied}" is not an input of ${recipe.tool.name}. The declared inputs are: ${declaredNames.join(', ')}. `
1069
+ + 'A recipe\'s inputs are closed, so a caller cannot add one — and cannot widen a limit through the argument '
1070
+ + 'object (RCP-05)'));
1071
+ }
1072
+ }
1073
+ const values = {};
1074
+ for (const [name, declaration] of Object.entries(recipe.inputs)) {
1075
+ const supplied = Object.prototype.hasOwnProperty.call(args, name) ? args[name] : undefined;
1076
+ if (supplied === undefined) {
1077
+ if (declaration.default !== undefined) {
1078
+ values[name] = declaration.default;
1079
+ }
1080
+ else if (declaration.required) {
1081
+ errors.push(formatRecipeError('RECIPE_BINDING_ERROR', `the required input "${name}" was not supplied`));
1082
+ }
1083
+ // An omitted optional input is OMITTED, never stringified: RCP-03 requires that a
1084
+ // missing optional binding drop that user content rather than contribute the text
1085
+ // "undefined" to the prompt.
1086
+ continue;
1087
+ }
1088
+ const bound = bindValue(name, declaration, supplied, errors);
1089
+ if (bound !== undefined)
1090
+ values[name] = bound;
1091
+ }
1092
+ /**
1093
+ * There is no image-count check here, and that is deliberate rather than missing.
1094
+ *
1095
+ * The inputs are a CLOSED name-keyed map, so the most images a caller can bind is the
1096
+ * number of image inputs the recipe declares — and parseRecipe already refuses a recipe
1097
+ * declaring more image inputs than its own `imageInputs.max` permits. A check here would
1098
+ * be unreachable, and an unreachable budget check is worse than none: it reads as
1099
+ * enforcement. The RUNTIME ceiling, which a narrowed host policy can lower below the
1100
+ * recipe's figure, is enforced in the broker where it can actually differ.
1101
+ */
1102
+ return errors.length > 0 ? { ok: false, errors } : { ok: true, values };
1103
+ }
1104
+ /**
1105
+ * Interpret a provider's raw reply under a recipe's declared response mode (RCP-04).
1106
+ *
1107
+ * `text` maps the whole reply into the one named payload string field. `json`
1108
+ * accepts exactly one JSON object containing exactly the named field — a code
1109
+ * fence, prose, a missing field or an extra field is an error, not a value to
1110
+ * unwrap or silently drop. A recipe with no declared response keeps the legacy
1111
+ * raw-string result.
1112
+ * @param recipe - the recipe whose mode applies.
1113
+ * @param text - the provider's reply text.
1114
+ * @returns the mapped payload, or a named invalid-output error.
1115
+ */
1116
+ export function interpretRecipeResponse(recipe, text) {
1117
+ if (recipe.response === undefined)
1118
+ return { ok: true, value: text };
1119
+ const { mode, field } = recipe.response;
1120
+ if (mode === 'text')
1121
+ return { ok: true, value: { [field]: text } };
1122
+ const trimmed = text.trim();
1123
+ if (trimmed.startsWith('```')) {
1124
+ return { ok: false, code: 'INVALID_MODEL_OUTPUT', reason: 'the response is a code-fenced document; a code fence is refused rather than unwrapped' };
1125
+ }
1126
+ let parsed;
1127
+ try {
1128
+ parsed = JSON.parse(trimmed);
1129
+ }
1130
+ catch {
1131
+ return { ok: false, code: 'INVALID_MODEL_OUTPUT', reason: 'the response is not a single JSON object' };
1132
+ }
1133
+ if (!isPlainObject(parsed)) {
1134
+ return { ok: false, code: 'INVALID_MODEL_OUTPUT', reason: `the response is ${describeKind(parsed)}, not a JSON object` };
1135
+ }
1136
+ const keys = Object.keys(parsed);
1137
+ const extras = keys.filter(key => key !== field);
1138
+ if (extras.length > 0) {
1139
+ return {
1140
+ ok: false,
1141
+ code: 'INVALID_MODEL_OUTPUT',
1142
+ reason: `the response carries fields the recipe did not ask for (${extras.join(', ')}); silent field dropping is refused`,
1143
+ };
1144
+ }
1145
+ if (!Object.prototype.hasOwnProperty.call(parsed, field)) {
1146
+ return { ok: false, code: 'INVALID_MODEL_OUTPUT', reason: `the response does not carry the declared field "${field}"` };
1147
+ }
1148
+ return { ok: true, value: { [field]: parsed[field] } };
1149
+ }
1150
+ /** Bind one supplied value against its declaration. */
1151
+ function bindValue(name, declaration, supplied, errors) {
1152
+ const path = `"${name}"`;
1153
+ if (declaration.type === 'image')
1154
+ return bindImage(name, declaration, supplied, errors);
1155
+ // A canonical `inputBindings` kind "json" binding (RCP-03): the caller
1156
+ // supplies a data-only object or array, which is JSON-serialized into the
1157
+ // bound string. Serialization is data-only — never evaluated as instructions
1158
+ // or code.
1159
+ if (declaration.jsonSerialized === true) {
1160
+ if (!isPlainObject(supplied) && !Array.isArray(supplied)) {
1161
+ errors.push(formatRecipeError('RECIPE_BINDING_ERROR', `${path} must be a JSON-serializable object or array (the binding kind is "json"); received ${describeKind(supplied)}`));
1162
+ return undefined;
1163
+ }
1164
+ const serializationErrors = [];
1165
+ checkDataOnly(supplied, path, serializationErrors);
1166
+ const serialized = JSON.stringify(supplied);
1167
+ if (serialized.length > MAX_JSON_BINDING_BYTES) {
1168
+ serializationErrors.push(formatRecipeError('RECIPE_BINDING_ERROR', `${path} serializes to ${serialized.length} characters, over the ${MAX_JSON_BINDING_BYTES}-character binding bound `
1169
+ + '(RCP-03)'));
1170
+ }
1171
+ for (const error of serializationErrors)
1172
+ errors.push(error);
1173
+ return serializationErrors.length === 0 ? serialized : undefined;
1174
+ }
1175
+ const scalar = typeof supplied === 'string' || typeof supplied === 'number' || typeof supplied === 'boolean';
1176
+ if (!scalar) {
1177
+ errors.push(formatRecipeError('RECIPE_BINDING_ERROR', `${path} must be a ${declaration.type}; received ${describeKind(supplied)}`));
1178
+ return undefined;
1179
+ }
1180
+ if (!matchesScalarType(declaration.type, supplied)) {
1181
+ errors.push(formatRecipeError('RECIPE_BINDING_ERROR', `${path} must be a ${declaration.type}; received ${describeKind(supplied)}. Values are not coerced, because a `
1182
+ + 'coerced binding is a binding the caller did not supply'));
1183
+ return undefined;
1184
+ }
1185
+ if (declaration.enum !== undefined && !declaration.enum.includes(supplied)) {
1186
+ errors.push(formatRecipeError('RECIPE_BINDING_ERROR', `${path} must be one of ${declaration.enum.map(member => JSON.stringify(member)).join(', ')}; received `
1187
+ + `${JSON.stringify(supplied)}`));
1188
+ return undefined;
1189
+ }
1190
+ if (typeof supplied === 'string') {
1191
+ if (declaration.minLength !== undefined && supplied.length < declaration.minLength) {
1192
+ errors.push(formatRecipeError('RECIPE_BINDING_ERROR', `${path} is ${supplied.length} characters, under the declared minLength ${declaration.minLength}`));
1193
+ return undefined;
1194
+ }
1195
+ if (declaration.maxLength !== undefined && supplied.length > declaration.maxLength) {
1196
+ errors.push(formatRecipeError('RECIPE_BINDING_ERROR', `${path} is ${supplied.length} characters, over the declared maxLength ${declaration.maxLength}. The binding is `
1197
+ + 'refused rather than truncated, because a truncated input is not the input the caller supplied'));
1198
+ return undefined;
1199
+ }
1200
+ }
1201
+ if (typeof supplied === 'number') {
1202
+ if (declaration.minimum !== undefined && supplied < declaration.minimum) {
1203
+ errors.push(formatRecipeError('RECIPE_BINDING_ERROR', `${path} is under the declared minimum ${declaration.minimum}`));
1204
+ return undefined;
1205
+ }
1206
+ if (declaration.maximum !== undefined && supplied > declaration.maximum) {
1207
+ errors.push(formatRecipeError('RECIPE_BINDING_ERROR', `${path} is over the declared maximum ${declaration.maximum}`));
1208
+ return undefined;
1209
+ }
1210
+ }
1211
+ return supplied;
1212
+ }
1213
+ /** Bind one image, which is the media type plus the bytes. */
1214
+ /**
1215
+ * Read the dimensions of the claimed format, and refuse a mismatch (HLM-11).
1216
+ *
1217
+ * A media type is a CLAIM, not evidence. Sending a JPEG labelled `image/png` to a
1218
+ * provider that trusts the label produces a decode failure mid-call at best, and a
1219
+ * fabricated description at worst. The header is the evidence available before
1220
+ * dispatch; a format this function cannot read is refused rather than guessed.
1221
+ * @param mediaType - the declared media type.
1222
+ * @param bytes - the actual bytes.
1223
+ * @returns a structural problem, or undefined when the header is consistent.
1224
+ */
1225
+ function imageProblem(mediaType, bytes) {
1226
+ if (!SUPPORTED_IMAGE_MEDIA_TYPES.includes(mediaType)) {
1227
+ return `media type ${JSON.stringify(mediaType)} has no validated decoder in this recipe format`;
1228
+ }
1229
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1230
+ if (mediaType === 'image/png') {
1231
+ const signature = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
1232
+ if (bytes.length < 24 || signature.some((byte, index) => bytes[index] !== byte)) {
1233
+ return 'the bytes are not a PNG (signature mismatch)';
1234
+ }
1235
+ if (String.fromCharCode(bytes[12] ?? 0, bytes[13] ?? 0, bytes[14] ?? 0, bytes[15] ?? 0) !== 'IHDR') {
1236
+ return 'the PNG has no IHDR header';
1237
+ }
1238
+ return pixelProblem(view.getUint32(16), view.getUint32(20));
1239
+ }
1240
+ if (mediaType === 'image/jpeg') {
1241
+ if (bytes.length < 4 || bytes[0] !== 0xFF || bytes[1] !== 0xD8) {
1242
+ return 'the bytes are not a JPEG (SOI marker missing)';
1243
+ }
1244
+ let offset = 2;
1245
+ while (offset + 9 < bytes.length) {
1246
+ if (bytes[offset] !== 0xFF)
1247
+ return 'the JPEG marker chain is corrupt';
1248
+ const marker = bytes[offset + 1] ?? 0;
1249
+ if (marker === 0xD8 || marker === 0xD9) {
1250
+ offset += 2;
1251
+ continue;
1252
+ }
1253
+ const length = view.getUint16(offset + 2);
1254
+ if (length < 2)
1255
+ return 'the JPEG segment length is invalid';
1256
+ if ((marker >= 0xC0 && marker <= 0xC3)) {
1257
+ return pixelProblem(view.getUint16(offset + 7), view.getUint16(offset + 5));
1258
+ }
1259
+ offset += 2 + length;
1260
+ }
1261
+ return 'the JPEG has no frame header';
1262
+ }
1263
+ // WebP: RIFF....WEBP plus a VP8X extended header carries canvas dimensions.
1264
+ if (bytes.length < 30
1265
+ || String.fromCharCode(bytes[0] ?? 0, bytes[1] ?? 0, bytes[2] ?? 0, bytes[3] ?? 0) !== 'RIFF'
1266
+ || String.fromCharCode(bytes[8] ?? 0, bytes[9] ?? 0, bytes[10] ?? 0, bytes[11] ?? 0) !== 'WEBP') {
1267
+ return 'the bytes are not a WebP (RIFF/WEBP mismatch)';
1268
+ }
1269
+ if (String.fromCharCode(bytes[12] ?? 0, bytes[13] ?? 0, bytes[14] ?? 0, bytes[15] ?? 0) !== 'VP8X') {
1270
+ return 'the WebP is not the extended VP8X form, whose dimensions can be checked';
1271
+ }
1272
+ const width = 1 + (bytes[24] ?? 0) + ((bytes[25] ?? 0) << 8) + ((bytes[26] ?? 0) << 16);
1273
+ const height = 1 + (bytes[27] ?? 0) + ((bytes[28] ?? 0) << 8) + ((bytes[29] ?? 0) << 16);
1274
+ return pixelProblem(width, height);
1275
+ }
1276
+ /** Bound one decoded dimension pair against the pixel ceiling. */
1277
+ function pixelProblem(width, height) {
1278
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
1279
+ return `the image has invalid dimensions ${width}x${height}`;
1280
+ }
1281
+ if (width * height > MAX_IMAGE_PIXELS) {
1282
+ return `the image is ${width}x${height} (${width * height} pixels), over the ${MAX_IMAGE_PIXELS}-pixel ceiling`;
1283
+ }
1284
+ return undefined;
1285
+ }
1286
+ function bindImage(name, declaration, supplied, errors) {
1287
+ const path = `"${name}"`;
1288
+ if (!isPlainObject(supplied)) {
1289
+ errors.push(formatRecipeError('RECIPE_BINDING_ERROR', `${path} must be a typed image ({ mediaType, bytes }); received ${describeKind(supplied)}. A path or a URL is not `
1290
+ + 'an image binding: the request must be fully described by the recipe and the bound inputs (HLM-05)'));
1291
+ return undefined;
1292
+ }
1293
+ for (const key of unknownKeys(supplied, ['mediaType', 'bytes'])) {
1294
+ errors.push(formatRecipeError('RECIPE_BINDING_ERROR', `${path}.${key} is not part of a typed image binding`));
1295
+ }
1296
+ const mediaType = supplied['mediaType'];
1297
+ if (typeof mediaType !== 'string' || !mediaType.startsWith('image/')) {
1298
+ errors.push(formatRecipeError('UNSUPPORTED_MODALITY', `${path}.mediaType must be an image media type (for example "image/png"); received ${JSON.stringify(mediaType)}. `
1299
+ + 'A non-image media type bound to an image input would be sent as an image the provider cannot read'));
1300
+ return undefined;
1301
+ }
1302
+ if (declaration.mediaTypes !== undefined && !declaration.mediaTypes.includes(mediaType)) {
1303
+ errors.push(formatRecipeError('UNSUPPORTED_MODALITY', `${path}.mediaType ${JSON.stringify(mediaType)} is not among the admitted types ${declaration.mediaTypes.join(', ')}`));
1304
+ return undefined;
1305
+ }
1306
+ const bytes = supplied['bytes'];
1307
+ if (!(bytes instanceof Uint8Array)) {
1308
+ errors.push(formatRecipeError('RECIPE_BINDING_ERROR', `${path}.bytes must be a Uint8Array of the image's actual bytes; received ${describeKind(bytes)}`));
1309
+ return undefined;
1310
+ }
1311
+ if (bytes.length === 0) {
1312
+ errors.push(formatRecipeError('RECIPE_BINDING_ERROR', `${path}.bytes is empty; an empty buffer is not an image`));
1313
+ return undefined;
1314
+ }
1315
+ if (declaration.maxBytes !== undefined && bytes.length > declaration.maxBytes) {
1316
+ errors.push(formatRecipeError('MODEL_BUDGET_EXCEEDED', `${path}.bytes is ${bytes.length} bytes, over the declared maxBytes ${declaration.maxBytes}`));
1317
+ return undefined;
1318
+ }
1319
+ const problem = imageProblem(mediaType, bytes);
1320
+ if (problem !== undefined) {
1321
+ const code = problem.includes('no validated decoder')
1322
+ ? 'UNSUPPORTED_MODALITY'
1323
+ : problem.includes('ceiling') ? 'MODEL_BUDGET_EXCEEDED' : 'RECIPE_BINDING_ERROR';
1324
+ errors.push(formatRecipeError(code, `${path} ${JSON.stringify(mediaType)} ${problem}`));
1325
+ return undefined;
1326
+ }
1327
+ /**
1328
+ * The bytes are COPIED, not aliased.
1329
+ *
1330
+ * HLM-09 requires model requests to use immutable captured inputs. Handing the
1331
+ * provider a buffer the caller still holds would let a mutation between dispatch and
1332
+ * dispatch change what the model received, which is exactly the kind of evidence gap
1333
+ * an auxiliary-call journal cannot describe after the fact.
1334
+ */
1335
+ return { mediaType, bytes: Uint8Array.from(bytes) };
1336
+ }
1337
+ /**
1338
+ * Render the prompt from validated bindings (RCP-02, RCP-03).
1339
+ *
1340
+ * A line that binds an absent optional input is DROPPED, whole. RCP-03 says a missing
1341
+ * optional focus omits that user content rather than stringifying `undefined`, and in a
1342
+ * flat string the only way to omit a labelled section is to omit its line.
1343
+ *
1344
+ * A bound image contributes a bounded REFERENCE — its media type and byte count — never
1345
+ * its bytes: MOD-06 forbids concatenating raw byte data into text.
1346
+ * @param recipe - the recipe to render.
1347
+ * @param values - bindings from {@link validateRecipeInputs}.
1348
+ * @returns the prompt text.
1349
+ * @throws Error when a required binding has no value, which means the caller skipped
1350
+ * validation; the broker always validates first.
1351
+ */
1352
+ export function renderPrompt(recipe, values) {
1353
+ const rendered = [];
1354
+ for (const line of recipe.promptTemplate.split('\n')) {
1355
+ const names = placeholdersIn(line);
1356
+ if (names.length === 0) {
1357
+ rendered.push(line);
1358
+ continue;
1359
+ }
1360
+ let omit = false;
1361
+ let text = line;
1362
+ for (const name of names) {
1363
+ const declaration = recipe.inputs[name];
1364
+ if (declaration === undefined) {
1365
+ // Unreachable for a parsed recipe; parseRecipe refuses an unbound placeholder.
1366
+ throw new Error(`the prompt binds {{${name}}}, which is not a declared input of ${recipe.tool.name}`);
1367
+ }
1368
+ const value = values[name];
1369
+ if (value === undefined) {
1370
+ if (declaration.required) {
1371
+ throw new Error(`renderPrompt requires validated bindings: the required input "${name}" has no value. Call `
1372
+ + 'validateRecipeInputs first; the broker always does');
1373
+ }
1374
+ omit = true;
1375
+ continue;
1376
+ }
1377
+ const substitute = declaration.type === 'image' ? imageReference(value) : String(value);
1378
+ text = text.split(`{{${name}}}`).join(substitute);
1379
+ }
1380
+ if (!omit)
1381
+ rendered.push(text);
1382
+ }
1383
+ return rendered.join('\n');
1384
+ }
1385
+ /**
1386
+ * The textual reference a bound image contributes to a prompt (HLM-09, MOD-06).
1387
+ * @param image - the bound image.
1388
+ * @returns a bounded reference naming the media type and size.
1389
+ */
1390
+ export function imageReference(image) {
1391
+ return `[image ${image.mediaType}, ${image.bytes.length} bytes]`;
1392
+ }
1393
+ /**
1394
+ * Project a recipe's input declarations into a JSON Schema (DECL-01).
1395
+ *
1396
+ * This is what makes one declaration serve three surfaces: the tool contract, the DSH
1397
+ * parameter projection and the MCP schema all read this object, so there is no second
1398
+ * description of the inputs to drift from the recipe. The schema is closed, because an
1399
+ * open object would advertise that a caller may add inputs the recipe does not have.
1400
+ * @param recipe - the recipe to project.
1401
+ * @returns a JSON Schema object for the tool's arguments.
1402
+ */
1403
+ export function inputSchemaFor(recipe) {
1404
+ const properties = {};
1405
+ const required = [];
1406
+ for (const [name, declaration] of Object.entries(recipe.inputs)) {
1407
+ const property = declaration.type === 'image'
1408
+ // An image binding is an object carrying its media type and bytes; the schema says
1409
+ // which media types are admitted, so a caller learns the bound from the contract.
1410
+ ? {
1411
+ type: 'object',
1412
+ additionalProperties: false,
1413
+ required: ['mediaType', 'bytes'],
1414
+ properties: {
1415
+ mediaType: { type: 'string', ...(declaration.mediaTypes === undefined ? {} : { enum: declaration.mediaTypes }) },
1416
+ bytes: { type: 'string', contentEncoding: 'base64', description: 'The image bytes.' },
1417
+ },
1418
+ }
1419
+ : { type: declaration.type };
1420
+ if (declaration.description !== undefined)
1421
+ property['description'] = declaration.description;
1422
+ if (declaration.minLength !== undefined)
1423
+ property['minLength'] = declaration.minLength;
1424
+ if (declaration.maxLength !== undefined)
1425
+ property['maxLength'] = declaration.maxLength;
1426
+ if (declaration.minimum !== undefined)
1427
+ property['minimum'] = declaration.minimum;
1428
+ if (declaration.maximum !== undefined)
1429
+ property['maximum'] = declaration.maximum;
1430
+ if (declaration.enum !== undefined)
1431
+ property['enum'] = [...declaration.enum];
1432
+ if (declaration.default !== undefined)
1433
+ property['default'] = declaration.default;
1434
+ properties[name] = property;
1435
+ if (declaration.required)
1436
+ required.push(name);
1437
+ }
1438
+ return { type: 'object', additionalProperties: false, required, properties };
1439
+ }
1440
+ /**
1441
+ * Describe a recipe and its actual executor possibilities (RCP-10).
1442
+ *
1443
+ * The description states what the invocation will really do — the effective token
1444
+ * ceiling after narrowing, whether a remote destination is possible at all, and that no
1445
+ * parent context is supplied — because discovery that describes the recipe's wishes
1446
+ * rather than the host's limits tells a caller it can do something it cannot.
1447
+ * @param recipe - the recipe to describe.
1448
+ * @param policy - the host policy in force.
1449
+ * @returns a bounded, honest description.
1450
+ */
1451
+ export function describeRecipe(recipe, policy) {
1452
+ const images = Object.entries(recipe.inputs).filter(([, declaration]) => declaration.type === 'image').length;
1453
+ const ceiling = Math.min(recipe.maxOutputTokens, policy.outputTokens);
1454
+ const imageCeiling = Math.min(recipe.imageInputs.max, policy.imageInputs, INITIAL_MAX_IMAGE_INPUTS);
1455
+ return [
1456
+ `${recipe.tool.name} (contract ${recipe.tool.contractId}) — ${recipe.description}`,
1457
+ `Recipe ${recipe.id}@${recipe.version}; ${Object.keys(recipe.inputs).length} declared input(s), ${images} image input(s).`,
1458
+ `Limits in force: at most ${ceiling} output tokens (recipe ${recipe.maxOutputTokens}, host ${policy.outputTokens}), `
1459
+ + `at most ${imageCeiling} image input(s) (recipe ${recipe.imageInputs.max}, host ${policy.imageInputs}).`,
1460
+ `Destinations: ${policy.remoteDestinations ? (recipe.policy.allowRemote ? 'local or remote' : 'local only, by the recipe') : 'local only, by the host policy'}.`,
1461
+ `Supplied context: the recipe instructions and the validated bound inputs only — no parent conversation, system `
1462
+ + `prompt, tools, skills, attachments or memory (HLM-05). Recording: ${recipe.policy.record ? 'on' : 'off by the recipe'}.`,
1463
+ ].join(' ');
1464
+ }
1465
+ //# sourceMappingURL=recipe-format.js.map