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,257 @@
1
+ import { api } from './auth.js';
2
+ import { auth, choosePaged, interactive, line, query } from './configuration.js';
3
+ import { CliError } from './http.js';
4
+ import { selectOne } from './prompts.js';
5
+ import { selectedWorld } from './world-access.js';
6
+ import { identifier } from './world-contracts.js';
7
+ import { readSavedRecords } from './saved-records.js';
8
+ const kinds = { workflows: 'workflow', events: 'event', rules: 'rule', collections: 'collection' };
9
+ const number = (value) => new Intl.NumberFormat().format(value);
10
+ const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
11
+ const pathName = (entry) => entry.kind === 'alternative' ? `Alternative ${entry.number}` : 'Reference path';
12
+ function nextCommand(context, action, args, page, search) {
13
+ return `firedrill inspect ${action}${args.length ? ' ' + args.map(quote).join(' ') : ''} --world ${context.world} --workspace ${context.workspace} --page ${page}${search ? ' --search ' + quote(search) : ''}`;
14
+ }
15
+ function summary(value) {
16
+ const result = [`${value.name}\n${value.id} · Revision ${value.revision}${value.current ? '' : ' · Historical declaration'}`];
17
+ if (value.kind === 'collection') {
18
+ result.push('Declared agent schema, not observed database records or relationships.');
19
+ if (value.content.declared_record_count !== null && value.content.declared_record_count !== undefined)
20
+ result.push(`Declared records: ${number(value.content.declared_record_count)}`);
21
+ if (value.content.schema_presentation_complete !== true)
22
+ result.push('Some authored fields may be omitted or redacted in this saved declaration.');
23
+ }
24
+ else if (value.kind === 'rule') {
25
+ result.push(value.content.statement ?? 'No rule statement was retained.');
26
+ result.push(value.content.origin === 'derived' && value.provenance.origin === 'derived' ? 'Identified from the agent contract. Not observed behavior.'
27
+ : value.content.origin === 'generated' && value.provenance.origin === 'generated' ? 'Proposed rule. Not observed behavior.' : 'Origin is shown in the retained source metadata.');
28
+ }
29
+ else if (value.kind === 'workflow' && value.content.schema === 'firedrill.workflow.v1') {
30
+ result.push(`${number(value.content.path_count ?? 0)} authored reference paths. Actual runs can take a different valid path.`);
31
+ }
32
+ else if (value.kind === 'event' && value.content.schema === 'firedrill.event-schedule.v1') {
33
+ result.push(`${number(value.content.event_count ?? 0)} scheduled record changes across ${number(value.content.advance_count ?? 0)} turn boundaries.`, `Schedule start: ${value.content.start_at ?? 'Not recorded'}`, 'These are authored positions, not elapsed time or proof of application.');
34
+ }
35
+ else
36
+ result.push('Inspectable path or schedule metadata was not retained.');
37
+ if (!value.source_current)
38
+ result.push("This declaration does not match the world's current source revision.");
39
+ if (value.provenance.source_revision)
40
+ result.push(`Source revision: ${value.provenance.source_revision}`);
41
+ if (value.provenance.source_path)
42
+ result.push(`Source path: ${value.provenance.source_path}${value.provenance.line_start ? ':' + value.provenance.line_start : ''}`);
43
+ if (value.content.gaps?.length)
44
+ result.push(...value.content.gaps);
45
+ return result.join('\n\n');
46
+ }
47
+ function entryText(entry, view) {
48
+ if (view === 'paths' && 'scenario_name' in entry)
49
+ return `${entry.position}. ${entry.scenario_name}\n${pathName(entry)} · ${number(entry.step_count)} steps`;
50
+ if (view === 'steps' && 'tool' in entry)
51
+ return `${entry.position}. ${entry.tool || 'Operation name not recorded'}\nInputs: ${entry.input_fields.join(', ') || 'None declared'}\nTrusted state fields: ${entry.trusted_fields.join(', ') || 'None declared'}`
52
+ + ((entry.input_field_count ?? 0) > (entry.input_fields?.length ?? 0) || (entry.trusted_field_count ?? 0) > (entry.trusted_fields?.length ?? 0) ? '\nSome field names are omitted in this view.' : '');
53
+ if (view !== 'events' || !('offset_ms' in entry))
54
+ throw new CliError('declaration_response_invalid', 'The declaration response does not match its selected view. Open it again.');
55
+ return `${entry.id}\n${entry.action} · ${entry.collection}\nBefore turn ${number(entry.before_turn + 1)} · Offset ${number(entry.offset_ms)} ms\nKey fields: ${entry.key_fields.join(', ') || 'None'}\nChanged fields: ${entry.value_fields.join(', ') || 'None'}`
56
+ + ((entry.key_field_count ?? 0) > (entry.key_fields?.length ?? 0) || (entry.value_field_count ?? 0) > (entry.value_fields?.length ?? 0) ? '\nSome field names are omitted in this view.' : '')
57
+ + (entry.condition ? `\nCondition: ${entry.condition.exists ? 'Matching record in' : 'Record absent from'} ${entry.condition.collection}\nCondition key fields: ${entry.condition.key_fields.join(', ')}\nMatching fields: ${entry.condition.match_fields.join(', ') || 'None'}\nChecked before each conversation turn from this offset until applied.`
58
+ + (entry.condition.key_field_count > entry.condition.key_fields.length || entry.condition.match_field_count > entry.condition.match_fields.length ? '\nSome condition field names are omitted in this view.' : '') : '');
59
+ }
60
+ async function entries(context, id, path, options, output) {
61
+ if (path !== undefined && !/^[1-9][0-9]{0,8}$/.test(path))
62
+ throw new CliError('arguments_invalid', 'Use the original positive path position from the workflow.', 2);
63
+ const page = await api(`${context.prefix}/items/${id}/entries?${query(options)}${path ? '&path=' + path : ''}`, auth(context, options));
64
+ const text = page.results.map(row => entryText(row, page.view)).join('\n\n') || 'No matching entries.';
65
+ output('world_declaration_entries', page, `${text}\n\nDeclaration revision ${page.revision}.${!page.current ? ' Historical declaration.' : ''}${!page.source_current ? " Does not match the world's current source revision." : ''}\nPage ${page.page}, ${number(page.count)} matching entries.${page.next
66
+ ? '\nContinue: ' + nextCommand(context, 'entries', [id, ...(path ? [path] : [])], page.page + 1, options.search ?? '') : ''}`);
67
+ return page;
68
+ }
69
+ async function choosePath(context, item, options) {
70
+ let page = 1, search = options.search ?? '';
71
+ while (true) {
72
+ const result = await api(`${context.prefix}/items/${item.id}/entries?page=${page}&q=${encodeURIComponent(search)}`, auth(context, options));
73
+ if (result.view !== 'paths')
74
+ throw new CliError('declaration_response_invalid', 'The workflow response is not a path page. Open it again.');
75
+ const choices = result.results.map(row => ({ value: String(row.position), label: row.scenario_name ?? '', detail: `${pathName(row)} · ${number(row.step_count ?? 0)} steps` }));
76
+ if (page > 1)
77
+ choices.push({ value: 'previous', label: 'Previous page', detail: '' });
78
+ if (result.next)
79
+ choices.push({ value: 'next', label: 'Next page', detail: '' });
80
+ choices.push({ value: 'search', label: 'Search scenario paths', detail: '' }, { value: 'done', label: 'Done', detail: '' });
81
+ const selected = await selectOne('Scenario path', choices, options.signal);
82
+ if (selected === 'done')
83
+ return null;
84
+ if (selected === 'previous') {
85
+ page -= 1;
86
+ continue;
87
+ }
88
+ if (selected === 'next') {
89
+ page += 1;
90
+ continue;
91
+ }
92
+ if (selected === 'search') {
93
+ search = await line('Find a scenario (blank for all)', 200, options.signal);
94
+ page = 1;
95
+ continue;
96
+ }
97
+ const row = result.results.find(value => String(value.position) === selected);
98
+ if (row)
99
+ return row;
100
+ throw new CliError('selection_changed', 'The selected path changed. Open the workflow again.', 2);
101
+ }
102
+ }
103
+ async function readItem(context, id, options) {
104
+ const item = await api(`${context.prefix}/items/${identifier(id)}`, auth(context, options));
105
+ if (!['workflow', 'event', 'rule', 'collection'].includes(item.kind))
106
+ throw new CliError('selection_invalid', 'Inspect a workflow, rule, event schedule or collection. Use connections for service details.', 2);
107
+ return item;
108
+ }
109
+ function schemaPath(raw) {
110
+ try {
111
+ if (raw && raw.length > 65536)
112
+ throw new Error();
113
+ const value = raw ? JSON.parse(raw) : [];
114
+ if (!Array.isArray(value) || value.length > 32 || value.some(part => typeof part !== 'string' || part.length > 4096
115
+ || /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/.test(part)))
116
+ throw new Error();
117
+ return value;
118
+ }
119
+ catch {
120
+ throw new CliError('arguments_invalid', 'Use a JSON array of original schema path segments, or omit it for the root.', 2);
121
+ }
122
+ }
123
+ async function schemaEntries(context, id, path, options, output) {
124
+ const value = await api(`${context.prefix}/items/${identifier(id)}/schema?${query(options)}&path=${encodeURIComponent(JSON.stringify(path))}`, auth(context, options));
125
+ if (value.item_id !== id || JSON.stringify(value.path) !== JSON.stringify(path))
126
+ throw new CliError('collection_schema_changed', 'The schema could not be confirmed. Open it again.', 2);
127
+ const text = value.node.type === 'object' || value.node.type === 'array'
128
+ ? value.results.map(row => `${row.segment} · ${row.type}${row.type === 'object' || row.type === 'array' ? ''
129
+ : '\n' + (typeof row.value === 'string' ? row.value : JSON.stringify(row.value))}${row.omitted ? '\nValue shortened.' : ''}`).join('\n\n') || 'No matching fields.'
130
+ : (typeof value.node.value === 'string' ? value.node.value : JSON.stringify(value.node.value)) + (value.node.omitted ? '\nValue shortened.' : '');
131
+ output('world_collection_schema', value, `${text}\n\nDeclared schema revision ${value.revision}.${!value.current ? ' Historical declaration.' : ''}${!value.source_current ? ' Source revision changed.' : ''}${!value.presentation_complete ? '\nSome authored fields may be omitted or redacted.' : ''}\nPage ${value.page}, ${number(value.count)} matching fields.${value.next
132
+ ? '\nContinue: ' + nextCommand(context, 'schema', [id, JSON.stringify(path)], value.page + 1, options.search ?? '') : ''}`);
133
+ return value;
134
+ }
135
+ async function chooseSchema(context, item, options, output) {
136
+ if (!item.content.schema_type || item.content.schema_type === 'missing')
137
+ return;
138
+ let path = [], page = 1, search = '';
139
+ while (true) {
140
+ const value = await schemaEntries(context, item.id, path, { ...options, page: String(page), search }, output);
141
+ if (value.revision !== item.revision)
142
+ throw new CliError('collection_schema_changed', 'The schema changed. Open the collection again.', 2);
143
+ const choices = value.results.filter(row => row.type === 'object' || row.type === 'array').map((row, index) => ({ value: `field:${index}`, label: row.segment, detail: row.type }));
144
+ const fields = value.results.filter(row => row.type === 'object' || row.type === 'array');
145
+ if (path.length)
146
+ choices.push({ value: 'back', label: 'Back to parent', detail: '' }, { value: 'root', label: 'Schema root', detail: '' });
147
+ if (page > 1)
148
+ choices.push({ value: 'previous', label: 'Previous page', detail: '' });
149
+ if (value.next)
150
+ choices.push({ value: 'next', label: 'Next page', detail: '' });
151
+ if (value.node.type === 'object' || value.node.type === 'array')
152
+ choices.push({ value: 'search', label: 'Find a field', detail: '' });
153
+ choices.push({ value: 'done', label: 'Done', detail: '' });
154
+ const selected = await selectOne('Schema', choices, options.signal);
155
+ if (selected === 'done')
156
+ return;
157
+ if (selected === 'previous') {
158
+ page -= 1;
159
+ continue;
160
+ }
161
+ if (selected === 'next') {
162
+ page += 1;
163
+ continue;
164
+ }
165
+ if (selected === 'search') {
166
+ search = await line('Find a field (blank for all)', 200, options.signal);
167
+ page = 1;
168
+ continue;
169
+ }
170
+ if (selected === 'root')
171
+ path = [];
172
+ else if (selected === 'back')
173
+ path = path.slice(0, -1);
174
+ else {
175
+ const field = fields[Number(selected.slice(6))];
176
+ if (!field)
177
+ throw new CliError('selection_changed', 'The schema field changed. Open it again.', 2);
178
+ path = [...path, field.segment];
179
+ }
180
+ page = 1;
181
+ search = '';
182
+ }
183
+ }
184
+ export async function inspectCommand(args, options, output) {
185
+ query(options);
186
+ if (/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/.test(options.search ?? '')) {
187
+ throw new CliError('arguments_invalid', 'Use search text without terminal control characters.', 2);
188
+ }
189
+ const action = args[0] ?? 'choose';
190
+ if (action === 'data' || action === 'capture') {
191
+ const captured = action === 'capture';
192
+ if (args.length < (captured ? 3 : 2) || args.length > (captured ? 5 : 4))
193
+ throw new CliError('arguments_invalid', 'Use inspect data <capture> [collection] [row-position] or inspect capture <session> <capture> [collection] [row-position].', 2);
194
+ const context = await selectedWorld({ workspace: options.workspace, world: options.world, expectedOwner: options.expectedOwner,
195
+ interactive: interactive(options), signal: options.signal, openBrowser: options.openBrowser });
196
+ const capture = identifier(args[captured ? 2 : 1]);
197
+ const prefix = captured ? `${context.prefix}/runtime-sessions/${identifier(args[1])}/observations/${capture}` : context.prefix;
198
+ return readSavedRecords(prefix, capture, args[captured ? 3 : 2], args[captured ? 4 : 3], { actor: context.owner, workspace: context.workspace, signal: options.signal, page: options.page, search: options.search, json: options.json }, `firedrill inspect ${captured ? 'capture ' + identifier(args[1]) : 'data'} ${capture} --world ${context.world} --workspace ${context.workspace}`, output);
199
+ }
200
+ if (!['choose', 'show', 'entries', 'schema', ...Object.keys(kinds)].includes(action)
201
+ || Object.hasOwn(kinds, action) && args.length !== 1 || action === 'show' && args.length !== 2
202
+ || ['entries', 'schema'].includes(action) && (args.length < 2 || args.length > 3) || action === 'choose' && args.length) {
203
+ throw new CliError('arguments_invalid', 'Use inspect, inspect workflows|rules|events|collections, inspect show <id>, inspect entries <id> [path], or inspect schema <id> [JSON path].', 2);
204
+ }
205
+ if (action === 'choose' && !interactive(options))
206
+ throw new CliError('selection_required', 'Use inspect workflows|rules|events|collections or show|entries|schema <id> with --json for automation.', 2);
207
+ if (action === 'show' && (options.page !== undefined || options.search !== undefined))
208
+ throw new CliError('arguments_invalid', 'Page and search apply to a list, not a single summary.', 2);
209
+ if (action === 'choose' && options.page !== undefined)
210
+ throw new CliError('arguments_invalid', 'Use the page choices in guided inspection, or pass a declaration list command with --page.', 2);
211
+ if (action === 'entries' && args[2] !== undefined && !/^[1-9][0-9]{0,8}$/.test(args[2]))
212
+ throw new CliError('arguments_invalid', 'Use the original positive path position from the workflow.', 2);
213
+ if (['show', 'entries', 'schema'].includes(action))
214
+ identifier(args[1]);
215
+ const selectedSchemaPath = action === 'schema' ? schemaPath(args[2]) : [];
216
+ const context = await selectedWorld({ workspace: options.workspace, world: options.world, expectedOwner: options.expectedOwner,
217
+ interactive: interactive(options), signal: options.signal, openBrowser: options.openBrowser });
218
+ if (Object.hasOwn(kinds, action)) {
219
+ const page = await api(`${context.prefix}/items?kind=${kinds[action]}&${query(options)}`, auth(context, options));
220
+ output('world_declarations', page, (page.results.map(row => `${row.name}\n${row.id} · Revision ${row.revision}`).join('\n\n') || 'No matching declarations.')
221
+ + `\n\nPage ${page.page}, ${number(page.count)} matching declarations.${page.next ? '\nContinue: ' + nextCommand(context, action, [], page.page + 1, options.search ?? '') : ''}`);
222
+ return;
223
+ }
224
+ if (action === 'show') {
225
+ const item = await readItem(context, args[1], options);
226
+ output('world_declaration', item, summary(item));
227
+ return;
228
+ }
229
+ if (action === 'entries') {
230
+ await entries(context, identifier(args[1]), args[2], options, output);
231
+ return;
232
+ }
233
+ if (action === 'schema') {
234
+ await schemaEntries(context, identifier(args[1]), selectedSchemaPath, options, output);
235
+ return;
236
+ }
237
+ const kind = await selectOne('Inspect world', [{ value: 'workflow', label: 'Workflows' }, { value: 'rule', label: 'Rules' }, { value: 'event', label: 'Event schedules' }, { value: 'collection', label: 'Declared collection schemas' }], options.signal);
238
+ const selected = await choosePaged('Declaration', `${context.prefix}/items?kind=${kind}`, context, options, item => ({ label: item.name, detail: `Revision ${item.revision}` }));
239
+ if (!selected)
240
+ return;
241
+ const item = await readItem(context, selected.id, options);
242
+ output('world_declaration', item, summary(item));
243
+ if (item.kind === 'collection') {
244
+ await chooseSchema(context, item, options, output);
245
+ return;
246
+ }
247
+ if (item.kind === 'rule')
248
+ return;
249
+ if (item.kind === 'event') {
250
+ await entries(context, item.id, undefined, { ...options, page: '1', search: '' }, output);
251
+ return;
252
+ }
253
+ let path;
254
+ while ((path = await choosePath(context, item, options))) {
255
+ await entries(context, item.id, String(path.position), { ...options, page: '1', search: '' }, output);
256
+ }
257
+ }
@@ -0,0 +1,103 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { api, openBrowser } from './auth.js';
3
+ import { CliError } from './http.js';
4
+ import { selectedWorkspace, selectedWorld } from './world-access.js';
5
+ import { identifier } from './world-contracts.js';
6
+ import { selectOne } from './prompts.js';
7
+ import { workspaceLink } from './browser-links.js';
8
+ import { rememberRecipe, retainedRecipe } from './recipe-intents.js';
9
+ import { recipeNameMatches, recipeMatches, recipePageMatches, recipeRequestMatches } from '../../contracts/world-recipe-contracts.js';
10
+ export async function worldRecipes(args, options, output, selected) {
11
+ const action = args[0] ?? 'list', interactive = !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
12
+ if (!['list', 'save', 'request', 'show', 'use', 'archive'].includes(action) || args.length > 2
13
+ || ['list', 'save'].includes(action) && args.length > 1 || !['list', 'save'].includes(action) && args.length !== 2
14
+ || options.name !== undefined && action !== 'save' || options.request !== undefined && action !== 'save'
15
+ || options.approve !== undefined && !['save', 'archive'].includes(action)
16
+ || (options.page !== undefined || options.search !== undefined) && action !== 'list'
17
+ || options.world !== undefined && !selected && !['save', 'request'].includes(action))
18
+ throw new CliError('arguments_invalid', 'Use recipes list|save|request <request-key>|show <id>|use <id>|archive <id>.', 2);
19
+ const scope = selected ?? await selectedWorkspace({ workspace: options.workspace, interactive, signal: options.signal, openBrowser: options.openBrowser });
20
+ const auth = { actor: scope.owner, workspace: scope.workspace, signal: options.signal }, base = `/api/workspaces/${scope.workspace}/world-recipes`;
21
+ const read = (row) => { if (!recipeMatches(row, scope.workspace))
22
+ throw new CliError('world_recipe_unconfirmed', 'The saved recipe could not be confirmed. Inspect its original request before saving another.'); return row; };
23
+ const confirm = async (message) => {
24
+ if (options.approve)
25
+ return;
26
+ if (!interactive || !await selectOne(message, [{ value: false, label: 'Not now' }, { value: true, label: 'Approve' }], options.signal))
27
+ throw new CliError('recipe_approval_required', 'Review the recipe metadata, then use --approve-change. No simulation has started.', 2);
28
+ };
29
+ if (action === 'list') {
30
+ const page = Number(options.page ?? '1'), search = options.search ?? '';
31
+ if (!Number.isSafeInteger(page) || page < 1 || page > 999999999 || search.length > 200)
32
+ throw new CliError('query_invalid', 'Choose a valid page and a shorter recipe search.', 2);
33
+ const row = await api(`${base}?${new URLSearchParams({ page: String(page), q: search })}`, auth);
34
+ if (!recipePageMatches(row, scope.workspace, page))
35
+ throw new CliError('world_recipe_unconfirmed', 'This recipe page could not be confirmed.');
36
+ output('world_recipes', row, row.results.map(one => `${one.id} ${one.name} ${one.available ? 'Current' : 'Review changed inputs'}`).join('\n') || 'No saved world recipes.');
37
+ return;
38
+ }
39
+ if (action === 'save') {
40
+ const world = selected ?? await selectedWorld({ workspace: scope.workspace, world: options.world, expectedOwner: scope.owner,
41
+ interactive, signal: options.signal, openBrowser: options.openBrowser });
42
+ let body, key = options.request ? identifier(options.request) : randomUUID();
43
+ if (options.request) {
44
+ const key = identifier(options.request);
45
+ const previous = await api(world.prefix + `/recipe-requests/${key}`, auth);
46
+ if (!recipeRequestMatches(previous, scope.workspace, world.world, key))
47
+ throw new CliError('world_recipe_unconfirmed', 'The original recipe request could not be confirmed.');
48
+ if (previous.recipe) {
49
+ if (options.name !== undefined && previous.recipe.name !== options.name.trim())
50
+ throw new CliError('world_recipe_original_metadata_required', 'This original request saved a different recipe name. It was not changed.', 2);
51
+ output('world_recipe', previous.recipe, `Saved recipe ${previous.recipe.id}. No target execution started.`);
52
+ return;
53
+ }
54
+ const original = await retainedRecipe(world, key);
55
+ if (!original || options.name !== undefined && options.name.trim() !== original.body.name)
56
+ throw new CliError('world_recipe_original_metadata_required', 'This device does not retain the exact original save metadata. Inspect recipes request with its original key; a newer prepared world was not substituted.', 2);
57
+ body = original.body;
58
+ }
59
+ else {
60
+ const prepared = await api(world.prefix + '/preparation', auth);
61
+ const name = options.name?.trim() || prepared.name;
62
+ if (!recipeNameMatches(name))
63
+ throw new CliError('recipe_name_invalid', 'Use a short recipe name without credentials or private URLs.', 2);
64
+ body = { name, world_revision: prepared.basis.world_revision, configuration_revision: prepared.basis.config_revision };
65
+ }
66
+ output('world_recipe_review', { world_id: world.world, ...body, execution_started: false }, `${body.name}: world ${body.world_revision}, configuration ${body.configuration_revision}. Reuse does not preserve a running process or grant account access.`);
67
+ await confirm('Save this private prepared recipe for future simulation setup?');
68
+ await rememberRecipe(world, key, body);
69
+ output('world_recipe_request', { request_key: key }, `Recipe request ${key}. Retain it to continue the same save.`);
70
+ const row = read(await api(world.prefix + '/recipes', { ...auth, method: 'POST', idempotencyKey: key, body }));
71
+ if (row.world_id !== world.world || row.name !== body.name || row.world_revision !== body.world_revision
72
+ || row.configuration_revision !== body.configuration_revision)
73
+ throw new CliError('world_recipe_unconfirmed', 'Inspect the original recipe save before changing its inputs.');
74
+ output('world_recipe', row, `Saved recipe ${row.id}. No target execution started.`);
75
+ return;
76
+ }
77
+ if (action === 'request') {
78
+ const world = selected ?? await selectedWorld({ workspace: scope.workspace, world: options.world, expectedOwner: scope.owner,
79
+ interactive, signal: options.signal, openBrowser: options.openBrowser });
80
+ const key = identifier(args[1]), previous = await api(world.prefix + `/recipe-requests/${key}`, auth);
81
+ if (!recipeRequestMatches(previous, scope.workspace, world.world, key))
82
+ throw new CliError('world_recipe_unconfirmed', 'The original recipe request could not be confirmed.');
83
+ output('world_recipe_request', previous, previous.recipe ? `Saved recipe ${previous.recipe.id}.` : 'No save is retained for this original request. No simulation was started.');
84
+ return;
85
+ }
86
+ const id = identifier(args[1]), row = read(await api(`${base}/${id}`, auth));
87
+ if (row.id !== id)
88
+ throw new CliError('world_recipe_unconfirmed', 'The saved recipe identity could not be confirmed.');
89
+ if (action === 'show') {
90
+ output('world_recipe', row, `${row.name}: ${row.available ? 'current prepared version' : 'review changed or unavailable inputs'}.`);
91
+ return;
92
+ }
93
+ if (action === 'archive') {
94
+ await confirm('Archive this saved recipe? Its world and simulation history remain unchanged.');
95
+ await api(`${base}/${id}`, { ...auth, method: 'DELETE' });
96
+ output('world_recipe_archived', { id }, 'Recipe archived.');
97
+ return;
98
+ }
99
+ if (!row.available || !row.setup_path)
100
+ throw new CliError('world_recipe_changed', 'This recipe no longer matches the world. Review the current world before using its setup.', 2);
101
+ const url = workspaceLink(scope.workspace, row.setup_path), opened = !options.json && options.openBrowser ? await openBrowser(url) : false;
102
+ output('world_recipe_setup', { recipe: row, world_id: row.world_id, url, opened, execution_started: false }, `Simulation setup: ${url}\nFrom the terminal, new simulations use firedrill plan, apply and run. Current permissions and a reviewed credit budget are still required.`);
103
+ }
@@ -0,0 +1,241 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { lstat, open, opendir, unlink } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { setTimeout as sleep } from 'node:timers/promises';
6
+ import { performance } from 'node:perf_hooks';
7
+ import { api } from './auth.js';
8
+ import { privateDirectory } from './credentials.js';
9
+ import { CliError } from './http.js';
10
+ import { identifier } from './world-contracts.js';
11
+ const PAGE_SIZE = 25, ACCEPTANCE_READ_LIMIT = 25;
12
+ const invalid = () => new CliError('request_journal_invalid', 'The original request metadata needs attention. It was not discarded or repeated.');
13
+ const prefix = (context) => `request-${identifier(context.owner)}-${identifier(context.workspace)}-${identifier(context.world)}-`;
14
+ const requestPath = (context, key) => `${context.prefix}/requests/${identifier(key)}?actor_id=${identifier(context.owner)}`;
15
+ function validBasis(basis) {
16
+ return !!basis && Object.keys(basis).sort().join(',') === 'config_revision,source_id,world_revision'
17
+ && typeof basis.source_id === 'string' && identifier(basis.source_id) === basis.source_id
18
+ && [basis.world_revision, basis.config_revision].every(one => Number.isSafeInteger(one) && one > 0);
19
+ }
20
+ function canonical(value) {
21
+ if (typeof value === 'number' && !Number.isSafeInteger(value))
22
+ throw new CliError('request_invalid', 'Use an exact integer in this request.');
23
+ if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
24
+ return JSON.stringify(value);
25
+ if (Array.isArray(value))
26
+ return '[' + value.map(canonical).join(',') + ']';
27
+ if (value && typeof value === 'object')
28
+ return '{' + Object.keys(value).sort().map(key => JSON.stringify(key) + ':' + canonical(value[key])).join(',') + '}';
29
+ throw new CliError('request_invalid', 'The world request contains an unsupported value.');
30
+ }
31
+ async function pathFor(context, key) {
32
+ return join(await privateDirectory(), `${prefix(context)}${identifier(key)}.json`);
33
+ }
34
+ async function retain(context, command) {
35
+ const result = { schema: 'firedrill.native-world-intent.v1', key: randomUUID(), owner: context.owner,
36
+ workspace: context.workspace, world: context.world, kind: command.kind,
37
+ digest: createHash('sha256').update(canonical(command)).digest('hex'), basis: command.basis, created_at: new Date().toISOString() };
38
+ const file = await open(await pathFor(context, result.key), 'wx', 0o600);
39
+ try {
40
+ await file.writeFile(JSON.stringify(result));
41
+ await file.sync();
42
+ }
43
+ finally {
44
+ await file.close();
45
+ }
46
+ // Only scope/key/digest/source basis are on disk. Message text, token and target evidence
47
+ // are never written to this journal. A lost initial reply remains unknown.
48
+ return result;
49
+ }
50
+ async function loadIntent(context, key) {
51
+ const path = await pathFor(context, key);
52
+ let stat;
53
+ try {
54
+ stat = await lstat(path);
55
+ }
56
+ catch (error) {
57
+ if (error.code === 'ENOENT')
58
+ return null;
59
+ throw error;
60
+ }
61
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 4096
62
+ || process.getuid && stat.uid !== process.getuid())
63
+ throw invalid();
64
+ let file;
65
+ try {
66
+ file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
67
+ }
68
+ catch (error) {
69
+ if (error.code === 'ENOENT')
70
+ return null;
71
+ throw error;
72
+ }
73
+ try {
74
+ const actual = await file.stat();
75
+ if (!actual.isFile() || actual.nlink !== 1 || actual.size !== stat.size || actual.size > 4096 || actual.dev !== stat.dev || actual.ino !== stat.ino
76
+ || process.getuid && actual.uid !== process.getuid() || process.platform !== 'win32' && (actual.mode & 0o077) !== 0)
77
+ throw new Error();
78
+ const bytes = Buffer.alloc(4097);
79
+ const { bytesRead } = await file.read(bytes, 0, bytes.length, 0);
80
+ if (bytesRead !== actual.size || bytesRead > 4096)
81
+ throw new Error();
82
+ const item = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, bytesRead)));
83
+ if (item.schema !== 'firedrill.native-world-intent.v1' || item.owner !== context.owner || item.workspace !== context.workspace
84
+ || item.world !== context.world || item.key !== identifier(key)
85
+ || !['message', 'build', 'stop_response', 'cancel_build', 'proposal'].includes(item.kind)
86
+ || typeof item.created_at !== 'string' || !Number.isFinite(Date.parse(item.created_at))
87
+ || !/^[a-f0-9]{64}$/.test(item.digest) || !validBasis(item.basis))
88
+ throw new Error();
89
+ return item;
90
+ }
91
+ catch {
92
+ throw invalid();
93
+ }
94
+ finally {
95
+ await file.close();
96
+ }
97
+ }
98
+ async function* intentKeys(context, signal) {
99
+ const scope = prefix(context), directory = await opendir(await privateDirectory());
100
+ for await (const entry of directory) {
101
+ signal?.throwIfAborted();
102
+ if (!entry.name.startsWith(scope) || !entry.name.endsWith('.json'))
103
+ continue;
104
+ const key = entry.name.slice(scope.length, -5);
105
+ if (!entry.isFile() || identifier(key) !== key)
106
+ throw invalid();
107
+ yield key;
108
+ }
109
+ }
110
+ export async function requestPage(context, after, signal) {
111
+ const cursor = after === undefined ? undefined : identifier(after), selected = [];
112
+ // Hold at most one page and its sentinel, not the private directory/history.
113
+ for await (const key of intentKeys(context, signal)) {
114
+ if (cursor && key <= cursor || selected.includes(key))
115
+ continue;
116
+ const position = selected.findIndex(value => key < value);
117
+ selected.splice(position < 0 ? selected.length : position, 0, key);
118
+ if (selected.length > PAGE_SIZE + 1)
119
+ selected.pop();
120
+ }
121
+ const results = [];
122
+ for (const key of selected.slice(0, PAGE_SIZE)) {
123
+ signal?.throwIfAborted();
124
+ results.push(await lookup(context, key, signal));
125
+ }
126
+ return { results, page_size: PAGE_SIZE, order: 'request_id',
127
+ next_after: selected.length > PAGE_SIZE ? selected[PAGE_SIZE - 1] : null };
128
+ }
129
+ function checked(context, key, result, original) {
130
+ if (!result || result.schema !== 'firedrill.world-command-receipt.v1' || result.actor_id !== context.owner
131
+ || result.workspace_id !== context.workspace || result.world_id !== context.world || result.request_key !== identifier(key)
132
+ || !['not_retained', 'pending', 'accepted', 'withdrawn', 'refused'].includes(result.state))
133
+ throw invalid();
134
+ if (['pending', 'accepted', 'refused'].includes(result.state)) {
135
+ if (!result.kind || !['message', 'build', 'stop_response', 'cancel_build', 'proposal'].includes(result.kind)
136
+ || typeof result.request_digest !== 'string' || !/^[a-f0-9]{64}$/.test(result.request_digest) || !validBasis(result.basis)
137
+ || original && (result.request_digest !== original.digest || result.kind !== original.kind
138
+ || canonical(result.basis) !== canonical(original.basis)))
139
+ throw invalid();
140
+ }
141
+ if (result.state === 'accepted') {
142
+ const receipt = result.receipt;
143
+ if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt))
144
+ throw invalid();
145
+ const uuid = (value) => typeof value === 'string' && identifier(value) === value;
146
+ if (result.kind === 'message' && (!uuid(receipt.message_id) || !uuid(receipt.conversation_id))
147
+ || result.kind === 'build' && !uuid(receipt.job_id)
148
+ || result.kind === 'proposal' && (!uuid(receipt.proposal_id) || receipt.job_id !== null && !uuid(receipt.job_id)
149
+ || !['applied', 'dismissed', 'queued', 'delivering', 'delivered', 'applying', 'not_applied', 'unknown'].includes(receipt.state ?? ''))
150
+ || result.kind === 'stop_response' && receipt.state !== 'stop_requested'
151
+ || result.kind === 'cancel_build' && typeof receipt.state !== 'string')
152
+ throw invalid();
153
+ }
154
+ return result;
155
+ }
156
+ export async function lookup(context, key, signal) {
157
+ const original = await loadIntent(context, key);
158
+ const result = checked(context, key, await api(requestPath(context, key), { actor: context.owner, workspace: context.workspace, signal }), original);
159
+ if (['accepted', 'withdrawn', 'refused'].includes(result.state))
160
+ await unlink(await pathFor(context, key)).catch(error => { if (error.code !== 'ENOENT')
161
+ throw error; });
162
+ return result;
163
+ }
164
+ export async function withdraw(context, key, signal) {
165
+ const original = await loadIntent(context, key);
166
+ let result;
167
+ try {
168
+ result = checked(context, key, await api(requestPath(context, key), { method: 'DELETE', actor: context.owner, workspace: context.workspace, signal }), original);
169
+ }
170
+ catch (error) {
171
+ signal?.throwIfAborted();
172
+ if (!(error instanceof CliError) || !['connection_uncertain', 'response_unavailable', 'world_request_accepted'].includes(error.code))
173
+ throw error;
174
+ // Recover only by GET. A lost withdrawal reply is never another DELETE.
175
+ result = await lookup(context, key, signal);
176
+ }
177
+ if (['accepted', 'withdrawn', 'refused'].includes(result.state))
178
+ await unlink(await pathFor(context, key)).catch(error => { if (error.code !== 'ENOENT')
179
+ throw error; });
180
+ return result;
181
+ }
182
+ export async function submit(context, command, signal, report) {
183
+ let acceptanceReads = 0;
184
+ for await (const key of intentKeys(context, signal)) {
185
+ if (acceptanceReads >= ACCEPTANCE_READ_LIMIT)
186
+ throw new CliError('world_request_review_required', `More saved requests need a current receipt. Request ${key} has no confirmed receipt. No new action was submitted.`);
187
+ acceptanceReads += 1;
188
+ const state = await lookup(context, key, signal);
189
+ if (state.state === 'pending' || state.state === 'not_retained') {
190
+ report(state);
191
+ throw new CliError('world_request_unresolved', `Resolve request ${key} with request status or withdraw before sending another action.`);
192
+ }
193
+ }
194
+ // The browser and terminal share the same server-side request lane. This is
195
+ // a bounded metadata read, never a recovered message/body or another PUT.
196
+ const page = await api(`${context.prefix}/requests?actor_id=${identifier(context.owner)}&state=pending`, { actor: context.owner, workspace: context.workspace, signal });
197
+ if (!page || !Array.isArray(page.results) || page.results.length > 1 || page.has_more !== false)
198
+ throw invalid();
199
+ if (page.results.length) {
200
+ const previous = checked(context, page.results[0].request_key, page.results[0], null);
201
+ if (previous.state !== 'pending')
202
+ throw invalid();
203
+ report(previous);
204
+ throw new CliError('world_request_unresolved', `Resolve request ${previous.request_key} with request status or withdraw before sending another action.`);
205
+ }
206
+ signal.throwIfAborted();
207
+ if (!validBasis(command.basis))
208
+ throw invalid();
209
+ const intent = await retain(context, command);
210
+ report({ schema: 'firedrill.world-command-receipt.v1', actor_id: context.owner, workspace_id: context.workspace,
211
+ world_id: context.world, basis: command.basis, request_key: intent.key, kind: command.kind,
212
+ state: 'not_retained', request_digest: intent.digest });
213
+ let current;
214
+ try {
215
+ current = checked(context, intent.key, await api(requestPath(context, intent.key), { method: 'PUT', body: command, actor: context.owner, workspace: context.workspace, signal }), intent);
216
+ }
217
+ catch (error) {
218
+ // Even an unobserved acceptance is recovered by READ only. An absent read
219
+ // is not proof that the original command will never arrive.
220
+ if (signal.aborted)
221
+ throw error;
222
+ current = await lookup(context, intent.key, signal);
223
+ if (current.state === 'not_retained') {
224
+ throw new CliError('world_request_uncertain', `Request ${intent.key} has no retained receipt yet. Inspect or withdraw that exact request; it was not resubmitted.`);
225
+ }
226
+ }
227
+ if (current.request_digest && current.request_digest !== intent.digest)
228
+ throw new CliError('request_identity_changed', 'The saved request identity changed. No action was repeated.');
229
+ const deadline = performance.now() + 30000;
230
+ while (current.state === 'pending' && performance.now() < deadline) {
231
+ report(current);
232
+ await sleep(1000, undefined, { signal });
233
+ current = await lookup(context, intent.key, signal);
234
+ }
235
+ if (['accepted', 'withdrawn', 'refused'].includes(current.state)) {
236
+ await unlink(await pathFor(context, intent.key)).catch(error => { if (error.code !== 'ENOENT')
237
+ throw error; });
238
+ }
239
+ report(current);
240
+ return current;
241
+ }