firedrill-run 2.2.4 → 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,69 @@
1
+ import { APP_ORIGIN } from './http.js';
2
+ import { api, openBrowser } from './auth.js';
3
+ import { auth, choosePaged, interactive, query } from './configuration.js';
4
+ import { CliError } from './http.js';
5
+ import { selectOne, terminalText } from './prompts.js';
6
+ import { selectedWorld } from './world-access.js';
7
+ import { identifier } from './world-contracts.js';
8
+ const states = { uploading: 'Upload pending', processing: 'Processing', review: 'Review before use',
9
+ accepted: 'Available for preparation', rejected: 'Could not process', revoked: 'Removed' };
10
+ function describe(value) {
11
+ return [value.filename, value.id, `${value.format} · ${states[value.state] ?? 'Status unavailable'}`,
12
+ ...(value.summary.record_count !== undefined ? [`${value.summary.record_count.toLocaleString()} retained observations`] : []),
13
+ ...(value.summary.gaps ?? []), ...(value.failure ? [value.failure] : []), `Source: ${value.source_revision}`].map(terminalText).join('\n');
14
+ }
15
+ export async function recordingsCommand(args, options, output) {
16
+ let action = args[0] ?? (interactive(options) ? '' : 'list');
17
+ if (options.file || options.request || options.approve || options.status || args.length > 2)
18
+ throw new CliError('arguments_invalid', 'Use recordings list, show|records <id>, or open|upload. Upload and consent review open in the browser.', 2);
19
+ if (!action)
20
+ action = await selectOne('Recorded activity', [{ value: 'list', label: 'View recordings' },
21
+ { value: 'upload', label: 'Upload recorded activity in the browser' }, { value: 'open', label: 'Review preparation context in the browser' }], options.signal);
22
+ if (!['list', 'show', 'records', 'open', 'upload', 'request'].includes(action) || ['list', 'open', 'upload'].includes(action) && args[1]
23
+ || !['list', 'records'].includes(action) && (options.page || options.search))
24
+ throw new CliError('arguments_invalid', 'Use recording IDs from this world; page/search apply to list or records.', 2);
25
+ const context = await selectedWorld({ ...options, interactive: interactive(options) }), access = auth(context, options);
26
+ const url = new URL(`/worlds/${identifier(context.world)}`, APP_ORIGIN);
27
+ url.searchParams.set('workspace', context.workspace);
28
+ url.hash = 'recorded-activity';
29
+ if (action === 'request') {
30
+ if (!args[1])
31
+ throw new CliError('recording_request_required', 'Use the original recorded activity request UUID.', 2);
32
+ const key = identifier(args[1]);
33
+ const receipt = await api(`${context.prefix}/contexts/requests/${key}`, access);
34
+ if (receipt.request_key !== key || !['received', 'not_retained'].includes(receipt.state)
35
+ || (receipt.state === 'received') !== Boolean(receipt.recording))
36
+ throw new CliError('recording_request_unconfirmed', 'The original recording request could not be confirmed. No replacement was created.', 2);
37
+ url.searchParams.set('context_request', key);
38
+ output(receipt, (receipt.recording ? describe(receipt.recording) : 'No saved recording is retained yet. A delayed original request may still arrive.') + '\n' + url.href);
39
+ return;
40
+ }
41
+ if (action === 'open' || action === 'upload') {
42
+ if (action === 'upload')
43
+ url.searchParams.set('context', 'upload');
44
+ output({ browser_url: url.href, workspace_id: context.workspace, world_id: context.world }, `${url.href}\n${action === 'upload' ? 'Choose a recording and review sharing consent in the browser. Nothing was uploaded by this command.' : 'Review accepted and pending observations in this world.'}`);
45
+ if (interactive(options) && options.openBrowser)
46
+ await openBrowser(url.href);
47
+ return;
48
+ }
49
+ if (action === 'list') {
50
+ const value = await api(`${context.prefix}/contexts?${query(options)}`, access);
51
+ output(value, [...value.results.map(describe), ...(!value.count ? ['No matching recordings.'] : []),
52
+ `Page ${value.page}; ${value.count.toLocaleString()} recordings.`, ...(value.next ? [`Continue with --page ${value.page + 1} and the same search.`] : [])].join('\n\n'));
53
+ return;
54
+ }
55
+ const id = args[1] ?? (interactive(options) ? (await choosePaged('Recording', `${context.prefix}/contexts`, context, options, row => ({ label: row.filename, detail: states[row.state] ?? 'Status unavailable' })))?.id : undefined);
56
+ if (!id)
57
+ throw new CliError('recording_required', 'Choose a recording ID with firedrill recordings list.', 2);
58
+ const prefix = `${context.prefix}/contexts/${identifier(id)}`;
59
+ if (action === 'show') {
60
+ const value = await api(prefix, access);
61
+ output(value, describe(value) + '\n\n' + url.href);
62
+ return;
63
+ }
64
+ const value = await api(`${prefix}/records?${query(options)}`, access);
65
+ output(value, [...value.results.map(row => [terminalText(row.operation ?? row.name ?? 'Observation'), terminalText(row.kind),
66
+ `Source: ${terminalText(row.pointer)}`, ...(row.input !== null ? ['Recorded input: ' + terminalText(JSON.stringify(row.input))] : []),
67
+ ...(row.output !== null ? ['Recorded output: ' + terminalText(JSON.stringify(row.output))] : [])].join('\n')),
68
+ `Page ${value.page}; ${value.count.toLocaleString()} observations.`, ...(value.next ? [`Continue with --page ${value.page + 1} and the same search.`] : [])].join('\n\n'));
69
+ }
@@ -0,0 +1,39 @@
1
+ import { API_ORIGIN, CliError } from './http.js';
2
+ /** Public discovery only. WorkOS owns registration, claim codes and token exchange. */
3
+ export async function registrationGuide(signal, output) {
4
+ const url = new URL('/auth.md', API_ORIGIN);
5
+ const response = await fetch(url, { headers: { Accept: 'text/markdown, text/plain' }, credentials: 'omit',
6
+ redirect: 'error', signal: AbortSignal.any([signal, AbortSignal.timeout(15000)]) });
7
+ if (!response.ok || !response.body || !/^(text\/markdown|text\/plain)(;|$)/i.test(response.headers.get('content-type') ?? '')) {
8
+ await response.body?.cancel();
9
+ throw new CliError('agent_registration_unavailable', 'Agent registration instructions are not available yet. Use firedrill signup for browser authorization.', 2);
10
+ }
11
+ const reader = response.body.getReader(), chunks = [];
12
+ let size = 0;
13
+ try {
14
+ while (true) {
15
+ const chunk = await reader.read();
16
+ if (chunk.done)
17
+ break;
18
+ size += chunk.value.byteLength;
19
+ if (size > 256 * 1024)
20
+ throw new CliError('agent_registration_response_invalid', 'Registration instructions exceeded the supported size.', 2);
21
+ chunks.push(chunk.value);
22
+ }
23
+ }
24
+ finally {
25
+ await reader.cancel().catch(() => undefined);
26
+ }
27
+ let instructions;
28
+ try {
29
+ instructions = new TextDecoder('utf8', { fatal: true }).decode(Buffer.concat(chunks));
30
+ }
31
+ catch {
32
+ throw new CliError('agent_registration_response_invalid', 'Registration instructions could not be read.', 2);
33
+ }
34
+ if (!instructions.trim() || /<html/i.test(instructions.slice(0, 512)))
35
+ throw new CliError('agent_registration_response_invalid', 'Registration instructions could not be confirmed.', 2);
36
+ output({ schema: 'firedrill.agent-registration-guide.v1', url: url.href, instructions,
37
+ default_terminal_command: 'firedrill signup', credential_storage: 'os_keyring',
38
+ payment_required: false, account_created: false, authorization_required: true }, instructions);
39
+ }
@@ -0,0 +1,213 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { link, lstat, open, realpath, unlink } from 'node:fs/promises';
3
+ import { basename, dirname, join, resolve } from 'node:path';
4
+ import { access, api } from './auth.js';
5
+ import { CliError, endpoint, responseError } from './http.js';
6
+ import { selectOne } from './prompts.js';
7
+ import { identifier } from './world-contracts.js';
8
+ import { reportExportMatches, reportExportsPageMatches, reportExportRecoveryMatches } from '../../contracts/export-contracts.js';
9
+ const actions = new Set(['list', 'create', 'show', 'request', 'cancel', 'download']);
10
+ const semanticStatuses = { assessed: 'succeeded', 'not-assessable': 'not_assessable', 'not-applicable': 'not_applicable' };
11
+ export function validateExport(args, options) {
12
+ const [action, value] = args;
13
+ if (args.length > 2 || action && !actions.has(action) || value && !['show', 'request', 'cancel', 'download'].includes(action)
14
+ || action && ['show', 'request', 'cancel', 'download'].includes(action) && !value
15
+ || (options.search !== undefined || options.scenario !== undefined || options.status !== undefined || options.checks !== undefined || options.requestKey !== undefined
16
+ || options.cohort !== undefined || options.scoreBucket !== undefined || options.assessmentStatus !== undefined) && action !== 'create'
17
+ || options.page !== undefined && action !== 'list'
18
+ || (options.into !== undefined || options.approveDownload !== undefined) && action !== 'download') {
19
+ throw new CliError('arguments_invalid', 'Use report <campaign> export list|create|show|request|cancel|download. Filters and --request-key apply to create; --page to list; --into and --approve-download to download.', 2);
20
+ }
21
+ if (value)
22
+ identifier(value);
23
+ if (options.requestKey)
24
+ identifier(options.requestKey);
25
+ if (options.cohort !== undefined)
26
+ identifier(options.cohort);
27
+ if ((options.scoreBucket !== undefined || options.assessmentStatus !== undefined) && !options.cohort) {
28
+ throw new CliError('arguments_invalid', 'Choose the exact score group with --cohort <assessment> before filtering its export.', 2);
29
+ }
30
+ if (options.scoreBucket !== undefined && !/^[0-8]$/.test(options.scoreBucket)
31
+ || options.assessmentStatus !== undefined && !Object.hasOwn(semanticStatuses, options.assessmentStatus)
32
+ || options.scoreBucket !== undefined && options.assessmentStatus !== undefined && options.assessmentStatus !== 'assessed') {
33
+ throw new CliError('arguments_invalid', 'Choose an assessed score interval or assessed, not-assessable or not-applicable status from the selected score group.', 2);
34
+ }
35
+ if (options.into !== undefined && (!options.into.trim() || /[\x00-\x1f\x7f]/.test(options.into))) {
36
+ throw new CliError('arguments_invalid', 'Choose a new report file using --into <file.jsonl>. Existing files are never replaced.', 2);
37
+ }
38
+ }
39
+ function readable(row) {
40
+ return [`Export ${row.id}: ${row.state.replaceAll('_', ' ')}`, `Original request: ${row.request_key}`,
41
+ ...(row.selection?.map(one => `${one.label}: ${one.value}`) ?? ['Selection details were not recorded.']),
42
+ ...(row.counts.trial !== undefined ? [`${row.counts.trial.toLocaleString()} drills recorded`] : []),
43
+ ...(row.problem ? [row.problem] : []), ...(row.file ? [`JSONL file: ${row.file.size.toLocaleString()} bytes`,
44
+ 'Original evidence and media are referenced, not copied. Downloads still require workspace access and retained evidence.'] : [])].join('\n');
45
+ }
46
+ async function saveReport(path, row, scope, interactive, options) {
47
+ const metadata = row.file;
48
+ if (!metadata || !Number.isSafeInteger(metadata.size) || metadata.size < 1 || !/^[a-f0-9]{64}$/.test(metadata.sha256)) {
49
+ throw new CliError('report_export_unavailable', 'The exact saved report is not available. Inspect its status first.');
50
+ }
51
+ if (!options.into)
52
+ throw new CliError('arguments_invalid', 'Choose a new private report file with --into <file.jsonl>. Existing files are never replaced.', 2);
53
+ if (!options.approveDownload) {
54
+ if (!interactive)
55
+ throw new CliError('report_export_consent_required', 'Use --approve-download to save the selected private report on this device.', 2);
56
+ if (!await selectOne('Save this private report on this device?', [{ value: false, label: 'Cancel' },
57
+ { value: true, label: 'Download report', detail: 'Contains selected results and assessments. Original media stays in the cloud.' }], options.signal)) {
58
+ throw new CliError('canceled', 'No report was downloaded.', 130);
59
+ }
60
+ }
61
+ const chosen = resolve(options.into), parent = await realpath(dirname(chosen)), destination = join(parent, basename(chosen));
62
+ try {
63
+ await lstat(destination);
64
+ throw new CliError('report_file_exists', 'The chosen file already exists. Choose a different path.', 2);
65
+ }
66
+ catch (error) {
67
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
68
+ throw error;
69
+ }
70
+ const partial = join(parent, `.firedrill-report-${randomUUID()}.partial`);
71
+ const file = await open(partial, 'wx', 0o600), identity = await file.stat();
72
+ try {
73
+ return await access(async (token) => {
74
+ const response = await fetch(endpoint(path), { redirect: 'error', credentials: 'omit',
75
+ headers: { Authorization: `Bearer ${token}`, 'X-Firedrill-Actor': scope.owner, 'X-Firedrill-Workspace': scope.workspace },
76
+ signal: AbortSignal.any([options.signal, AbortSignal.timeout(2 * 60 * 60 * 1000)]) });
77
+ if (response.status !== 200) {
78
+ await response.body?.cancel();
79
+ throw responseError(null, response.status);
80
+ }
81
+ if (response.headers.get('content-length') !== String(metadata.size) || response.headers.get('x-content-sha256') !== metadata.sha256
82
+ || response.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/x-ndjson' || !response.body) {
83
+ await response.body?.cancel();
84
+ throw new CliError('report_export_integrity', 'The response did not match the saved report. No completed file was saved.');
85
+ }
86
+ const reader = response.body.getReader(), digest = createHash('sha256');
87
+ let size = 0;
88
+ try {
89
+ while (true) {
90
+ const part = await reader.read();
91
+ if (part.done)
92
+ break;
93
+ size += part.value.byteLength;
94
+ if (size > metadata.size)
95
+ throw new CliError('report_export_integrity', 'The report exceeded its recorded size. No completed file was saved.');
96
+ digest.update(part.value);
97
+ let offset = 0;
98
+ while (offset < part.value.byteLength) {
99
+ const result = await file.write(part.value, offset, part.value.byteLength - offset);
100
+ if (!result.bytesWritten)
101
+ throw new CliError('report_file_unavailable', 'The private report file could not be written.');
102
+ offset += result.bytesWritten;
103
+ }
104
+ }
105
+ }
106
+ finally {
107
+ await reader.cancel().catch(() => undefined);
108
+ reader.releaseLock();
109
+ }
110
+ if (size !== metadata.size || digest.digest('hex') !== metadata.sha256) {
111
+ throw new CliError('report_export_integrity', 'The report was incomplete or changed. No completed file was saved.');
112
+ }
113
+ await file.sync();
114
+ const current = await lstat(partial);
115
+ if (!current.isFile() || current.dev !== identity.dev || current.ino !== identity.ino)
116
+ throw new CliError('report_file_changed', 'The private file changed during download. Nothing was published to the chosen path.');
117
+ // A hard link publishes only complete bytes and refuses an existing path.
118
+ await link(partial, destination);
119
+ return { path: destination, size, sha256: metadata.sha256 };
120
+ });
121
+ }
122
+ catch (error) {
123
+ if (error instanceof CliError)
124
+ throw error;
125
+ throw new CliError('report_download_unconfirmed', 'The report download could not finish. Inspect the chosen file path before trying again; no existing file was replaced.');
126
+ }
127
+ finally {
128
+ await file.close().catch(() => undefined);
129
+ const current = await lstat(partial).catch(() => null);
130
+ if (current?.isFile() && current.dev === identity.dev && current.ino === identity.ino)
131
+ await unlink(partial).catch(() => undefined);
132
+ }
133
+ }
134
+ export async function reportExport(args, prefix, scope, interactive, options, output) {
135
+ validateExport(args, options);
136
+ const path = `${prefix}/report-exports`, auth = { actor: scope.owner, workspace: scope.workspace, signal: options.signal };
137
+ const campaign = identifier(prefix.split('/').at(-1));
138
+ const selectedScope = { workspace: scope.workspace, campaign };
139
+ const unavailable = () => new CliError('report_export_response_invalid', 'The exact saved export could not be confirmed. Inspect the original request; no replacement export was requested.');
140
+ const action = args[0] ?? (interactive ? await selectOne('Report export', [{ value: 'list', label: 'Saved exports' },
141
+ { value: 'create', label: 'Create a JSONL report', detail: 'Selected results and assessments with original evidence references.' }], options.signal) : 'list');
142
+ let selected = args[1] ? identifier(args[1]) : undefined;
143
+ if (action === 'create') {
144
+ if (!interactive && !options.requestKey)
145
+ throw new CliError('request_key_required', 'Pass --request-key <uuid> and reuse it after an uncertain response. Do not create a new key to retry.', 2);
146
+ const request = options.requestKey ? identifier(options.requestKey) : randomUUID();
147
+ process.stderr.write(`Original export request: ${request}\nRecovery: firedrill report ${prefix.split('/').at(-1)} export request ${request} --workspace ${scope.workspace}\n`);
148
+ const filters = { q: options.search ?? '', status: options.status ?? '', checks: options.checks ?? '', scenario: options.scenario ?? '',
149
+ ...(options.cohort ? { cohort: identifier(options.cohort), score_bucket: options.scoreBucket ?? '',
150
+ assessment_status: options.assessmentStatus ? semanticStatuses[options.assessmentStatus] : '' } : {}) };
151
+ const row = await api(path, { ...auth, method: 'POST', idempotencyKey: request, body: { filters } });
152
+ if (!reportExportMatches(row, { ...selectedScope, actor: scope.owner }, { key: request, filters }))
153
+ throw unavailable();
154
+ const original = await api(`${path}?request_key=${request}`, auth);
155
+ if (!reportExportRecoveryMatches(original, { ...selectedScope, actor: scope.owner }, request)
156
+ || !reportExportMatches(original, selectedScope, { id: row.id, key: request, filters }))
157
+ throw unavailable();
158
+ output(original, readable(original));
159
+ return;
160
+ }
161
+ if (action === 'request') {
162
+ const row = await api(`${path}?request_key=${selected}`, auth);
163
+ if (!reportExportRecoveryMatches(row, { ...selectedScope, actor: scope.owner }, selected))
164
+ throw unavailable();
165
+ output(row, readable(row));
166
+ return;
167
+ }
168
+ if (action === 'list') {
169
+ let page = options.page ?? '1';
170
+ while (true) {
171
+ const data = await api(`${path}?page=${page}`, auth);
172
+ if (!reportExportsPageMatches(data, selectedScope))
173
+ throw unavailable();
174
+ if (!interactive) {
175
+ output(data, [...data.results.map(readable), `Page ${data.page} of ${Math.max(1, Math.ceil(data.count / data.page_size))}`,
176
+ ...(data.next ? [`Continue: firedrill report ${campaign} export list --workspace ${scope.workspace} --page ${data.page + 1}${options.json ? ' --json' : ''}`] : [])].join('\n\n'));
177
+ return;
178
+ }
179
+ const choices = data.results.map(value => ({ value: value.id, label: new Date(value.created_at).toLocaleString(),
180
+ detail: [value.state.replaceAll('_', ' '), ...(value.selection?.map(one => `${one.label}: ${one.value}`) ?? [])].join('; ') }));
181
+ if (Number(page) > 1)
182
+ choices.unshift({ value: 'previous', label: 'Previous page', detail: '' });
183
+ if (data.next)
184
+ choices.push({ value: 'next', label: 'Next page', detail: '' });
185
+ if (!choices.length) {
186
+ output(data, 'No saved exports.');
187
+ return;
188
+ }
189
+ const choice = await selectOne('Saved report', choices, options.signal);
190
+ if (choice === 'previous' || choice === 'next') {
191
+ page = String(Number(page) + (choice === 'next' ? 1 : -1));
192
+ continue;
193
+ }
194
+ selected = identifier(choice);
195
+ break;
196
+ }
197
+ }
198
+ const row = await api(`${path}/${selected}`, auth);
199
+ if (!reportExportMatches(row, selectedScope, { id: selected }))
200
+ throw unavailable();
201
+ if (action === 'download') {
202
+ const saved = await saveReport(`${path}/${selected}/download`, row, scope, interactive, options);
203
+ output({ export_id: row.id, ...saved }, `Report saved to ${saved.path}\n${saved.size.toLocaleString()} bytes; SHA-256 ${saved.sha256}`);
204
+ }
205
+ else if (action === 'cancel') {
206
+ const changed = await api(`${path}/${selected}`, { ...auth, method: 'POST', body: { action: 'cancel' } });
207
+ if (!reportExportMatches(changed, { ...selectedScope, actor: scope.owner }, { id: row.id, key: row.request_key, filters: row.filters }))
208
+ throw unavailable();
209
+ output(changed, readable(changed));
210
+ }
211
+ else
212
+ output(row, readable(row));
213
+ }
@@ -0,0 +1,47 @@
1
+ import { constants } from 'node:fs';
2
+ import { lstat, open } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+ import { CliError } from './http.js';
5
+ /** Read one bounded, unchanged JSON object without rewriting number literals. */
6
+ export async function requestInput(path, signal, maximumBytes = 8 * 1024 * 1024) {
7
+ if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 2 || maximumBytes > 8 * 1024 * 1024) {
8
+ throw new CliError('request_capacity_invalid', 'Choose a supported request capacity.', 2);
9
+ }
10
+ if (!path)
11
+ return '{}';
12
+ const before = await lstat(resolve(path));
13
+ if (!before.isFile() || before.isSymbolicLink() || before.size > maximumBytes) {
14
+ throw new CliError('request_file_invalid', 'Choose a regular JSON file within transport capacity.', 2);
15
+ }
16
+ const file = await open(resolve(path), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
17
+ try {
18
+ const held = await file.stat();
19
+ if (held.dev !== before.dev || held.ino !== before.ino || held.size !== before.size || held.mtimeMs !== before.mtimeMs)
20
+ throw new Error();
21
+ const chunks = [];
22
+ let size = 0;
23
+ for await (const chunk of file.createReadStream({ autoClose: false })) {
24
+ signal.throwIfAborted();
25
+ size += chunk.length;
26
+ if (size > maximumBytes)
27
+ throw new Error();
28
+ chunks.push(Buffer.from(chunk));
29
+ }
30
+ const after = await file.stat();
31
+ if (after.size !== held.size || after.mtimeMs !== held.mtimeMs || after.ctimeMs !== held.ctimeMs)
32
+ throw new Error();
33
+ const raw = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks));
34
+ const value = JSON.parse(raw);
35
+ if (!value || typeof value !== 'object' || Array.isArray(value))
36
+ throw new Error();
37
+ return raw;
38
+ }
39
+ catch (error) {
40
+ if (signal.aborted)
41
+ throw error;
42
+ throw new CliError('request_file_invalid', 'The file changed or is not a JSON object. Nothing was submitted.', 2);
43
+ }
44
+ finally {
45
+ await file.close();
46
+ }
47
+ }
@@ -0,0 +1,141 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { open } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { api } from './auth.js';
5
+ import { auth, choosePaged, interactive, line } from './configuration.js';
6
+ import { readMetadata, record } from './configuration-intents.js';
7
+ import { privateDirectory } from './credentials.js';
8
+ import { CliError } from './http.js';
9
+ import { selectOne, terminalText } from './prompts.js';
10
+ import { selectedWorkspace } from './world-access.js';
11
+ import { identifier } from './world-contracts.js';
12
+ import { resourceLifecycleBodyMatches, resourceLifecycleMetadataMatches, resourceLifecycleReceiptMatches, resourceLifecycleReceiptSame, resourceLifecyclePageMatches } from '../../contracts/resource-lifecycle-contracts.js';
13
+ const invalid = () => new CliError('resource_change_unconfirmed', 'Inspect the original resource change before starting another. No success is implied.', 2);
14
+ const canonical = (value) => JSON.stringify([value.kind, value.id, value.action, value.basis, value.confirm_name, value.retain_saved_data]);
15
+ function bodyData(value, kind) {
16
+ if (!resourceLifecycleBodyMatches(value) || value.kind !== kind)
17
+ throw invalid();
18
+ return value;
19
+ }
20
+ function checked(value, scope, key, kind, body) {
21
+ if (!resourceLifecycleReceiptMatches(value, { actor: scope.owner, workspace: scope.workspace }, { key, kind, body }))
22
+ throw invalid();
23
+ return value;
24
+ }
25
+ function show(value, output) {
26
+ output(value, `${value.request_key}\n${value.state === 'accepted' ? `${value.body.confirm_name} · ${value.body.action} saved`
27
+ : value.state === 'withdrawn' ? 'Unsent request discarded.' : 'The original request is not confirmed. It may still arrive.'}`.split('\n').map(terminalText).join('\n'));
28
+ }
29
+ const location = async (scope, key) => join(await privateDirectory(), `resource-${identifier(scope.owner)}-${identifier(scope.workspace)}-${identifier(key)}.json`);
30
+ async function retained(scope, key, kind) {
31
+ const value = await readMetadata(await location(scope, key), true);
32
+ if (value.schema !== 'firedrill.resource-intent.v1' || value.actor !== scope.owner || value.workspace !== scope.workspace
33
+ || value.key !== key || Object.keys(value).sort().join(',') !== 'actor,body,key,schema,workspace')
34
+ throw invalid();
35
+ bodyData(value.body, kind);
36
+ return value;
37
+ }
38
+ async function save(value, scope, kind) {
39
+ try {
40
+ const file = await open(await location(scope, value.key), 'wx', 0o600);
41
+ try {
42
+ await file.writeFile(JSON.stringify(value));
43
+ await file.sync();
44
+ }
45
+ finally {
46
+ await file.close();
47
+ }
48
+ }
49
+ catch (error) {
50
+ if (!record(error) || error.code !== 'EEXIST')
51
+ throw error;
52
+ if (canonical((await retained(scope, value.key, kind)).body) !== canonical(value.body))
53
+ throw invalid();
54
+ }
55
+ }
56
+ async function approve(options, label) {
57
+ if (options.approve)
58
+ return;
59
+ if (!interactive(options))
60
+ throw new CliError('approval_required', 'Review the resource and pass --approve-change. No change was submitted.', 2);
61
+ if (!await selectOne(label, [{ value: false, label: 'Cancel' }, { value: true, label: 'Confirm' }], options.signal))
62
+ throw new CliError('canceled', 'No change was submitted.', 130);
63
+ }
64
+ async function submit(intent, scope, options, output) {
65
+ const base = `/api/workspaces/${scope.workspace}`, body = intent.body;
66
+ const original = checked(await api(`${base}/resource-requests/${intent.key}`, auth(scope, options)), scope, intent.key, body.kind, body);
67
+ if (original.state !== 'not_received') {
68
+ show(original, output);
69
+ return;
70
+ }
71
+ output({ phase: 'request_prepared', request_key: intent.key }, `Request ${intent.key}\nRecovery: firedrill ${body.kind} request ${intent.key} --workspace ${scope.workspace}`);
72
+ const { kind, id, ...fields } = body;
73
+ const data = fields;
74
+ const accepted = checked(await api(`${base}/resources/${kind}/${id}/lifecycle`, { ...auth(scope, options), method: 'POST',
75
+ idempotencyKey: intent.key, body: data }), scope, intent.key, kind, body);
76
+ const observed = checked(await api(`${base}/resource-requests/${intent.key}`, auth(scope, options)), scope, intent.key, kind, body);
77
+ if (!resourceLifecycleReceiptSame(accepted, observed))
78
+ throw invalid();
79
+ show(observed, output);
80
+ }
81
+ export async function resourceLifecycle(kind, args, options, output) {
82
+ const action = args[0];
83
+ if (args.length > 2 || !['archive', 'restore', 'remove', 'requests', 'request', 'resume', 'withdraw'].includes(action)
84
+ || (options.page !== undefined || options.search !== undefined || options.status !== undefined) && action !== 'requests'
85
+ || options.request !== undefined && !['archive', 'restore', 'remove'].includes(action)
86
+ || options.approve && ['requests', 'request'].includes(action) || action === 'requests' && args[1])
87
+ throw invalid();
88
+ const scope = await selectedWorkspace({ ...options, interactive: interactive(options) });
89
+ const base = `/api/workspaces/${scope.workspace}`, access = auth(scope, options);
90
+ if (action === 'requests') {
91
+ const page = options.page ?? '1';
92
+ const search = options.search ?? '', state = options.status ?? '';
93
+ if (!/^[1-9][0-9]{0,8}$/.test(page) || search.length > 120 || /[\u0000-\u001f\u007f]/.test(search) || !['', 'accepted', 'withdrawn'].includes(state))
94
+ throw invalid();
95
+ const value = await api(`${base}/resource-requests?${new URLSearchParams({ kind, page, q: search, state })}`, access);
96
+ if (!resourceLifecyclePageMatches(value, { actor: scope.owner, workspace: scope.workspace }, { kind, query: search, state, page: Number(page) }))
97
+ throw invalid();
98
+ output(value, [...value.results.map(row => `${row.request_key}\n${row.body?.confirm_name ?? 'Unsent request'} · ${row.body?.action ?? ''} · ${row.state}`),
99
+ `Page ${value.page}; ${value.count} requests.`, ...(value.next ? [`Continue with --page ${value.page + 1} and the same workspace/search/status filters.`] : [])].map(terminalText).join('\n\n'));
100
+ return;
101
+ }
102
+ if (['request', 'resume', 'withdraw'].includes(action)) {
103
+ const key = identifier(args[1] ?? ''), path = `${base}/resource-requests/${key}`;
104
+ const original = checked(await api(path, access), scope, key, kind);
105
+ if (action === 'request' || original.state !== 'not_received') {
106
+ show(original, output);
107
+ return;
108
+ }
109
+ await approve(options, action === 'withdraw' ? 'Discard the unsent original change? A change already saved will not be undone.' : 'Continue this exact original change?');
110
+ if (action === 'withdraw') {
111
+ const accepted = checked(await api(path, { ...access, method: 'DELETE', body: {} }), scope, key, kind);
112
+ const observed = checked(await api(path, access), scope, key, kind);
113
+ if (!resourceLifecycleReceiptSame(accepted, observed))
114
+ throw invalid();
115
+ show(observed, output);
116
+ }
117
+ else
118
+ await submit(await retained(scope, key, kind), scope, options, output);
119
+ return;
120
+ }
121
+ let id = args[1] ? identifier(args[1]) : undefined;
122
+ if (!id && interactive(options))
123
+ id = (await choosePaged(kind === 'agents' ? 'Agent' : 'World', `${base}/${kind}?lifecycle=all`, scope, options, row => ({ label: row.name, detail: row.archived_at ? 'Archived' : '' })))?.id;
124
+ if (!id)
125
+ throw new CliError('resource_required', `Pass an exact ${kind === 'agents' ? 'agent' : 'world'} ID or run interactively.`, 2);
126
+ const current = await api(`${base}/resources/${kind}/${id}/lifecycle`, access);
127
+ if (!resourceLifecycleMetadataMatches(current, { actor: scope.owner, workspace: scope.workspace, id, kind })
128
+ || current.removed || !current.can_write
129
+ || action === 'remove' && !current.can_remove)
130
+ throw invalid();
131
+ if (action === 'remove' && interactive(options) && await line('Type the exact record name to remove it', 120, options.signal) !== current.name)
132
+ throw new CliError('canceled', 'The name did not match. Nothing was submitted.', 130);
133
+ await approve(options, `${action} ${terminalText(current.name)}? ${action === 'remove'
134
+ ? 'Active work must finish first. Saved evidence follows its retention policy; no real-account effects are erased.'
135
+ : action === 'archive' ? 'This moves the record to Archived history and does not stop active work.' : 'No work will restart.'}`);
136
+ const intent = { schema: 'firedrill.resource-intent.v1', actor: scope.owner, workspace: scope.workspace,
137
+ key: options.request !== undefined ? identifier(options.request) : randomUUID(), body: { kind, id, action: action,
138
+ basis: current.revision, confirm_name: current.name, retain_saved_data: true } };
139
+ await save(intent, scope, kind);
140
+ await submit(intent, scope, options, output);
141
+ }
@@ -0,0 +1,55 @@
1
+ import { api } from './auth.js';
2
+ import { CliError } from './http.js';
3
+ import { selectOne, terminalText } from './prompts.js';
4
+ import { selectedWorld } from './world-access.js';
5
+ const text = (value, label) => `${value.request_key}\n${value.state === 'accepted'
6
+ ? value.resource_available ? `${value.name} · Revision ${value.revision}\n${value.item_id}` : `Saved change; ${label} no longer available.`
7
+ : value.state === 'withdrawn' ? 'Canceled unsaved request. No revision will be created by it.' : 'Not received yet. This is not proof that the original request cannot arrive.'}`;
8
+ const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
9
+ export async function revisionRequests(kind, args, options, output) {
10
+ const label = kind === 'scenario' ? 'scenario' : 'criterion', command = kind === 'scenario' ? 'scenarios' : 'criteria';
11
+ const invalid = () => new CliError(`${kind}_request_invalid`, `The original ${label} request could not be confirmed. No change was repeated.`);
12
+ const action = args[0];
13
+ if (!['requests', 'request', 'withdraw-request'].includes(action) || args.length !== (action === 'requests' ? 1 : 2)
14
+ || action !== 'requests' && (options.page !== undefined || options.search !== undefined || options.status !== undefined)
15
+ || action !== 'withdraw-request' && options.approve)
16
+ throw new CliError('arguments_invalid', `Use ${command} requests, request <key> or withdraw-request <key> with world/workspace scope.`, 2);
17
+ const key = args[1] ?? '';
18
+ if (action !== 'requests' && !/^[A-Za-z0-9][A-Za-z0-9:._-]{0,127}$/.test(key))
19
+ throw invalid();
20
+ const page = options.page ?? '1', query = options.search ?? '', status = options.status ?? '';
21
+ if (!/^[1-9][0-9]{0,8}$/.test(page) || query.length > 200 || !['', 'accepted', 'withdrawn'].includes(status))
22
+ throw new CliError('arguments_invalid', 'Use a positive page, a search of up to 200 characters and accepted or withdrawn status.', 2);
23
+ const interactive = !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
24
+ if (action === 'withdraw-request' && !options.approve) {
25
+ if (!interactive)
26
+ throw new CliError('approval_required', 'Use --approve-change to cancel this exact unsaved request. An accepted change is only inspected.', 2);
27
+ if (!await selectOne('Cancel this unsaved request? A change already saved will not be undone.', [
28
+ { value: false, label: 'Keep request' }, { value: true, label: 'Cancel unsaved request' }
29
+ ], options.signal))
30
+ return;
31
+ }
32
+ const scope = await selectedWorld({ ...options, interactive });
33
+ const auth = { actor: scope.owner, workspace: scope.workspace, signal: options.signal };
34
+ const checked = (value) => {
35
+ if (!value || value.schema !== `firedrill.${kind}-change-receipt.v1` || value.actor_id !== scope.owner
36
+ || value.workspace_id !== scope.workspace || value.world_id !== scope.world || key && value.request_key !== key
37
+ || !['not_received', 'accepted', 'withdrawn'].includes(value.state) || typeof value.resource_available !== 'boolean'
38
+ || value.resource_available && (value.state !== 'accepted' || typeof value.item_id !== 'string'
39
+ || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value.item_id))
40
+ || !value.resource_available && value.item_id !== null)
41
+ throw invalid();
42
+ return value;
43
+ };
44
+ if (action === 'requests') {
45
+ const queryParams = new URLSearchParams({ page, q: query, status });
46
+ const result = await api(`${scope.prefix}/${kind}-requests?${queryParams}`, auth);
47
+ const next = `firedrill ${command} requests --world ${scope.world} --workspace ${scope.workspace} --page ${result.page + 1}${query ? ' --search ' + quote(query) : ''}${status ? ' --status ' + status : ''}${options.json ? ' --json' : ''}`;
48
+ output(result, (result.results.map(value => text(checked(value), label).split('\n').map(terminalText).join('\n')).join('\n\n') || `No matching ${label} changes.`)
49
+ + `\nPage ${result.page}, ${result.count} changes.${result.next ? '\nContinue: ' + next : ''}`);
50
+ return;
51
+ }
52
+ const body = { request_key: key };
53
+ const value = checked(await api(`${scope.prefix}/${kind}-request${action === 'request' ? '?request_key=' + encodeURIComponent(key) : ''}`, action === 'request' ? auth : { ...auth, method: 'POST', body }));
54
+ output(value, text(value, label).split('\n').map(terminalText).join('\n'));
55
+ }
@@ -0,0 +1,53 @@
1
+ import { api } from './auth.js';
2
+ import { CliError } from './http.js';
3
+ import { withUnstartedLocalClaim } from './runner-ownership.js';
4
+ import { hasLocalContainerJournal } from './runner-container.js';
5
+ import { savedLocalPermission } from './runner-grants.js';
6
+ import { syncClosingReceipt } from './runner-completion.js';
7
+ import { discardPrivateDataInputs } from './runner-inputs.js';
8
+ import { localClosingReceiptMatches } from '../../contracts/local-evidence-contracts.js';
9
+ /** Close only a retained pre-launch claim. No Docker/provider absence probe,
10
+ * replacement grant, target invocation or lease-expiry inference is involved. */
11
+ export async function abandonLocalAssignment(context, device, job, signal) {
12
+ return withUnstartedLocalClaim(context, device, job, signal, async (journal, claim, fence, active) => {
13
+ const lease = journal.lease, attempt = journal.attempt;
14
+ if (await hasLocalContainerJournal(context, lease))
15
+ throw new CliError('local_resource_recovery_required', `A resource journal exists for ${lease}. Use runner cleanup and evidence sync instead of abandoning it as unstarted.`);
16
+ let image = journal.runtimeImage;
17
+ if (!image) {
18
+ const permission = await savedLocalPermission(context, journal.grantRequest, active);
19
+ if (permission.state === 'not_retained' || !permission.id || permission.id !== journal.grant || !permission.runtime?.image
20
+ || permission.device_key !== device || !permission.operations?.includes(claim.purpose)) {
21
+ throw new CliError('local_owner_identity_changed', 'The original runtime approval could not be reconciled. Nothing was closed.');
22
+ }
23
+ image = permission.runtime.image;
24
+ }
25
+ // Persist before any closing dispatch. Subsequent runner execute refuses
26
+ // this job even if receipt delivery is interrupted or returns uncertain.
27
+ await fence();
28
+ await discardPrivateDataInputs({ ...context, job, lease });
29
+ const path = `/api/workspaces/${context.workspace}/local-runners/${device}/jobs/${job}/claims/${journal.request}/completion`;
30
+ const receipt = await api(path, { actor: context.owner, workspace: context.workspace, signal: active });
31
+ const selected = { ...context, device, job, request: journal.request, lease, attempt, manifestSha256: receipt.manifest_sha256 };
32
+ if (!localClosingReceiptMatches(receipt, selected)
33
+ || receipt.manifest_sha256 !== null && !/^[a-f0-9]{64}$/.test(receipt.manifest_sha256)) {
34
+ throw new CliError('local_completion_unconfirmed', 'The original pre-launch closing receipt could not be reconciled. Keep its journal.');
35
+ }
36
+ const document = {
37
+ schema: claim.purpose === 'trial' ? 'firedrill.local-trial-container-receipt.v1'
38
+ : claim.purpose === 'world_session' ? 'firedrill.local-world-container-receipt.v1' : 'firedrill.local-container-receipt.v1',
39
+ operation_id: lease, container_id: null, name: 'fd-job-' + lease, image,
40
+ resource_state: 'not_dispatched', removed: false, launch: null, status: null, exit_code: null,
41
+ };
42
+ if (claim.purpose === 'trial')
43
+ document.evidence = null;
44
+ if (claim.purpose === 'world_session') {
45
+ if (!claim.world_session)
46
+ throw new CliError('local_owner_identity_changed', 'The original world-session identity is missing.');
47
+ document.world_session = claim.world_session;
48
+ document.session_released = false;
49
+ document.evidence = { schema: 'firedrill.local-world-closing-evidence.v1', native_status: null };
50
+ }
51
+ return syncClosingReceipt(context, { ...selected, document }, active, { wait: true });
52
+ });
53
+ }