firedrill-run 2.2.5 → 3.0.0

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 (392) hide show
  1. package/README.md +178 -22
  2. package/bin/firedrill.mjs +29 -5
  3. package/dist/assembly.json +13 -0
  4. package/dist/cli/runtime/candidate_copy.py +395 -0
  5. package/dist/cli/runtime/source_snapshot.py +391 -0
  6. package/dist/cli/src/administration.js +295 -0
  7. package/dist/cli/src/agent-changes.js +496 -0
  8. package/dist/cli/src/assessment-requests.js +67 -0
  9. package/dist/cli/src/auth.js +465 -0
  10. package/dist/cli/src/billing-commerce-contracts.js +1 -0
  11. package/dist/cli/src/billing-usage.js +227 -0
  12. package/dist/cli/src/billing-wallet-contracts.js +1 -0
  13. package/dist/cli/src/billing.js +170 -0
  14. package/dist/cli/src/bootstrap-process-lock.js +136 -0
  15. package/dist/cli/src/browser-links.js +14 -0
  16. package/dist/cli/src/campaign-controls.js +122 -0
  17. package/dist/cli/src/campaign-diagnoses.js +118 -0
  18. package/dist/cli/src/campaign-reports.js +1215 -0
  19. package/dist/cli/src/campaign-world.js +156 -0
  20. package/dist/cli/src/candidate-copy.js +376 -0
  21. package/dist/cli/src/catalog-contracts.js +1 -0
  22. package/dist/cli/src/ci-access-contracts.js +1 -0
  23. package/dist/cli/src/ci-configuration-contracts.js +1 -0
  24. package/dist/cli/src/ci-coverage.js +64 -0
  25. package/dist/cli/src/ci-delivery-contracts.js +1 -0
  26. package/dist/cli/src/ci-result-contracts.js +1 -0
  27. package/dist/cli/src/ci-run-contracts.js +1 -0
  28. package/dist/cli/src/ci-setup-proposals.js +52 -0
  29. package/dist/cli/src/company-policies.js +229 -0
  30. package/dist/cli/src/completion.js +79 -0
  31. package/dist/cli/src/configuration-intents.js +277 -0
  32. package/dist/cli/src/configuration.js +369 -0
  33. package/dist/cli/src/continuous-simulation.js +277 -0
  34. package/dist/cli/src/coordinated-checkpoints.js +133 -0
  35. package/dist/cli/src/credential-mode.js +139 -0
  36. package/dist/cli/src/credentials.js +331 -0
  37. package/dist/cli/src/credit-format.js +8 -0
  38. package/dist/cli/src/dataset-contracts.js +1 -0
  39. package/dist/cli/src/dataset-export.js +154 -0
  40. package/dist/cli/src/dataset-reads.js +37 -0
  41. package/dist/cli/src/dependency-preparation.js +239 -0
  42. package/dist/cli/src/director-tool-setup.js +104 -0
  43. package/dist/cli/src/driver-review.js +225 -0
  44. package/dist/cli/src/evaluation-authoring.js +315 -0
  45. package/dist/cli/src/evidence-bundles.js +367 -0
  46. package/dist/cli/src/execution-proposals.js +36 -0
  47. package/dist/cli/src/http.js +302 -0
  48. package/dist/cli/src/local-container-boundary.js +38 -0
  49. package/dist/cli/src/local-container-policy.js +201 -0
  50. package/dist/cli/src/local-execution-setup.js +81 -0
  51. package/dist/cli/src/local-source-basis.js +73 -0
  52. package/dist/cli/src/local-source.js +66 -0
  53. package/dist/cli/src/local-value-store.js +91 -0
  54. package/dist/cli/src/local-values.js +163 -0
  55. package/dist/cli/src/login-organizations.js +70 -0
  56. package/dist/cli/src/main.js +1622 -0
  57. package/dist/cli/src/mcp-connection.js +109 -0
  58. package/dist/cli/src/native/bindings.js +137 -0
  59. package/dist/cli/src/native/canonical.js +370 -0
  60. package/dist/cli/src/native/cloud-resources.js +942 -0
  61. package/dist/cli/src/native/companion-channel.js +421 -0
  62. package/dist/cli/src/native/companion-ipc.js +473 -0
  63. package/dist/cli/src/native/contracts.js +218 -0
  64. package/dist/cli/src/native/control-plane.js +1354 -0
  65. package/dist/cli/src/native/credit-approval.js +15 -0
  66. package/dist/cli/src/native/dependency-proxy.js +1006 -0
  67. package/dist/cli/src/native/drill-cleanup.js +115 -0
  68. package/dist/cli/src/native/drill-runner.js +1562 -0
  69. package/dist/cli/src/native/drivers/action-trace.js +210 -0
  70. package/dist/cli/src/native/drivers/function-wrapper.js +152 -0
  71. package/dist/cli/src/native/drivers/http.js +442 -0
  72. package/dist/cli/src/native/drivers/index.js +47 -0
  73. package/dist/cli/src/native/drivers/jsonl-process.js +538 -0
  74. package/dist/cli/src/native/drivers/playwright.js +600 -0
  75. package/dist/cli/src/native/drivers/signed-event.js +244 -0
  76. package/dist/cli/src/native/drivers/types.js +578 -0
  77. package/dist/cli/src/native/evidence-filter.js +255 -0
  78. package/dist/cli/src/native/isolated-access.js +241 -0
  79. package/dist/cli/src/native/library-sharing.js +220 -0
  80. package/dist/cli/src/native/local-run.js +667 -0
  81. package/dist/cli/src/native/local-secrets.js +19 -0
  82. package/dist/cli/src/native/mcp-config.js +123 -0
  83. package/dist/cli/src/native/metadata-requests.js +120 -0
  84. package/dist/cli/src/native/observations.js +1159 -0
  85. package/dist/cli/src/native/owned-groups.js +46 -0
  86. package/dist/cli/src/native/plan.js +594 -0
  87. package/dist/cli/src/native/png.js +159 -0
  88. package/dist/cli/src/native/ports.js +43 -0
  89. package/dist/cli/src/native/readiness.js +139 -0
  90. package/dist/cli/src/native/reassessments.js +206 -0
  91. package/dist/cli/src/native/redaction.js +235 -0
  92. package/dist/cli/src/native/registration.js +155 -0
  93. package/dist/cli/src/native/roots.js +110 -0
  94. package/dist/cli/src/native/run-cleanup.js +199 -0
  95. package/dist/cli/src/native/scratch.js +117 -0
  96. package/dist/cli/src/native/screenshots.js +60 -0
  97. package/dist/cli/src/native/session.js +556 -0
  98. package/dist/cli/src/native/standalone-proxy.js +580 -0
  99. package/dist/cli/src/native/standalone-renewals.js +183 -0
  100. package/dist/cli/src/native/standalone-state.js +193 -0
  101. package/dist/cli/src/native/standalone-tools.js +187 -0
  102. package/dist/cli/src/native/supervisor.js +831 -0
  103. package/dist/cli/src/native/target-auth.js +161 -0
  104. package/dist/cli/src/native/target-state.js +330 -0
  105. package/dist/cli/src/native/tool-reuse.js +364 -0
  106. package/dist/cli/src/native/tool-setups.js +336 -0
  107. package/dist/cli/src/native/world-reuse.js +288 -0
  108. package/dist/cli/src/onboarding.js +23 -0
  109. package/dist/cli/src/priority-requests.js +70 -0
  110. package/dist/cli/src/priority.js +223 -0
  111. package/dist/cli/src/prompts.js +184 -0
  112. package/dist/cli/src/real-operations.js +221 -0
  113. package/dist/cli/src/recipe-intents.js +58 -0
  114. package/dist/cli/src/recorded-context-intents.js +59 -0
  115. package/dist/cli/src/recorded-context.js +248 -0
  116. package/dist/cli/src/recordings.js +69 -0
  117. package/dist/cli/src/registration-guide.js +39 -0
  118. package/dist/cli/src/report-exports.js +213 -0
  119. package/dist/cli/src/request-input.js +47 -0
  120. package/dist/cli/src/resource-lifecycle.js +141 -0
  121. package/dist/cli/src/revision-requests.js +55 -0
  122. package/dist/cli/src/runner-abandon.js +53 -0
  123. package/dist/cli/src/runner-campaign-controls.js +262 -0
  124. package/dist/cli/src/runner-claim-reads.js +36 -0
  125. package/dist/cli/src/runner-completion.js +65 -0
  126. package/dist/cli/src/runner-configuration.js +546 -0
  127. package/dist/cli/src/runner-container.js +794 -0
  128. package/dist/cli/src/runner-evidence-erasure.js +351 -0
  129. package/dist/cli/src/runner-evidence-privacy.js +37 -0
  130. package/dist/cli/src/runner-evidence-retention.js +161 -0
  131. package/dist/cli/src/runner-evidence.js +326 -0
  132. package/dist/cli/src/runner-execute.js +92 -0
  133. package/dist/cli/src/runner-execution-deadline.js +25 -0
  134. package/dist/cli/src/runner-grants.js +307 -0
  135. package/dist/cli/src/runner-identity.js +615 -0
  136. package/dist/cli/src/runner-inputs.js +308 -0
  137. package/dist/cli/src/runner-jobs.js +49 -0
  138. package/dist/cli/src/runner-model-transport.js +392 -0
  139. package/dist/cli/src/runner-ownership.js +332 -0
  140. package/dist/cli/src/runner-process-lock.js +175 -0
  141. package/dist/cli/src/runner-runtime.js +396 -0
  142. package/dist/cli/src/runner-session-channel.js +377 -0
  143. package/dist/cli/src/runner-trial-completion.js +34 -0
  144. package/dist/cli/src/runner-trial-journal.js +358 -0
  145. package/dist/cli/src/runner-trial-live.js +393 -0
  146. package/dist/cli/src/runner-trial-protocol.js +289 -0
  147. package/dist/cli/src/runner-trial-sync.js +110 -0
  148. package/dist/cli/src/runner-trial-transfer.js +107 -0
  149. package/dist/cli/src/runner-voice.js +40 -0
  150. package/dist/cli/src/runner-world-claim.js +21 -0
  151. package/dist/cli/src/runner-world-collector.js +159 -0
  152. package/dist/cli/src/runner-world-command-evidence.js +487 -0
  153. package/dist/cli/src/runner-world-commands.js +268 -0
  154. package/dist/cli/src/runner-world-completion.js +76 -0
  155. package/dist/cli/src/runner-world-erasure.js +95 -0
  156. package/dist/cli/src/runner-world-evidence-sync.js +91 -0
  157. package/dist/cli/src/runner-world-journal.js +510 -0
  158. package/dist/cli/src/runner-world-native-filters.js +134 -0
  159. package/dist/cli/src/runner-world-observations.js +50 -0
  160. package/dist/cli/src/runner-world-session-protocol.js +134 -0
  161. package/dist/cli/src/saved-records.js +40 -0
  162. package/dist/cli/src/scenario-authoring.js +212 -0
  163. package/dist/cli/src/scenario-discovery.js +216 -0
  164. package/dist/cli/src/scenario-paths.js +68 -0
  165. package/dist/cli/src/scenario-requests.js +4 -0
  166. package/dist/cli/src/service-receipts.js +105 -0
  167. package/dist/cli/src/simple/authoring.js +270 -0
  168. package/dist/cli/src/simple/credit-approval.js +16 -0
  169. package/dist/cli/src/simple/help.js +98 -0
  170. package/dist/cli/src/simple/init.js +61 -0
  171. package/dist/cli/src/simple/library-sharing.js +414 -0
  172. package/dist/cli/src/simple/metadata-requests.js +46 -0
  173. package/dist/cli/src/simple/native-report.js +841 -0
  174. package/dist/cli/src/simple/native-tools.js +780 -0
  175. package/dist/cli/src/simple/plan-apply.js +506 -0
  176. package/dist/cli/src/simple/reassessment.js +323 -0
  177. package/dist/cli/src/simple/register.js +123 -0
  178. package/dist/cli/src/simple/run.js +122 -0
  179. package/dist/cli/src/simple/status.js +49 -0
  180. package/dist/cli/src/simple/tool-renewal.js +308 -0
  181. package/dist/cli/src/simple/tool-reuse.js +526 -0
  182. package/dist/cli/src/simple/tool-start-catalog.js +100 -0
  183. package/dist/cli/src/simple/tool-state.js +426 -0
  184. package/dist/cli/src/simple/tools-author.js +222 -0
  185. package/dist/cli/src/simple/watch.js +28 -0
  186. package/dist/cli/src/simple/world-reuse.js +586 -0
  187. package/dist/cli/src/simulation-intents.js +406 -0
  188. package/dist/cli/src/simulation-run.js +1058 -0
  189. package/dist/cli/src/source-batch.js +322 -0
  190. package/dist/cli/src/source-connection.js +451 -0
  191. package/dist/cli/src/source-runtime.js +346 -0
  192. package/dist/cli/src/source-selection-intents.js +75 -0
  193. package/dist/cli/src/source-sets.js +214 -0
  194. package/dist/cli/src/target-models.js +109 -0
  195. package/dist/cli/src/tool-catalog.js +224 -0
  196. package/dist/cli/src/tool-command-contracts.js +1 -0
  197. package/dist/cli/src/tool-command-export.js +245 -0
  198. package/dist/cli/src/tool-connection-contracts.js +1 -0
  199. package/dist/cli/src/tool-connection-reads.js +23 -0
  200. package/dist/cli/src/tool-contributions.js +58 -0
  201. package/dist/cli/src/tool-data.js +116 -0
  202. package/dist/cli/src/tool-datasets.js +176 -0
  203. package/dist/cli/src/tool-group-contracts.js +1 -0
  204. package/dist/cli/src/tool-group-reads.js +26 -0
  205. package/dist/cli/src/tool-group-state.js +147 -0
  206. package/dist/cli/src/tool-groups.js +311 -0
  207. package/dist/cli/src/tool-instances.js +186 -0
  208. package/dist/cli/src/tool-lifecycle-contracts.js +1 -0
  209. package/dist/cli/src/tool-lifecycle-reads.js +30 -0
  210. package/dist/cli/src/tool-lifecycle.js +323 -0
  211. package/dist/cli/src/tool-package-contracts.js +1 -0
  212. package/dist/cli/src/tool-package-reads.js +41 -0
  213. package/dist/cli/src/tool-records.js +53 -0
  214. package/dist/cli/src/tool-runtime.js +96 -0
  215. package/dist/cli/src/tool-state-contracts.js +1 -0
  216. package/dist/cli/src/tool-state-reads.js +31 -0
  217. package/dist/cli/src/tools.js +306 -0
  218. package/dist/cli/src/trial-actions.js +93 -0
  219. package/dist/cli/src/trial-checkpoints.js +133 -0
  220. package/dist/cli/src/trial-checks.js +70 -0
  221. package/dist/cli/src/trial-state.js +197 -0
  222. package/dist/cli/src/vault.js +266 -0
  223. package/dist/cli/src/workspace-registration.js +383 -0
  224. package/dist/cli/src/workspace-team-contracts.js +1 -0
  225. package/dist/cli/src/world-access.js +231 -0
  226. package/dist/cli/src/world-agent.js +4 -0
  227. package/dist/cli/src/world-checkpoint-scopes.js +15 -0
  228. package/dist/cli/src/world-contracts.js +6 -0
  229. package/dist/cli/src/world-creation.js +201 -0
  230. package/dist/cli/src/world-datasets.js +207 -0
  231. package/dist/cli/src/world-inspection.js +257 -0
  232. package/dist/cli/src/world-recipes.js +103 -0
  233. package/dist/cli/src/world-requests.js +241 -0
  234. package/dist/cli/src/world-session-checkpoints.js +196 -0
  235. package/dist/cli/src/world-session-lifecycle.js +207 -0
  236. package/dist/cli/src/world-session-requests.js +215 -0
  237. package/dist/cli/src/world-session-state.js +145 -0
  238. package/dist/cli/src/world-session-voice.js +66 -0
  239. package/dist/cli/src/world-sessions.js +288 -0
  240. package/dist/cli/src/world-stream.js +99 -0
  241. package/dist/cli/src/world-time-report.js +58 -0
  242. package/dist/contracts/agent-change-contracts.js +75 -0
  243. package/dist/contracts/agent-execution-contracts.js +38 -0
  244. package/dist/contracts/application-state-contracts.js +26 -0
  245. package/dist/contracts/assessment-aggregate-contracts.js +15 -0
  246. package/dist/contracts/assessment-contracts.js +74 -0
  247. package/dist/contracts/billing-commerce-contracts.js +107 -0
  248. package/dist/contracts/billing-wallet-contracts.js +43 -0
  249. package/dist/contracts/campaign-lifecycle.js +1 -0
  250. package/dist/contracts/campaign-world-contracts.js +80 -0
  251. package/dist/contracts/catalog-contracts.js +73 -0
  252. package/dist/contracts/ci-access-contracts.js +68 -0
  253. package/dist/contracts/ci-configuration-contracts.js +88 -0
  254. package/dist/contracts/ci-coverage-contracts.js +65 -0
  255. package/dist/contracts/ci-coverage-labels.js +25 -0
  256. package/dist/contracts/ci-delivery-contracts.js +61 -0
  257. package/dist/contracts/ci-result-contracts.js +103 -0
  258. package/dist/contracts/ci-run-contracts.js +51 -0
  259. package/dist/contracts/ci-setup-contracts.js +88 -0
  260. package/dist/contracts/clip-contracts.js +32 -0
  261. package/dist/contracts/comparison-contracts.js +53 -0
  262. package/dist/contracts/composite-contracts.js +34 -0
  263. package/dist/contracts/context-contracts.js +76 -0
  264. package/dist/contracts/continuation-report-contracts.js +1 -0
  265. package/dist/contracts/continuity-requirements.js +8 -0
  266. package/dist/contracts/coordinated-checkpoint-contracts.js +34 -0
  267. package/dist/contracts/dataset-contracts.js +88 -0
  268. package/dist/contracts/dependency-decision-contracts.js +36 -0
  269. package/dist/contracts/dependency-protocol-contracts.js +43 -0
  270. package/dist/contracts/director-execution.js +51 -0
  271. package/dist/contracts/driver-review-contracts.js +99 -0
  272. package/dist/contracts/evidence-contracts.js +65 -0
  273. package/dist/contracts/evidence-coverage-contracts.js +10 -0
  274. package/dist/contracts/export-contracts.js +44 -0
  275. package/dist/contracts/github-directory-contracts.js +96 -0
  276. package/dist/contracts/insight-contracts.js +11 -0
  277. package/dist/contracts/inspection-contracts.js +1 -0
  278. package/dist/contracts/local-claim-contracts.js +39 -0
  279. package/dist/contracts/local-configuration-contracts.js +7 -0
  280. package/dist/contracts/local-evidence-contracts.js +32 -0
  281. package/dist/contracts/local-grant-contracts.js +83 -0
  282. package/dist/contracts/local-input-contracts.js +19 -0
  283. package/dist/contracts/local-runner-contracts.js +42 -0
  284. package/dist/contracts/local-trial-live-contracts.js +16 -0
  285. package/dist/contracts/local-value-source-contracts.js +92 -0
  286. package/dist/contracts/local-world-native-contracts.js +8 -0
  287. package/dist/contracts/mcp-contracts.js +42 -0
  288. package/dist/contracts/media-contracts.js +28 -0
  289. package/dist/contracts/native-check-contracts.js +31 -0
  290. package/dist/contracts/native-identity-contracts.js +52 -0
  291. package/dist/contracts/onboarding-contracts.js +32 -0
  292. package/dist/contracts/performance-contracts.js +85 -0
  293. package/dist/contracts/planning-contracts.js +81 -0
  294. package/dist/contracts/real-operation-contracts.js +30 -0
  295. package/dist/contracts/recommendation-contracts.js +54 -0
  296. package/dist/contracts/report-contracts.js +1 -0
  297. package/dist/contracts/resource-lifecycle-contracts.js +57 -0
  298. package/dist/contracts/revision-contracts.js +12 -0
  299. package/dist/contracts/revision-request-contracts.js +39 -0
  300. package/dist/contracts/saved-record-contracts.js +76 -0
  301. package/dist/contracts/scenario-draft-comparison.js +91 -0
  302. package/dist/contracts/scenario-world-contracts.js +14 -0
  303. package/dist/contracts/service-contracts.js +1 -0
  304. package/dist/contracts/session-command-contracts.js +56 -0
  305. package/dist/contracts/session-contracts.js +65 -0
  306. package/dist/contracts/session-state-contracts.js +67 -0
  307. package/dist/contracts/session-voice-contracts.js +45 -0
  308. package/dist/contracts/source-set-contracts.js +26 -0
  309. package/dist/contracts/source-upload-contracts.js +22 -0
  310. package/dist/contracts/tool-command-contracts.js +57 -0
  311. package/dist/contracts/tool-connection-contracts.js +87 -0
  312. package/dist/contracts/tool-contribution-contracts.js +35 -0
  313. package/dist/contracts/tool-group-contracts.js +53 -0
  314. package/dist/contracts/tool-group-state-contracts.js +63 -0
  315. package/dist/contracts/tool-lifecycle-contracts.js +94 -0
  316. package/dist/contracts/tool-package-contracts.js +91 -0
  317. package/dist/contracts/tool-protocol-contracts.js +71 -0
  318. package/dist/contracts/tool-runtime-billing-contracts.js +28 -0
  319. package/dist/contracts/tool-setup-proposal.js +59 -0
  320. package/dist/contracts/tool-state-contracts.js +79 -0
  321. package/dist/contracts/trial-state-contracts.js +30 -0
  322. package/dist/contracts/typescript/agent-change.js +1 -0
  323. package/dist/contracts/typescript/assessment-aggregate.js +1 -0
  324. package/dist/contracts/typescript/assessment.js +1 -0
  325. package/dist/contracts/typescript/billing-commerce.js +1 -0
  326. package/dist/contracts/typescript/campaigns.js +1 -0
  327. package/dist/contracts/typescript/catalog.js +1 -0
  328. package/dist/contracts/typescript/ci-access.js +1 -0
  329. package/dist/contracts/typescript/ci-configuration.js +1 -0
  330. package/dist/contracts/typescript/ci-delivery.js +1 -0
  331. package/dist/contracts/typescript/ci-result.js +1 -0
  332. package/dist/contracts/typescript/ci-runs.js +1 -0
  333. package/dist/contracts/typescript/clips.js +1 -0
  334. package/dist/contracts/typescript/comparison.js +1 -0
  335. package/dist/contracts/typescript/composite.js +1 -0
  336. package/dist/contracts/typescript/connections.js +1 -0
  337. package/dist/contracts/typescript/context.js +1 -0
  338. package/dist/contracts/typescript/datasets.js +1 -0
  339. package/dist/contracts/typescript/evidence-coverage.js +1 -0
  340. package/dist/contracts/typescript/export.js +1 -0
  341. package/dist/contracts/typescript/github-directory.js +1 -0
  342. package/dist/contracts/typescript/insight.js +1 -0
  343. package/dist/contracts/typescript/inspection.js +1 -0
  344. package/dist/contracts/typescript/intake.js +1 -0
  345. package/dist/contracts/typescript/local-claim.js +1 -0
  346. package/dist/contracts/typescript/local-configuration.js +1 -0
  347. package/dist/contracts/typescript/local-evidence.js +1 -0
  348. package/dist/contracts/typescript/local-grant.js +1 -0
  349. package/dist/contracts/typescript/local-input.js +1 -0
  350. package/dist/contracts/typescript/local-runner.js +1 -0
  351. package/dist/contracts/typescript/local-trial-live.js +1 -0
  352. package/dist/contracts/typescript/local-value-source.js +1 -0
  353. package/dist/contracts/typescript/local-world-native.js +1 -0
  354. package/dist/contracts/typescript/mcp.js +1 -0
  355. package/dist/contracts/typescript/media.js +1 -0
  356. package/dist/contracts/typescript/native-check.js +1 -0
  357. package/dist/contracts/typescript/native-identity.js +1 -0
  358. package/dist/contracts/typescript/onboarding.js +1 -0
  359. package/dist/contracts/typescript/performance.js +1 -0
  360. package/dist/contracts/typescript/planning.js +1 -0
  361. package/dist/contracts/typescript/preparation.js +1 -0
  362. package/dist/contracts/typescript/real-operation.js +1 -0
  363. package/dist/contracts/typescript/recommendation.js +1 -0
  364. package/dist/contracts/typescript/reports.js +1 -0
  365. package/dist/contracts/typescript/resource-lifecycle.js +1 -0
  366. package/dist/contracts/typescript/saved-record.js +1 -0
  367. package/dist/contracts/typescript/scenarios.js +1 -0
  368. package/dist/contracts/typescript/services.js +1 -0
  369. package/dist/contracts/typescript/session-command.js +1 -0
  370. package/dist/contracts/typescript/session-state.js +1 -0
  371. package/dist/contracts/typescript/session-voice.js +1 -0
  372. package/dist/contracts/typescript/session.js +1 -0
  373. package/dist/contracts/typescript/tool-command.js +1 -0
  374. package/dist/contracts/typescript/tool-connection.js +1 -0
  375. package/dist/contracts/typescript/tool-contribution.js +1 -0
  376. package/dist/contracts/typescript/tool-groups.js +1 -0
  377. package/dist/contracts/typescript/tool-lifecycle.js +1 -0
  378. package/dist/contracts/typescript/tool-package.js +1 -0
  379. package/dist/contracts/typescript/tool-state.js +1 -0
  380. package/dist/contracts/typescript/trial-state.js +1 -0
  381. package/dist/contracts/typescript/vault.js +1 -0
  382. package/dist/contracts/typescript/workspace-team.js +1 -0
  383. package/dist/contracts/typescript/world-stream.js +1 -0
  384. package/dist/contracts/workspace-team-contracts.js +55 -0
  385. package/dist/contracts/world-command.js +65 -0
  386. package/dist/contracts/world-lifetime-contracts.js +74 -0
  387. package/dist/contracts/world-recipe-contracts.js +27 -0
  388. package/dist/contracts/world-stream-contracts.js +27 -0
  389. package/dist/package.json +1 -0
  390. package/package.json +16 -16
  391. package/LICENSE.md +0 -23
  392. package/PACKAGING.json +0 -50
@@ -0,0 +1,426 @@
1
+ // `firedrill tools state <instance-id> ...`: the data of one running shared-pool
2
+ // tool instance. Save a checkpoint of it, or change it through one of the
3
+ // instance's own declared operations, under a request key printed before
4
+ // anything is sent. A lost answer is read back by that key; when root has no
5
+ // record of it yet the request stays unknown, and the human either replays the
6
+ // same key or withdraws it explicitly. Withdrawing never undoes anything.
7
+ // Snapshots (the baseline and each checkpoint) are listed here. A reset (from
8
+ // the baseline) or fork (from any snapshot) is a new one-tool session: root
9
+ // reviews it under a printed request key, quotes it, and admits it only with
10
+ // the exact estimate the human approved. Nothing is admitted automatically.
11
+ import { randomUUID } from 'node:crypto';
12
+ import { approveCredits } from './credit-approval.js';
13
+ import { CliError } from '../http.js';
14
+ import { selectOne, terminalText } from '../prompts.js';
15
+ import { requestInput } from '../request-input.js';
16
+ import { errorCode, isTransient, jitteredInterval, retryTransient, sleep } from '../native/control-plane.js';
17
+ import { getStateOperation, listStateOperations, listStateSnapshots, recoverCloneRequest, recoverStateOperation, requestClone, requestStateOperation, stateId, stateMatches, withdrawStateRequest, STATE_OPEN, } from '../native/standalone-state.js';
18
+ import { admitStandaloneBatch, getStandaloneBatch, getStandaloneInstance, quoteStandaloneBatch, STANDALONE_ACTIVE, } from '../native/standalone-tools.js';
19
+ import { amount } from './plan-apply.js';
20
+ const ARGUMENTS_LIMIT = 64 * 1024;
21
+ const FOLLOW_MS = 2_000;
22
+ const FOLLOW_LIMIT_MS = 2 * 60_000;
23
+ const RECOVER_MS = 15_000;
24
+ /** Root's session length bounds (one minute to one year). */
25
+ const CLONE_MIN_SECONDS = 60;
26
+ const CLONE_MAX_SECONDS = 31_536_000;
27
+ /** An estimate this close to expiry is not approved: admission would race it. */
28
+ const QUOTE_MARGIN_MS = 60_000;
29
+ const STATE_TEXT = {
30
+ pending: 'waiting to run',
31
+ effect_pending: 'running',
32
+ unknown: 'outcome not confirmed yet (Firedrill is confirming it; do not send it again under a new key)',
33
+ completed: 'completed',
34
+ cancelled: 'cancelled before it ran',
35
+ };
36
+ const tty = (options) => !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
37
+ function pageNumber(value) {
38
+ if (value === undefined)
39
+ return undefined;
40
+ if (!/^[1-9][0-9]{0,6}$/.test(value))
41
+ throw new CliError('arguments_invalid', 'Use --page with a positive page number.', 2);
42
+ return Number(value);
43
+ }
44
+ function operationText(row) {
45
+ const what = row.action === 'checkpoint' ? 'checkpoint' : `edit ${terminalText(row.operation ?? '')}`;
46
+ const result = row.result;
47
+ const detail = !result ? ''
48
+ : row.action === 'checkpoint' ? ` (snapshot ${result.snapshot_id})`
49
+ : ` (${[result.before_revision !== undefined && result.after_revision !== undefined ? `revision ${result.before_revision} -> ${result.after_revision}` : null,
50
+ result.outcome ? terminalText(result.outcome) : null, typeof result.status === 'number' ? `status ${result.status}` : null].filter(Boolean).join(', ')})`;
51
+ return `${row.id} ${what} ${STATE_TEXT[row.state] ?? terminalText(row.state)}${detail} ${row.created_at} request ${row.request_key}`;
52
+ }
53
+ function batchText(batch) {
54
+ return `Tool session ${batch.id} (${terminalText(batch.state)}), request ${batch.request_key}: `
55
+ + batch.instances.map((one) => `${terminalText(one.name)} instance ${one.id}`).join(', ');
56
+ }
57
+ /** The instance must be a shared-pool instance of this workspace; isolated tools keep their own state. */
58
+ async function instanceOf(scope, instanceId, signal) {
59
+ try {
60
+ return await getStandaloneInstance(scope, { instanceId, signal });
61
+ }
62
+ catch (error) {
63
+ if (error instanceof CliError && error.httpStatus === 404) {
64
+ throw new CliError('tool_instance_not_found', 'No shared-pool tool instance with that id is in this workspace. State operations apply to shared-pool '
65
+ + 'instances (firedrill tools session <session-id> lists them); an isolated tool keeps its own state. Nothing was sent.', 2);
66
+ }
67
+ throw error;
68
+ }
69
+ }
70
+ const withdrawnError = (requestKey) => new CliError('native_tool_request_withdrawn', `Request ${requestKey} was withdrawn and can never run. Start a new request without --request-key.`, 2);
71
+ /** A printed key given back, checked before anything is printed or sent; otherwise a new one. */
72
+ const keyOf = (value) => value === undefined ? randomUUID() : stateId(value, 'request key');
73
+ async function consent(question, detail, retry, options) {
74
+ let approved = options.approve === true;
75
+ if (!approved && tty(options)) {
76
+ approved = await selectOne(question, [
77
+ { value: false, label: 'Not now', detail: 'Nothing is sent.' },
78
+ { value: true, label: 'Yes', detail },
79
+ ], options.signal);
80
+ }
81
+ if (!approved)
82
+ throw new CliError('tool_change_consent_required', `Nothing was sent. After the human approves: ${retry} --approve-tool-change`, 2);
83
+ }
84
+ /** Read an accepted operation back until root settles it, for a bounded time. */
85
+ async function follow(scope, row, signal) {
86
+ const deadline = Date.now() + FOLLOW_LIMIT_MS;
87
+ let current = row;
88
+ while (STATE_OPEN.has(current.state) && Date.now() < deadline) {
89
+ if (!await sleep(jitteredInterval(FOLLOW_MS), signal))
90
+ break;
91
+ const operationId = current.id;
92
+ current = await retryTransient(() => getStateOperation(scope, { instanceId: row.instance_id, operationId, signal }), { signal, attempts: 4 });
93
+ }
94
+ return current;
95
+ }
96
+ /** `tools state <instance> checkpoint|edit ...`: one request under a printed key, replayed (never repeated) with --request-key. */
97
+ async function send(scope, instance, request, command, options, emit) {
98
+ const requestKey = keyOf(options.request);
99
+ // An edit reaches here only after the human approved it, so its replay keeps that approval (a checkpoint takes none);
100
+ // a replay without it would stop for consent and print a retry the human might rerun under a new key.
101
+ const replay = `${command} --request-key ${requestKey}${request.action === 'edit' ? ' --approve-tool-change' : ''}`;
102
+ const read = `firedrill tools state ${instance.id} request ${requestKey}`;
103
+ const withdraw = `firedrill tools state ${instance.id} withdraw-request ${requestKey}`;
104
+ emit('tool_state_request', { instance_id: instance.id, request_key: requestKey, action: request.action,
105
+ operation: request.action === 'edit' ? request.operation : null }, `State request: ${requestKey}\nIf interrupted, read it with ${read}, `
106
+ + `or replay exactly this request with ${replay}. Do not send it again under a new key.`);
107
+ let row;
108
+ try {
109
+ row = await requestStateOperation(scope, { instanceId: instance.id, requestKey, request, signal: options.signal });
110
+ }
111
+ catch (error) {
112
+ if (errorCode(error) === 'native_tool_request_withdrawn')
113
+ throw withdrawnError(requestKey);
114
+ // Only an unanswered (or cancelled) request may have landed; a refusal or a bad local input is reported as it is.
115
+ if (!options.signal.aborted && !isTransient(error) && errorCode(error) !== 'native_response_unconfirmed')
116
+ throw error;
117
+ const found = await recoverStateOperation(scope, { instanceId: instance.id, requestKey, signal: AbortSignal.timeout(RECOVER_MS) })
118
+ .catch((recovery) => { if (errorCode(recovery) === 'native_tool_request_withdrawn')
119
+ throw withdrawnError(requestKey); return null; });
120
+ if (!found || !stateMatches(found, request)) {
121
+ throw new CliError('tool_state_unconfirmed', `The request was not confirmed. Read it with ${read}; replay it with ${replay}; `
122
+ + `or, if the human decides it must never run, withdraw it with ${withdraw} (which undoes nothing).`);
123
+ }
124
+ row = found;
125
+ }
126
+ const settled = await follow(scope, row, options.signal);
127
+ emit('tool_state_operation', settled, `${operationText(settled)}`
128
+ + (STATE_OPEN.has(settled.state) ? `\nNot settled yet. Check it with ${read}.` : '')
129
+ + (settled.state === 'completed' && settled.action === 'checkpoint' ? `\nSnapshots: firedrill tools state ${instance.id} snapshots` : ''));
130
+ }
131
+ function cloneSeconds(value) {
132
+ if (value === undefined)
133
+ return null;
134
+ if (!/^[1-9][0-9]{0,8}$/.test(value) || Number(value) < CLONE_MIN_SECONDS || Number(value) > CLONE_MAX_SECONDS) {
135
+ throw new CliError('tool_duration_invalid', `Use --seconds with a whole number from ${CLONE_MIN_SECONDS} to ${CLONE_MAX_SECONDS} (one year). Nothing was sent.`, 2);
136
+ }
137
+ return Number(value);
138
+ }
139
+ /** The instance's own baseline snapshot, which a reset starts from. */
140
+ async function baselineOf(scope, instanceId, signal) {
141
+ {
142
+ const snapshots = await listStateSnapshots(scope, { instanceId, page: 1, kind: 'baseline', signal });
143
+ const baseline = snapshots.results.find((one) => one.kind === 'baseline');
144
+ if (baseline) {
145
+ if (!baseline.available)
146
+ throw new CliError('tool_snapshot_unavailable', 'This instance\'s baseline is no longer retained, so it cannot be reset. Nothing was sent.', 2);
147
+ return baseline.id;
148
+ }
149
+ }
150
+ throw new CliError('tool_snapshot_unavailable', `No baseline snapshot was found for this instance. List them with firedrill tools state ${instanceId} snapshots. Nothing was sent.`, 2);
151
+ }
152
+ const seconds = (value) => value % 86_400 === 0 ? `${value / 86_400} day${value === 86_400 ? '' : 's'}`
153
+ : value % 3_600 === 0 ? `${value / 3_600} hour${value === 3_600 ? '' : 's'}` : value % 60 === 0 ? `${value / 60} minutes` : `${value} seconds`;
154
+ function cloneQuoteText(quotes) {
155
+ const [first, second] = quotes.quotes;
156
+ const money = (units, one) => amount(units, one.credit_scale, one.denomination);
157
+ const same = first.denomination === second.denomination && first.credit_scale === second.credit_scale;
158
+ const earliest = Date.parse(first.expires_at) <= Date.parse(second.expires_at) ? first.expires_at : second.expires_at;
159
+ return [
160
+ `Cloud-work estimate (maximum, reserved only once approved)${same ? `: ${money(first.maximum_units + second.maximum_units, first)}` : ', every part below:'}`,
161
+ ` shared pool preparation: up to ${money(first.maximum_units, first)}`,
162
+ ` shared pool running time: up to ${money(second.maximum_units, second)}`,
163
+ `Estimate expires: ${terminalText(earliest)}`,
164
+ ].join('\n');
165
+ }
166
+ /**
167
+ * `tools state <instance> reset|fork <snapshot-id> --seconds <n> [--request-key <key>] [--usage-quote <key>]
168
+ * [--approve-tool-change]`: root reviews the reset or fork as a new one-tool session under a printed request
169
+ * key (a rerun with that key continues the original, never a second review), quotes it, and admits it on the
170
+ * shared pool only with the exact estimate the human approved, through the batch's own quote and admit routes.
171
+ */
172
+ async function startClone(scope, instanceId, action, snapshotArg, options, emit) {
173
+ const signal = options.signal;
174
+ const requestKey = keyOf(options.request);
175
+ const wanted = cloneSeconds(options.seconds);
176
+ if (options.quote !== undefined && options.request === undefined) {
177
+ throw new CliError('arguments_invalid', 'Use --usage-quote with the quote key and --request-key that were printed together. Nothing was sent.', 2);
178
+ }
179
+ const quoteGiven = options.quote === undefined ? undefined : stateId(options.quote, 'quote key');
180
+ const snapshotGiven = action === 'fork' ? stateId(snapshotArg, 'snapshot id') : null;
181
+ const recovery = `firedrill tools state ${instanceId} ${action}${snapshotGiven ? ` ${snapshotGiven}` : ''}${wanted ? ` --seconds ${wanted}` : ''} --request-key ${requestKey}`;
182
+ const read = `firedrill tools state ${instanceId} clone-request ${requestKey}`;
183
+ const withdraw = `firedrill tools state ${instanceId} withdraw-clone ${requestKey}`;
184
+ // A reset or fork already reviewed under this key is continued, never reviewed again.
185
+ let batch = options.request === undefined ? null : await recoverCloneRequest(scope, { instanceId, requestKey, signal }).catch((error) => {
186
+ if (errorCode(error) === 'native_tool_request_withdrawn')
187
+ throw withdrawnError(requestKey);
188
+ throw error;
189
+ });
190
+ if (batch && wanted !== null && batch.duration_seconds !== wanted) {
191
+ throw new CliError('idempotency_conflict', `That request was reviewed for ${seconds(batch.duration_seconds)}. Continue it without --seconds, or start a new one without --request-key.`, 2);
192
+ }
193
+ if (!batch) {
194
+ if (wanted === null)
195
+ throw new CliError('tool_duration_required', 'Use --seconds <n> to say how long the new tool session runs. Nothing was sent.', 2);
196
+ const instance = await instanceOf(scope, instanceId, signal);
197
+ const snapshotId = snapshotGiven ?? await baselineOf(scope, instance.id, signal);
198
+ emit('tool_clone_request', { instance_id: instance.id, action, snapshot_id: snapshotId, duration_seconds: wanted, request_key: requestKey }, `${action === 'reset' ? 'Reset' : 'Fork'} request: ${requestKey}\nIf interrupted, continue with ${recovery}. Do not start again under a new key.`);
199
+ try {
200
+ batch = await requestClone(scope, { instanceId: instance.id, requestKey, snapshotId, durationSeconds: wanted, action, signal });
201
+ }
202
+ catch (error) {
203
+ if (errorCode(error) === 'native_tool_request_withdrawn')
204
+ throw withdrawnError(requestKey);
205
+ // Only an unanswered (or cancelled) review may have landed; a refusal or a bad local input is reported as it is.
206
+ if (!signal.aborted && !isTransient(error) && errorCode(error) !== 'native_response_unconfirmed')
207
+ throw error;
208
+ batch = await recoverCloneRequest(scope, { instanceId, requestKey, signal: AbortSignal.timeout(RECOVER_MS) }).catch(() => null);
209
+ if (!batch) {
210
+ throw new CliError('tool_clone_unconfirmed', `The request was not confirmed. Read it with ${read}; continue it with ${recovery}; `
211
+ + `or, if the human decides it must never be reviewed, withdraw it with ${withdraw} (which undoes nothing).`);
212
+ }
213
+ }
214
+ }
215
+ const reviewed = batch;
216
+ if (reviewed.clone_source?.action !== action || reviewed.clone_source.source_instance_id !== instanceId
217
+ || (snapshotGiven !== null && reviewed.clone_source.source_snapshot_id !== snapshotGiven)
218
+ || (wanted !== null && reviewed.duration_seconds !== wanted)) {
219
+ throw new CliError('idempotency_conflict', 'This key belongs to a different original reset or fork. Read its original receipt before approving it. Nothing further was sent.', 2);
220
+ }
221
+ const sessionText = (current) => `${batchText(current)}\nWatch it: firedrill tools session ${current.id}`;
222
+ if (reviewed.state !== 'reviewed' || reviewed.stop_requested_at) {
223
+ // Already approved (or ended): shown as it is, never approved again.
224
+ emit('tool_session', reviewed, sessionText(reviewed));
225
+ return;
226
+ }
227
+ // Quoting reserves nothing. The same quote key replays the same estimate, so an approval given for that
228
+ // key admits exactly what the human saw; a new or different key is a new estimate that needs its own approval.
229
+ const quoteKey = quoteGiven ?? randomUUID();
230
+ const quotes = await retryTransient(() => quoteStandaloneBatch(scope, { batch: reviewed, quoteKey, signal }), { signal, attempts: 3 });
231
+ if (!quotes.quotes.every((one) => Date.parse(one.expires_at) - Date.now() > QUOTE_MARGIN_MS)) {
232
+ throw new CliError('tool_quote_expired', 'This estimate has expired or expires within a minute. Nothing was reserved or started. '
233
+ + `For a fresh estimate: ${recovery}. If the human no longer wants this ${action}, close the unapproved review: `
234
+ + `firedrill tools session ${reviewed.id} stop --approve-tool-change`, 2);
235
+ }
236
+ const notice = quoteGiven !== undefined && !quotes.replayed ? 'No estimate was quoted for this session under that quote key, so this is a new one. ' : '';
237
+ let approved = options.approve === true && (!quotes.quotes.some(quote => quote.credit_approval_required) || (quoteGiven !== undefined && quotes.replayed));
238
+ const interactive = tty(options);
239
+ if (!approved && interactive)
240
+ process.stderr.write(`${batchText(reviewed)}\n`);
241
+ emit('tool_clone_quote', quotes, `${notice}${cloneQuoteText(quotes)}`);
242
+ if (!approved && interactive) {
243
+ approved = await selectOne(`Start this ${action} for ${seconds(reviewed.duration_seconds)}?`, [
244
+ { value: false, label: 'Not now', detail: 'Nothing is reserved or started.' },
245
+ { value: true, label: `Start the ${action}`, detail: 'Reserves up to the estimate above. The tool runs on the shared pool in the cloud, not on this machine.' },
246
+ ], signal);
247
+ }
248
+ if (!approved) {
249
+ throw new CliError('tool_change_consent_required', `${notice}Nothing was reserved or started. Show the human this estimate; after they approve it: `
250
+ + `${recovery} --usage-quote ${quoteKey} --approve-tool-change`, 2);
251
+ }
252
+ const creditApproval = await approveCredits(quotes.quotes, options, interactive, `${recovery} --usage-quote ${quoteKey} --approve-tool-change`);
253
+ let admitted;
254
+ try {
255
+ admitted = await retryTransient(() => admitStandaloneBatch(scope, { quotes, signal, creditApproval }), { signal, attempts: 3 });
256
+ }
257
+ catch (error) {
258
+ if (signal.aborted)
259
+ throw error;
260
+ // An unanswered approval may have landed: the session says, and nothing is admitted twice.
261
+ const current = await getStandaloneBatch(scope, { batchId: quotes.batch_id, signal: AbortSignal.timeout(RECOVER_MS) }).catch(() => null);
262
+ if (!current || current.original_digest !== quotes.batch_digest || current.state === 'reviewed') {
263
+ if (error instanceof CliError && error.httpStatus !== null && !isTransient(error))
264
+ throw error;
265
+ throw new CliError('tool_start_unconfirmed', `The approval was not confirmed. Check firedrill tools session ${quotes.batch_id}; `
266
+ + `if it is still reviewed, run ${recovery} --usage-quote ${quoteKey} --approve-tool-change.`);
267
+ }
268
+ admitted = current;
269
+ }
270
+ emit('tool_session', admitted, sessionText(admitted));
271
+ }
272
+ /** Withdraw a state or reset/fork request after the human's explicit approval; root returns the original instead when it already landed. */
273
+ async function withdraw(scope, instanceId, requestKey, kind, options, emit) {
274
+ const verb = kind === 'state' ? 'withdraw-request' : 'withdraw-clone';
275
+ await consent(`Withdraw request ${requestKey}?`, 'If it has not arrived, it can never run. If it already arrived, it is shown instead; nothing is undone.', `firedrill tools state ${instanceId} ${verb} ${requestKey}`, options);
276
+ const result = await retryTransient(() => withdrawStateRequest(scope, { instanceId, requestKey, kind, signal: options.signal }), { signal: options.signal, attempts: 3 });
277
+ if (result.state === 'withdrawn') {
278
+ emit('tool_state_withdrawal', result, `Request ${requestKey} is withdrawn: it can never run. Nothing else changed.`);
279
+ return;
280
+ }
281
+ const receipt = result.receipt;
282
+ emit('tool_state_withdrawal', result, `Request ${requestKey} had already arrived, so it was not withdrawn and nothing was undone:\n`
283
+ + ('action' in receipt ? operationText(receipt) : batchText(receipt)));
284
+ }
285
+ /**
286
+ * `tools state <instance> [checkpoint | edit <operation> --file <arguments.json> | operation <id> | snapshots
287
+ * | request <key> | withdraw-request <key> | clone-request <key> | withdraw-clone <key>]`.
288
+ */
289
+ export async function toolStateCommand(scope, args, options, emit) {
290
+ const [instanceArg, verb, target, ...rest] = args;
291
+ const usage = 'Use tools state <instance-id> [checkpoint | edit <operation> --file <arguments.json> | operation <operation-id> | snapshots '
292
+ + '| reset --seconds <n> | fork <snapshot-id> --seconds <n> | request <key> | withdraw-request <key> | clone-request <key> | withdraw-clone <key>].';
293
+ if (rest.length)
294
+ throw new CliError('arguments_invalid', usage, 2);
295
+ const instanceId = stateId(instanceArg, 'tool instance id');
296
+ const signal = options.signal;
297
+ const takes = (allowed) => {
298
+ const given = ['file', 'request', 'approve', 'approveCredits', 'page', 'seconds', 'quote']
299
+ .filter((flag) => options[flag] !== undefined && options[flag] !== false && !allowed.includes(flag));
300
+ if (given.length)
301
+ throw new CliError('arguments_invalid', `${usage} --request-key is for checkpoint, edit, reset and fork, --file for edit, `
302
+ + '--seconds and --usage-quote for reset and fork, --approve-tool-change for edit, reset, fork and withdrawals, --page for lists.', 2);
303
+ };
304
+ if (verb === undefined || verb === 'snapshots') {
305
+ takes(['page']);
306
+ if (target !== undefined)
307
+ throw new CliError('arguments_invalid', usage, 2);
308
+ const page = pageNumber(options.page);
309
+ if (verb === 'snapshots') {
310
+ const snapshots = await listStateSnapshots(scope, { instanceId, page, signal });
311
+ emit('tool_state_snapshots', snapshots, [
312
+ `Snapshots of instance ${instanceId}, page ${snapshots.page ?? page ?? 1} (${snapshots.count} in all):`,
313
+ ...snapshots.results.map((one) => ` ${one.id} ${terminalText(one.kind)} ${one.created_at}${one.available ? '' : ' (no longer available)'}`),
314
+ snapshots.results.length ? null : ' none',
315
+ snapshots.next ? `More: --page ${(snapshots.page ?? page ?? 1) + 1}` : null,
316
+ `A new tool session from the baseline: firedrill tools state ${instanceId} reset --seconds <n>`,
317
+ `A new tool session from a snapshot: firedrill tools state ${instanceId} fork <snapshot-id> --seconds <n>`,
318
+ 'Either one is reviewed and quoted first, and starts only after the human approves the estimate; this instance is not changed.',
319
+ ].filter((line) => line !== null).join('\n'));
320
+ return;
321
+ }
322
+ const operations = await listStateOperations(scope, { instanceId, page, signal });
323
+ emit('tool_state_operations', operations, [
324
+ `State operations on instance ${instanceId}, newest first, page ${operations.page ?? page ?? 1} (${operations.count} in all):`,
325
+ ...operations.results.map((one) => ` ${operationText(one)}`),
326
+ operations.results.length ? null : ' none',
327
+ operations.next ? `More: --page ${(operations.page ?? page ?? 1) + 1}` : null,
328
+ `Save a checkpoint: firedrill tools state ${instanceId} checkpoint`,
329
+ `Change its data through one of its operations: firedrill tools state ${instanceId} edit <operation> --file <arguments.json>`,
330
+ ].filter((line) => line !== null).join('\n'));
331
+ return;
332
+ }
333
+ if (verb === 'checkpoint') {
334
+ takes(['request']);
335
+ if (target !== undefined)
336
+ throw new CliError('arguments_invalid', usage, 2);
337
+ const instance = await instanceOf(scope, instanceId, signal);
338
+ if (options.request === undefined && instance.state !== STANDALONE_ACTIVE) {
339
+ throw new CliError('tool_instance_inactive', `This instance is ${terminalText(instance.state)}, not running, so its data cannot be saved now. Nothing was sent.`, 2);
340
+ }
341
+ await send(scope, instance, { action: 'checkpoint' }, `firedrill tools state ${instance.id} checkpoint`, options, emit);
342
+ return;
343
+ }
344
+ if (verb === 'edit') {
345
+ takes(['request', 'file', 'approve']);
346
+ if (!target || target.length > 200)
347
+ throw new CliError('arguments_invalid', `Use tools state ${instanceId} edit <operation> --file <arguments.json>.`, 2);
348
+ if (options.file === undefined)
349
+ throw new CliError('arguments_invalid', 'Use --file <arguments.json> (or - for stdin) with the operation\'s JSON arguments. Nothing was sent.', 2);
350
+ const instance = await instanceOf(scope, instanceId, signal);
351
+ if (!instance.operations.includes(target)) {
352
+ throw new CliError('tool_operation_unknown', `This instance does not declare ${terminalText(target)}. Its operations: ${instance.operations.map(terminalText).join(', ')}. Nothing was sent.`, 2);
353
+ }
354
+ if (options.request === undefined && instance.state !== STANDALONE_ACTIVE) {
355
+ throw new CliError('tool_instance_inactive', `This instance is ${terminalText(instance.state)}, not running, so its data cannot be changed now. Nothing was sent.`, 2);
356
+ }
357
+ let parsed;
358
+ try {
359
+ parsed = JSON.parse(await requestInput(options.file, signal, ARGUMENTS_LIMIT));
360
+ }
361
+ catch (error) {
362
+ if (error instanceof CliError)
363
+ throw error;
364
+ throw new CliError('request_file_invalid', 'The arguments file is not valid JSON. Nothing was sent.', 2);
365
+ }
366
+ const command = `firedrill tools state ${instance.id} edit ${target} --file ${options.file === '-' ? '<the same arguments file>' : options.file}`;
367
+ // A replay's approval retry keeps its original key: without it the rerun would mint a new key and could apply the edit twice.
368
+ const retry = options.request === undefined ? command : `${command} --request-key ${stateId(options.request, 'request key')}`;
369
+ if (!options.approve && tty(options)) {
370
+ process.stderr.write(`${terminalText(instance.name)} (instance ${instance.id}): run ${terminalText(target)} against its data with the arguments in the file.\n`);
371
+ }
372
+ await consent(`Change this tool's data with ${terminalText(target)}?`, 'The tool records the change; it is not undone automatically.', retry, options);
373
+ await send(scope, instance, { action: 'edit', operation: target, arguments: parsed }, command, options, emit);
374
+ return;
375
+ }
376
+ if (verb === 'reset' || verb === 'fork') {
377
+ takes(['request', 'seconds', 'quote', 'approve', 'approveCredits']);
378
+ if (verb === 'reset' && target !== undefined)
379
+ throw new CliError('arguments_invalid', `Use tools state ${instanceId} reset --seconds <n>; a reset always starts from the baseline.`, 2);
380
+ if (verb === 'fork' && target === undefined)
381
+ throw new CliError('arguments_invalid', `Use tools state ${instanceId} fork <snapshot-id> --seconds <n> (snapshot ids from tools state ${instanceId} snapshots).`, 2);
382
+ await startClone(scope, instanceId, verb, target, options, emit);
383
+ return;
384
+ }
385
+ if (verb === 'operation') {
386
+ takes([]);
387
+ const operationId = stateId(target, 'state operation id');
388
+ const row = await getStateOperation(scope, { instanceId, operationId, signal }).catch((error) => {
389
+ if (error instanceof CliError && error.httpStatus === 404) {
390
+ throw new CliError('tool_state_operation_not_found', `That operation is not on this instance. List them with firedrill tools state ${instanceId}.`, 2);
391
+ }
392
+ throw error;
393
+ });
394
+ emit('tool_state_operation', row, operationText(row));
395
+ return;
396
+ }
397
+ if (verb === 'request' || verb === 'clone-request') {
398
+ takes([]);
399
+ const requestKey = stateId(target, 'request key');
400
+ const unknown = (what) => `Firedrill has no ${what} under ${requestKey} for you on this instance yet. That is not proof it never arrives: `
401
+ + `replay it with the same --request-key, or withdraw it with firedrill tools state ${instanceId} ${verb === 'request' ? 'withdraw-request' : 'withdraw-clone'} ${requestKey} `
402
+ + '(which undoes nothing).';
403
+ try {
404
+ if (verb === 'request') {
405
+ const row = await recoverStateOperation(scope, { instanceId, requestKey, signal });
406
+ emit('tool_state_request_status', { request_key: requestKey, operation: row }, row ? operationText(row) : unknown('state request'));
407
+ }
408
+ else {
409
+ const batch = await recoverCloneRequest(scope, { instanceId, requestKey, signal });
410
+ emit('tool_state_clone_request', { request_key: requestKey, session: batch }, batch ? batchText(batch) : unknown('reset or fork request'));
411
+ }
412
+ }
413
+ catch (error) {
414
+ if (errorCode(error) !== 'native_tool_request_withdrawn')
415
+ throw error;
416
+ emit('tool_state_request_status', { request_key: requestKey, withdrawn: true }, `Request ${requestKey} was withdrawn; it can never run.`);
417
+ }
418
+ return;
419
+ }
420
+ if (verb === 'withdraw-request' || verb === 'withdraw-clone') {
421
+ takes(['approve']);
422
+ await withdraw(scope, instanceId, stateId(target, 'request key'), verb === 'withdraw-request' ? 'state' : 'clone', options, emit);
423
+ return;
424
+ }
425
+ throw new CliError('arguments_invalid', usage, 2);
426
+ }
@@ -0,0 +1,222 @@
1
+ import { constants } from 'node:fs';
2
+ import { lstat, mkdir, open, readdir } from 'node:fs/promises';
3
+ import { basename, join, resolve } from 'node:path';
4
+ import { CliError } from '../http.js';
5
+ import { terminalText } from '../prompts.js';
6
+ import { toolsCommand } from '../tools.js';
7
+ // Client-side reading bound only. The cloud package inspection is authoritative
8
+ // for capacity, engine preflight, capability references and credential screening.
9
+ const MAX_JSON_BYTES = 2 * 1024 * 1024;
10
+ const SLUG = /^[a-z][a-z0-9_-]{0,79}$/;
11
+ const NAME = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
12
+ const VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?$/;
13
+ const CONTROL = /[\u0000-\u001f\u007f]/;
14
+ const PRIVATE_BUNDLE_ROOTS = new Set(['scenarios', 'checks', 'handlers', 'simulator_prompt.md', 'sub_goals.json', 'generic-harness']);
15
+ async function readObject(path, label, signal) {
16
+ let info;
17
+ try {
18
+ info = await lstat(path);
19
+ }
20
+ catch {
21
+ throw new CliError('tool_package_file_missing', `${label} was not found.`, 2);
22
+ }
23
+ if (!info.isFile() || info.isSymbolicLink() || info.size > MAX_JSON_BYTES)
24
+ throw new CliError('tool_package_file_invalid', `${label} must be a regular file of at most ${MAX_JSON_BYTES} bytes.`, 2);
25
+ const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
26
+ try {
27
+ const bytes = await file.readFile({ signal });
28
+ const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
29
+ if (!value || typeof value !== 'object' || Array.isArray(value))
30
+ throw new Error();
31
+ return value;
32
+ }
33
+ catch (error) {
34
+ if (signal.aborted)
35
+ throw error;
36
+ throw new CliError('tool_package_file_invalid', `${label} is not a UTF-8 JSON object.`, 2);
37
+ }
38
+ finally {
39
+ await file.close();
40
+ }
41
+ }
42
+ const text = (value, maximum, empty = false) => typeof value === 'string' && value.length <= maximum && (empty || !!value.trim()) && !CONTROL.test(value);
43
+ const route = (value) => typeof value === 'string' && value.length <= 2048 && value.startsWith('/') && !value.startsWith('//')
44
+ && !/[\\?#%]/.test(value) && !/[\u0000- \u007f]/.test(value) && !value.split('/').some(part => part === '.' || part === '..');
45
+ /** Structural mirror of the tool.json rules; mirrors field names, not engine semantics. */
46
+ function contractProblems(value) {
47
+ const problems = [];
48
+ const add = (path, message) => { problems.push({ path, message }); };
49
+ const required = ['schema', 'key', 'version', 'name', 'description', 'services'];
50
+ for (const field of required)
51
+ if (!(field in value))
52
+ add(field, 'Required field is missing.');
53
+ for (const field of Object.keys(value))
54
+ if (![...required, 'state'].includes(field))
55
+ add(field, 'Unknown top-level field.');
56
+ if (value.schema !== 'firedrill.tool-package.v1')
57
+ add('schema', 'Use firedrill.tool-package.v1.');
58
+ if (typeof value.key !== 'string' || !SLUG.test(value.key))
59
+ add('key', 'Use lowercase letters, digits, _ or -, starting with a letter.');
60
+ if (typeof value.version !== 'string' || value.version.length > 80 || !VERSION.test(value.version))
61
+ add('version', 'Use a semantic version such as 1.0.0.');
62
+ if (!text(value.name, 120))
63
+ add('name', 'Use a non-empty name of up to 120 characters.');
64
+ if (!text(value.description, 2000, true))
65
+ add('description', 'Use a description of up to 2000 characters.');
66
+ const services = value.services;
67
+ if (!Array.isArray(services) || !services.length) {
68
+ add('services', 'Declare at least one service.');
69
+ return problems;
70
+ }
71
+ const keys = new Set();
72
+ services.forEach((service, index) => {
73
+ const at = `services[${index}]`;
74
+ if (!service || typeof service !== 'object' || Array.isArray(service)) {
75
+ add(at, 'Each service must be an object.');
76
+ return;
77
+ }
78
+ const row = service;
79
+ const fields = Object.keys(row).sort().join(',');
80
+ if (fields !== 'key,name,operations,surface')
81
+ add(at, 'Use exactly key, name, operations and surface.');
82
+ if (typeof row.key !== 'string' || !SLUG.test(row.key))
83
+ add(`${at}.key`, 'Use a lowercase service key.');
84
+ else if (keys.has(row.key))
85
+ add(`${at}.key`, 'Service keys must be unique.');
86
+ else
87
+ keys.add(row.key);
88
+ if (!text(row.name, 120))
89
+ add(`${at}.name`, 'Use a non-empty name of up to 120 characters.');
90
+ if (row.surface !== null && row.surface !== undefined) {
91
+ const surface = row.surface;
92
+ if (typeof surface !== 'object' || Array.isArray(surface) || Object.keys(surface).sort().join(',') !== 'capability,path'
93
+ || typeof surface.capability !== 'string' || !route(surface.path))
94
+ add(`${at}.surface`, 'Use null or { capability, path } with an absolute path.');
95
+ }
96
+ if (!Array.isArray(row.operations) || !row.operations.length) {
97
+ add(`${at}.operations`, 'Declare at least one operation.');
98
+ return;
99
+ }
100
+ const names = new Set();
101
+ row.operations.forEach((operation, position) => {
102
+ const op = `${at}.operations[${position}]`;
103
+ if (!operation || typeof operation !== 'object' || Array.isArray(operation)) {
104
+ add(op, 'Each operation must be an object.');
105
+ return;
106
+ }
107
+ const item = operation;
108
+ const allowed = ['name', 'description', 'effect', 'input_schema', 'output_schema', 'transport'];
109
+ for (const field of allowed)
110
+ if (!(field in item))
111
+ add(`${op}.${field}`, 'Required field is missing.');
112
+ for (const field of Object.keys(item))
113
+ if (![...allowed, 'mcp_aliases'].includes(field))
114
+ add(`${op}.${field}`, 'Unknown operation field.');
115
+ if (typeof item.name !== 'string' || !NAME.test(item.name))
116
+ add(`${op}.name`, 'Use a letter followed by letters, digits, _, . or -.');
117
+ else if (names.has(item.name))
118
+ add(`${op}.name`, 'Operation names must be unique within a service.');
119
+ else
120
+ names.add(item.name);
121
+ if (!text(item.description, 2000, true))
122
+ add(`${op}.description`, 'Use a description of up to 2000 characters.');
123
+ if (!['none', 'read', 'write', 'mixed'].includes(item.effect))
124
+ add(`${op}.effect`, 'Use none, read, write or mixed.');
125
+ if (!item.input_schema || typeof item.input_schema !== 'object' || Array.isArray(item.input_schema))
126
+ add(`${op}.input_schema`, 'Use a JSON schema object.');
127
+ const transport = item.transport;
128
+ if (!transport || typeof transport !== 'object' || Array.isArray(transport) || !['http', 'mcp'].includes(transport.kind)
129
+ || typeof transport.capability !== 'string') {
130
+ add(`${op}.transport`, 'Use an http or mcp transport with a capability name.');
131
+ return;
132
+ }
133
+ if (transport.kind === 'mcp' && (Object.keys(transport).sort().join(',') !== 'capability,kind,path,tool'
134
+ || typeof transport.tool !== 'string' || !NAME.test(transport.tool) || !route(transport.path)))
135
+ add(`${op}.transport`, 'An mcp transport uses exactly kind, capability, path and tool.');
136
+ });
137
+ });
138
+ return problems;
139
+ }
140
+ async function validate(directory, signal) {
141
+ const root = resolve(directory);
142
+ const info = await lstat(root).catch(() => null);
143
+ if (!info?.isDirectory())
144
+ throw new CliError('tool_package_directory_invalid', 'Choose the tool package directory that contains tool.json.', 2);
145
+ const contract = await readObject(join(root, 'tool.json'), 'tool.json', signal);
146
+ const manifest = await readObject(join(root, 'bundle', 'manifest.json'), 'bundle/manifest.json', signal);
147
+ const problems = contractProblems(contract);
148
+ if (Array.isArray(manifest.processes))
149
+ manifest.processes.forEach((process, index) => {
150
+ if (!process || typeof process !== 'object' || Array.isArray(process) || (process.kind ?? 'source') !== 'source')
151
+ return;
152
+ const path = process.working_directory;
153
+ const parts = typeof path === 'string' ? path.split('/').filter(part => part !== '' && part !== '.') : [];
154
+ if (!parts.length || parts.includes('..') || PRIVATE_BUNDLE_ROOTS.has(parts[0]))
155
+ problems.push({ path: `bundle/manifest.json.processes[${index}].working_directory`,
156
+ message: 'Use a dedicated executable directory such as app, not the bundle root or private assessment directories.' });
157
+ });
158
+ const extra = (await readdir(root)).filter(name => !['tool.json', 'bundle'].includes(name)).sort();
159
+ return { directory: root, problems,
160
+ warnings: extra.length ? [`Not part of the package archive: ${extra.join(', ')}. The archive root holds only tool.json and bundle/.`] : [] };
161
+ }
162
+ export async function toolsAuthorCommand(args, options, emit) {
163
+ const action = args[0];
164
+ if (action === 'init') {
165
+ if (args.length > 2 || options.file !== undefined || options.upload || options.request !== undefined)
166
+ throw new CliError('arguments_invalid', 'Use firedrill tools init [directory]. Nothing is uploaded.', 2);
167
+ const root = resolve(args[1] ?? '.');
168
+ await mkdir(root, { recursive: true, mode: 0o755 });
169
+ const derived = basename(root).toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^[^a-z]+/, '').slice(0, 80);
170
+ const contract = { schema: 'firedrill.tool-package.v1', key: SLUG.test(derived) ? derived : 'custom-tool', version: '0.1.0',
171
+ name: 'Custom tool', description: '', services: [] };
172
+ let file;
173
+ try {
174
+ file = await open(join(root, 'tool.json'), 'wx', 0o644);
175
+ }
176
+ catch {
177
+ throw new CliError('tool_package_exists', 'tool.json already exists in this directory. Nothing was changed.', 2);
178
+ }
179
+ try {
180
+ await file.writeFile(JSON.stringify(contract, null, 2) + '\n');
181
+ }
182
+ finally {
183
+ await file.close();
184
+ }
185
+ // Only after tool.json was created exclusively, so an existing package is left untouched.
186
+ await mkdir(join(root, 'bundle'), { recursive: true, mode: 0o755 });
187
+ await mkdir(join(root, 'bundle', 'app'), { recursive: true, mode: 0o755 });
188
+ emit('tool_package_initialized', { directory: root, files: ['tool.json', 'bundle/', 'bundle/app/'], uploaded: false }, [`Created ${terminalText(join(root, 'tool.json'))} and bundle/.`,
189
+ 'Declare at least one service in tool.json and add the process bundle with bundle/manifest.json.',
190
+ 'Put executable files in bundle/app/ and use working_directory: "app". Never use the bundle root.',
191
+ 'Then run firedrill tools validate and firedrill tools push <package.zip>. Nothing was uploaded.'].join('\n'));
192
+ return;
193
+ }
194
+ if (action === 'validate') {
195
+ if (args.length > 2 || options.file !== undefined || options.upload || options.request !== undefined)
196
+ throw new CliError('arguments_invalid', 'Use firedrill tools validate [directory]. Nothing is uploaded.', 2);
197
+ const result = await validate(args[1] ?? '.', options.signal);
198
+ const valid = !result.problems.length;
199
+ emit('tool_package_validation', { ...result, valid, scope: 'structural', uploaded: false }, [
200
+ valid ? 'tool.json and bundle/manifest.json passed local structural checks.' : `${result.problems.length} problems found:`,
201
+ ...result.problems.map(one => ` ${terminalText(one.path)}: ${one.message}`), ...result.warnings.map(terminalText),
202
+ 'Capability references, engine preflight and capacity are checked by the cloud inspection after push.'
203
+ ].join('\n'));
204
+ if (!valid)
205
+ throw new CliError('tool_package_invalid', 'The tool package has structural problems. Nothing was uploaded.', 1);
206
+ return;
207
+ }
208
+ if (action === 'push') {
209
+ if (args.length > 2 || args[1] !== undefined && options.file !== undefined)
210
+ throw new CliError('arguments_invalid', 'Use firedrill tools push <package.zip> or --file <package.zip>.', 2);
211
+ const path = args[1] ?? options.file;
212
+ if (!path)
213
+ throw new CliError('tool_package_file_required', 'Use firedrill tools push <package.zip>. No file was read or uploaded.', 2);
214
+ const info = await lstat(resolve(path)).catch(() => null);
215
+ if (info?.isDirectory())
216
+ throw new CliError('tool_package_archive_required', 'Create a ZIP whose root holds tool.json and bundle/, then push that file. Nothing was uploaded.', 2);
217
+ await toolsCommand(['import'], { json: options.json, workspace: options.workspace, openBrowser: options.openBrowser,
218
+ signal: options.signal, file: path, upload: options.upload, request: options.request }, emit);
219
+ return;
220
+ }
221
+ throw new CliError('arguments_invalid', 'Use firedrill tools init|validate|push.', 2);
222
+ }