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
package/README.md CHANGED
@@ -1,35 +1,191 @@
1
- # Firedrill
1
+ # Firedrill CLI
2
2
 
3
- Simulate AI agents with Firedrill. Your coding agent can operate simulations on
4
- your behalf. Start in your application's repository:
3
+ Firedrill simulates your AI agent before it meets real users. Your agent runs on
4
+ your machine as an ordinary local process. Firedrill keeps the reusable cloud
5
+ resources: worlds, synthetic users, scenarios, hosted tools, evaluations and
6
+ reports. The web app shows the same resources and progress as this CLI.
7
+
8
+ Use the CLI yourself, or let a coding agent operate it for you. Local execution
9
+ is not an operating-system sandbox. Approved commands run with your own user
10
+ permissions.
11
+
12
+ **Release candidate:** the proposed public package is `firedrill-run@3.0.0`.
13
+ It is not published or approved for public activation yet. The existing
14
+ unversioned package resolves to the older product. Do not use `@latest` to
15
+ install this candidate. Once the owner approves and publishes this release,
16
+ the intended commands are:
5
17
 
6
18
  ```sh
7
- npx firedrill-run
19
+ npx --yes firedrill-run@3.0.0 init
20
+ npx --yes firedrill-run@3.0.0 --help
8
21
  ```
9
22
 
10
- Choose any optional additions: CLI in this project or globally, a coding-agent
11
- MCP connection, Firedrill skills, and a TypeScript/Python SDK. Every step can be
12
- skipped; review before anything is installed. Then optionally sign in and select
13
- your project. `npx firedrill-run init` also offers the first-use additions before
14
- account setup. Return with `firedrill install`, or use `init --skip-install` to
15
- skip optional installation entirely. No simulation starts during setup.
23
+ Until then, use only an explicitly supplied private candidate tarball.
24
+ `firedrill --help` of that exact installed candidate is authoritative.
25
+
26
+ ## Requirements
27
+
28
+ - Node.js `>=22.23.4 <23`.
29
+ - macOS, Windows or Linux are source-supported targets. A release qualification
30
+ must name the actual OS/architecture used; support is not proof each platform
31
+ was exercised. Native keyring availability also depends on its platform build.
32
+ - An OS credential store for persistent sign-in:
33
+ - macOS: Keychain;
34
+ - Windows: Credential Manager;
35
+ - desktop Linux: Secret Service (GNOME Keyring, KWallet or KeePassXC).
36
+
37
+ Without a credential store, for example on a headless or remote Linux machine,
38
+ use session-only sign-in. Credentials are never written to a file.
39
+ Source snapshot and candidate-copy commands also need an external `python3`
40
+ on PATH. Browser drivers need their own supported browser/driver installation.
41
+ These are not included in the npm package. Ordinary cloud metadata commands
42
+ do not require Docker or a bundled Python environment.
16
43
 
17
- Or install globally and use the `firedrill` command:
44
+ ## Sign in
18
45
 
19
46
  ```sh
20
- npm install --global firedrill-run
21
47
  firedrill init
22
48
  ```
23
49
 
24
- Requires Node.js 22 or later. This is the official short-name launcher for the
25
- Firedrill CLI. It invokes the exact declared CLI dependency in the same process;
26
- authentication and commands are supplied by that CLI. It does not run a
27
- local MCP server or bundle the private Firedrill runtime. Your application runs
28
- in your own repository or CI.
50
+ `init` signs you in with WorkOS device authorization, or starts account creation.
51
+ It then selects a workspace and checks this machine.
52
+
53
+ - `--json --no-browser` prints the verification link and code instead of
54
+ opening a browser. A person approves in the browser.
55
+ - `--session-only` keeps credentials in memory in a companion process. Keep that
56
+ terminal open while other `firedrill` commands use it. Stop it with Ctrl-C or
57
+ `firedrill logout`.
58
+ - `--persistent` uses the OS credential store. This is the default when one is
59
+ available.
60
+ - `--workspace <id>` selects a workspace without a prompt.
61
+
62
+ `firedrill status` shows the account, workspace, credential mode, companion and
63
+ local drivers. `firedrill logout` revokes terminal access and removes saved
64
+ credentials.
65
+
66
+ Terminal sign-in and remote MCP sign-in are separate authorizations. Account
67
+ creation does not authorize execution or payment.
68
+
69
+ ## Simulate your agent
70
+
71
+ | Step | Command |
72
+ | --- | --- |
73
+ | Register the agent: metadata only, no source, paths, commands or secrets | `firedrill register --descriptor descriptor.json --name "<name>"` |
74
+ | Find reusable resources | `firedrill library --kind persona\|scenario\|evaluation [--search <prefix>]`, `firedrill tools catalog` |
75
+ | Author a private draft | `firedrill library save --kind <kind> --file <path>` |
76
+ | Declare a world (nothing starts) | `firedrill world save --agent <id> --file world.json` |
77
+ | Review the plan and its cloud-work estimate | `firedrill plan --world <world-id>` |
78
+ | Apply after approval | `firedrill apply <plan-id> --approve --follow` |
79
+ | Enroll this machine once | `firedrill runner enroll` |
80
+ | Run the drills on this machine | `firedrill run --plan local-plan.json --descriptor descriptor.json --campaign <campaign-id>` |
81
+ | Watch progress | `firedrill watch <campaign> --follow` |
82
+ | Read results and evidence | `firedrill report <campaign>`, `… quality`, `… drill <drill> [results \| effects \| conversation \| evidence]` |
83
+ | Cancel | `firedrill simulation cancel <campaign> --approve-change` |
84
+
85
+ What to expect at each step:
86
+
87
+ - **Plan.** `plan` saves a review but starts no tools or drills and makes no
88
+ charge. It shows:
89
+ - pinned versions;
90
+ - how each dependency is reached;
91
+ - unmet requirements;
92
+ - the maximum cloud work.
93
+
94
+ Local target compute is never charged.
95
+ - **Apply.** `apply` reserves the approved estimate and prepares the plan. When
96
+ preparation is ready, it creates the campaign. No target starts at this step.
97
+ - **Run.** `run` shows the exact local plan: roots, argv, working directories,
98
+ environment variable names, readiness checks, driver and reset hooks. Nothing
99
+ runs until you approve that exact plan. In JSON mode, pass `--approve` only
100
+ after a person has approved it.
101
+ - **Report.** The report keeps four things separate:
102
+ - execution state;
103
+ - the observed outcome;
104
+ - per-criterion quality;
105
+ - required checks.
106
+
107
+ Missing evidence is never a zero or a pass.
108
+
109
+ ## JSON mode
110
+
111
+ Every command takes `--json`. In JSON mode the CLI:
112
+
113
+ - never prompts;
114
+ - opens no browser unless `--open` is given;
115
+ - prints one `{"type", "data"}` object per line.
116
+
117
+ Errors go to stderr with a nonzero exit code and a stable `code`.
118
+
119
+ ## Hosted tools outside a drill
120
+
121
+ Start reviewed catalog tools in the cloud for a set time and use them from a
122
+ local agent:
123
+
124
+ ```sh
125
+ firedrill tools runnable
126
+ firedrill tools start <version-id>... --seconds <n>
127
+ firedrill tools sessions
128
+ firedrill tools access <session-id> [--allow-writes]
129
+ firedrill tools session <session-id> stop
130
+ ```
131
+
132
+ - `start` shows a cost estimate first. After approval, run the printed
133
+ `--usage-quote <key> --approve-tool-change` command. It admits exactly the
134
+ quotes you approved.
135
+ - `access` serves loopback MCP and JSON URLs. They are read-only unless
136
+ `--allow-writes` is given.
137
+
138
+ Custom tool packages use `firedrill tools init`, `validate` and `push`. Pushing a
139
+ private version does not publish it or prove it runs.
140
+
141
+ ## Remote MCP
142
+
143
+ ```sh
144
+ firedrill mcp --agent codex|claude|other
145
+ ```
146
+
147
+ This prints connection guidance for the management MCP server. The MCP client
148
+ signs in through its own browser OAuth. Remote MCP manages cloud resources but
149
+ cannot run anything on this machine.
150
+
151
+ ## Safety
152
+
153
+ - Commands, argv, roots and hooks come only from the plan you approved. A later
154
+ change to argv or a hashed file voids that approval.
155
+ - A plan never:
156
+ - disables TLS verification;
157
+ - adds a certificate authority;
158
+ - sets a proxy;
159
+ - injects a loader (`LD_*`, `DYLD_*`).
160
+
161
+ Secret configuration is never a fixed plan value.
162
+ - Simulated dependencies are reached through per-drill loopback URLs. Short-lived
163
+ access stays in the CLI's memory and never reaches the target. Only `observed`
164
+ tool results are passed to the target. A pending or unknown outcome arrives as
165
+ an error with a recovery path.
166
+ - Attached services are only checked for readiness and are never signalled.
167
+ Launched services run in their own process group and are stopped with it.
168
+ - On Windows there are no process groups. Only each started process is stopped,
169
+ and processes it started may keep running.
170
+
171
+ ## Recovery
172
+
173
+ - Commands that change cloud state print a request key. Rerun with
174
+ `--request <key>` to read the original result. That never applies or charges
175
+ twice.
176
+ - A step whose outcome is unknown stays unknown and is never replayed under a new
177
+ key.
178
+ - If the CLI stops during a run, a lost local process is not resumed. Inspect the
179
+ campaign with `watch` and `report`, and follow the recovery it returns.
180
+
181
+ ## Advanced commands
29
182
 
30
- For non-interactive package installation use `npx --yes firedrill-run`. Coding
31
- agents and CI should also use the CLI's documented `--json --non-interactive`
32
- inputs for the action they intend to run. First-use browser consent stays human.
183
+ `firedrill help advanced` lists the remaining commands, including:
33
184
 
34
- [Documentation](https://docs.firedrill.run) ·
35
- [Connect your coding agent](https://firedrill.run/agents)
185
+ - catalog tool management (`tools`);
186
+ - tool sessions (`sessions`);
187
+ - runner devices and grants (`runner`);
188
+ - billing and usage (`billing`);
189
+ - CI connections (`ci`);
190
+ - team, workspace and settings;
191
+ - `login`, `signup` and `workspace` as separate steps of `init`.
package/bin/firedrill.mjs CHANGED
@@ -1,6 +1,30 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from "node:module";
3
- import { pathToFileURL } from "node:url";
4
-
5
- const require = createRequire(import.meta.url);
6
- await import(pathToFileURL(require.resolve("@firedrill-run/cli/bin/firedrill.mjs")).href);
2
+ // Keep this bootstrap plain JavaScript. An unsupported Node runtime must not
3
+ // try to parse TypeScript before it can explain the required toolchain.
4
+ const [major, minor, patch] = process.versions.node.split('.').map(Number);
5
+ const supported = major === 22 && (minor > 23 || minor === 23 && patch >= 4);
6
+ function report(code, message, exitCode = 1) {
7
+ process.stderr.write(process.argv.includes('--json')
8
+ ? JSON.stringify({ type: 'error', error: { code, message } }) + '\n'
9
+ : message + '\n');
10
+ process.exitCode = exitCode;
11
+ }
12
+ if (!supported) {
13
+ report('node_runtime_unsupported', 'Firedrill requires Node 22.23.4 or a later 22.x release. Update Node, then run the command again.');
14
+ } else {
15
+ let entry;
16
+ try { entry = await import('../dist/cli/src/main.js'); }
17
+ catch (error) {
18
+ if (error?.code === 'platform_configuration_invalid') {
19
+ report('platform_configuration_invalid', 'Use FIREDRILL_PLATFORM=sim-dev for isolated hosted development with all FIREDRILL_LOCAL_* variables unset. Unset all platform variables to select the public platform. Unknown or mixed profiles are refused.', 2);
20
+ } else if (error?.code === 'local_platform_configuration_invalid') {
21
+ report('local_platform_configuration_invalid', 'Local development requires FIREDRILL_LOCAL_PLATFORM=1 and paired HTTP loopback API and app origins with explicit ports. Arbitrary service URLs are refused.', 2);
22
+ } else {
23
+ report('cli_installation_unavailable', 'The CLI could not load. Restore the Firedrill installation, then try again.');
24
+ }
25
+ }
26
+ if (entry) {
27
+ try { await entry.main(); }
28
+ catch { report('command_unconfirmed', 'The command could not finish. No success is implied. Inspect its original request before trying again.'); }
29
+ }
30
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "schema": "firedrill.cli-assembly.v1",
3
+ "package": "firedrill-run",
4
+ "version": "3.0.0",
5
+ "entry": "cli/src/main.js",
6
+ "helpers": [
7
+ "cli/runtime/source_snapshot.py",
8
+ "cli/runtime/candidate_copy.py"
9
+ ],
10
+ "shared": [
11
+ "contracts/world-command.js"
12
+ ]
13
+ }
@@ -0,0 +1,395 @@
1
+ """Original, data-only candidate export. Never imports or runs customer source.
2
+
3
+ Only creates a new owned destination, or resumes that exact destination after
4
+ checking every existing file. It never overwrites or deletes destination files.
5
+ """
6
+ import argparse
7
+ import hashlib
8
+ import json
9
+ import os
10
+ from pathlib import Path, PurePosixPath
11
+ import re
12
+ import stat
13
+ import struct
14
+ import tempfile
15
+ import zipfile
16
+ import sqlite3
17
+ from contextlib import closing
18
+
19
+
20
+ class Refused(Exception):
21
+ pass
22
+
23
+
24
+ def canonical(value):
25
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
26
+
27
+
28
+ def private_json(path, maximum):
29
+ before = os.lstat(path)
30
+ if (not stat.S_ISREG(before.st_mode) or before.st_uid != os.getuid() or before.st_mode & 0o077
31
+ or not 0 < before.st_size <= maximum):
32
+ raise Refused()
33
+ fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
34
+ try:
35
+ actual = os.fstat(fd)
36
+ if (actual.st_dev, actual.st_ino, actual.st_size, actual.st_mtime_ns) != (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns):
37
+ raise Refused()
38
+ with os.fdopen(os.dup(fd), "rb") as stream:
39
+ body = stream.read(maximum + 1)
40
+ after = os.fstat(fd)
41
+ if (after.st_size, after.st_mtime_ns, after.st_ctime_ns) != (actual.st_size, actual.st_mtime_ns, actual.st_ctime_ns):
42
+ raise Refused()
43
+ return json.loads(body)
44
+ finally:
45
+ os.close(fd)
46
+
47
+
48
+ def receipt(path, value):
49
+ # A private cache file, never a customer destination. Atomic replacement
50
+ # retains recovery through a crash; source files use exclusive links below.
51
+ fd, temporary = tempfile.mkstemp(prefix="receipt-", dir=path.parent)
52
+ try:
53
+ with os.fdopen(fd, "wb") as stream:
54
+ stream.write(canonical(value))
55
+ stream.flush()
56
+ os.fsync(stream.fileno())
57
+ os.replace(temporary, path)
58
+ parent = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
59
+ try:
60
+ os.fsync(parent)
61
+ finally:
62
+ os.close(parent)
63
+ finally:
64
+ if os.path.exists(temporary):
65
+ os.unlink(temporary)
66
+
67
+
68
+ def safe_path(raw):
69
+ path = PurePosixPath(raw)
70
+ denied = {".git", ".hg", ".svn", ".aws", ".ssh", ".gcloud", "node_modules", ".venv", "venv", "__pycache__", ".next", ".turbo", "coverage", ".firedrill-simulation"}
71
+ if (not raw or len(raw) > 1024 or path.is_absolute() or path.as_posix() != raw or ".." in path.parts
72
+ or "\\" in raw or any(ord(one) < 32 or ord(one) == 127 for one in raw)
73
+ or ":" in path.parts[0] or denied.intersection(path.parts)
74
+ or any(one.startswith(".env") for one in path.parts)
75
+ or path.suffix.lower() in {".pem", ".key", ".p12", ".pfx", ".sqlite", ".sqlite3", ".db", ".kdbx"}
76
+ or path.name in {".npmrc", ".pypirc", ".netrc", "credentials.json", "id_rsa", "id_ed25519"}):
77
+ raise Refused()
78
+ return path
79
+
80
+
81
+ def directories(root, parts, create=False):
82
+ current = os.dup(root)
83
+ try:
84
+ for part in parts:
85
+ if create:
86
+ try:
87
+ os.mkdir(part, 0o700, dir_fd=current)
88
+ os.fsync(current)
89
+ except FileExistsError:
90
+ pass
91
+ new = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=current)
92
+ os.close(current)
93
+ current = new
94
+ return current
95
+ except BaseException:
96
+ os.close(current)
97
+ raise
98
+
99
+
100
+ def matching(fd, expected):
101
+ before = os.fstat(fd)
102
+ if (not stat.S_ISREG(before.st_mode) or before.st_uid != os.getuid() or before.st_size != expected["size"]
103
+ or stat.S_IMODE(before.st_mode) != expected["mode"]):
104
+ raise Refused()
105
+ with os.fdopen(os.dup(fd), "rb") as stream:
106
+ if hashlib.file_digest(stream, "sha256").hexdigest() != expected["sha256"]:
107
+ raise Refused()
108
+ after = os.fstat(fd)
109
+ if (before.st_size, before.st_mtime_ns, before.st_ctime_ns) != (after.st_size, after.st_mtime_ns, after.st_ctime_ns):
110
+ raise Refused()
111
+
112
+
113
+ class FileIndex:
114
+ """Disk-backed identity index. Each read retains at most one manifest row."""
115
+ def __init__(self, db):
116
+ self.db = db
117
+ self.count = 0
118
+ db.execute("CREATE TABLE files(path TEXT PRIMARY KEY, document TEXT NOT NULL, archive_seen INTEGER NOT NULL DEFAULT 0)")
119
+
120
+ def __len__(self):
121
+ return self.count
122
+
123
+ def __contains__(self, key):
124
+ return self.db.execute("SELECT 1 FROM files WHERE path=?", (key,)).fetchone() is not None
125
+
126
+ def get(self, key):
127
+ row = self.db.execute("SELECT document FROM files WHERE path=?", (key,)).fetchone()
128
+ return json.loads(row[0]) if row else None
129
+
130
+ def __getitem__(self, key):
131
+ result = self.get(key)
132
+ if result is None:
133
+ raise Refused()
134
+ return result
135
+
136
+ def add(self, value):
137
+ self.db.execute("INSERT INTO files(path,document) VALUES(?,?)", (value["path"], canonical(value).decode()))
138
+ self.count += 1
139
+
140
+ def items(self):
141
+ for path, document in self.db.execute("SELECT path,document FROM files ORDER BY path"):
142
+ yield path, json.loads(document)
143
+
144
+ def observe_archive(self, key):
145
+ if self.db.execute("UPDATE files SET archive_seen=1 WHERE path=? AND archive_seen=0", (key,)).rowcount != 1:
146
+ raise Refused()
147
+
148
+
149
+ class FolderIndex:
150
+ def __init__(self, db):
151
+ self.db = db
152
+ self.count = 0
153
+ db.execute("CREATE TABLE folders(path TEXT PRIMARY KEY)")
154
+
155
+ def __len__(self):
156
+ return self.count
157
+
158
+ def __contains__(self, key):
159
+ return self.db.execute("SELECT 1 FROM folders WHERE path=?", (key,)).fetchone() is not None
160
+
161
+ def add(self, key):
162
+ self.db.execute("INSERT INTO folders(path) VALUES(?)", (key,))
163
+ self.count += 1
164
+
165
+ def overlaps_files(self):
166
+ return self.db.execute("SELECT 1 FROM folders JOIN files USING(path) LIMIT 1").fetchone() is not None
167
+
168
+
169
+ def inspect_existing(root, files, folders, parts=(), seen=None):
170
+ seen = [0] if seen is None else seen
171
+ maximum = len(files) + len(folders)
172
+ with os.scandir(root) as entries:
173
+ for entry in entries:
174
+ name = entry.name
175
+ path = "/".join((*parts, name))
176
+ seen[0] += 1
177
+ if seen[0] > maximum:
178
+ raise Refused()
179
+ before = os.stat(name, dir_fd=root, follow_symlinks=False)
180
+ if stat.S_ISDIR(before.st_mode) and path in folders and before.st_uid == os.getuid():
181
+ child = os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root)
182
+ try:
183
+ inspect_existing(child, files, folders, (*parts, name), seen)
184
+ finally:
185
+ os.close(child)
186
+ elif path in files and stat.S_ISREG(before.st_mode):
187
+ fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW, dir_fd=root)
188
+ try:
189
+ matching(fd, files[path])
190
+ finally:
191
+ os.close(fd)
192
+ else:
193
+ raise Refused()
194
+ return seen[0]
195
+
196
+
197
+ def copy(args, db):
198
+ if os.name != "posix" or not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "getuid"):
199
+ raise Refused()
200
+ cache = Path(args.cache)
201
+ if not cache.is_absolute() or cache != cache.resolve(strict=True):
202
+ raise Refused()
203
+ owned = cache.stat()
204
+ if not stat.S_ISDIR(owned.st_mode) or owned.st_uid != os.getuid() or owned.st_mode & 0o077:
205
+ raise Refused()
206
+ intent = private_json(cache / "intent.json", 65536)
207
+ document = private_json(cache / "manifest.json", 65536)
208
+ identity = document["identity"]
209
+ if (intent["schema"] != "firedrill.native-candidate-copy.v1" or identity["schema"] != "firedrill.agent-change-export.v1"
210
+ or document.get("schema") != "firedrill.candidate-manifest-pages.v1"
211
+ or not re.fullmatch(r"[a-f0-9-]{36}", document.get("page_set", ""))
212
+ or identity["change_id"] != intent["change"] or identity["baseline_id"] != intent["campaign"]
213
+ or identity["scope_sha256"] != intent["scope_sha256"] or identity["draft_sha256"] != intent["draft_sha256"]
214
+ or type(identity["file_count"]) is not int or not 1 <= identity["file_count"] <= identity["capacity"]["files"]
215
+ or type(document.get("pages")) is not int or document["pages"] != (identity["file_count"] + 99) // 100):
216
+ raise Refused()
217
+ files, folders, total, folder_bytes, count = FileIndex(db), FolderIndex(db), 0, 0, 0
218
+ manifest_hash = hashlib.sha256(b"[")
219
+ for page in range(1, document["pages"] + 1):
220
+ packet = private_json(cache / f"manifest-{document['page_set']}-{page}.json", 1024 * 1024)
221
+ entries = packet.get("entries")
222
+ if (packet.get("page") != page or not isinstance(entries, list) or not 1 <= len(entries) <= 100
223
+ or page < document["pages"] and len(entries) != 100):
224
+ raise Refused()
225
+ for one in entries:
226
+ if (not isinstance(one, dict) or set(one) != {"path", "size", "sha256", "mode"} or type(one["size"]) is not int
227
+ or not 0 <= one["size"] <= identity["capacity"]["file_bytes"] or one["mode"] not in {0o644, 0o755}
228
+ or not re.fullmatch("[0-9a-f]{64}", one["sha256"])):
229
+ raise Refused()
230
+ path = safe_path(one["path"])
231
+ if count:
232
+ manifest_hash.update(b",")
233
+ manifest_hash.update(canonical(one))
234
+ count += 1
235
+ if count > identity["file_count"]:
236
+ raise Refused()
237
+ files.add(one)
238
+ total += one["size"]
239
+ for parent in path.parents:
240
+ name = parent.as_posix()
241
+ if name != "." and name not in folders:
242
+ folder_bytes += len(name.encode("utf-8")) + 64
243
+ if folder_bytes > identity["capacity"]["directory_bytes"]:
244
+ raise Refused()
245
+ folders.add(name)
246
+ manifest_hash.update(b"]")
247
+ if (count != identity["file_count"] or manifest_hash.hexdigest() != identity["files_sha256"]
248
+ or total != identity["expanded_bytes"] or total > identity["capacity"]["expanded_bytes"] or folders.overlaps_files()):
249
+ raise Refused()
250
+ destination = Path(intent["destination"])
251
+ if not destination.is_absolute() or destination.name in {"", ".", ".."}:
252
+ raise Refused()
253
+ parent = destination.parent.resolve(strict=True)
254
+ if destination != parent / destination.name:
255
+ raise Refused()
256
+ archive = cache / "source.zip"
257
+ fd = os.open(archive, os.O_RDONLY | os.O_NOFOLLOW)
258
+ with os.fdopen(fd, "rb") as raw:
259
+ stored = os.fstat(raw.fileno())
260
+ if (not stat.S_ISREG(stored.st_mode) or stored.st_uid != os.getuid() or stored.st_mode & 0o077
261
+ or stored.st_size != identity["snapshot"]["size"] or stored.st_size > identity["capacity"]["archive_bytes"]
262
+ or hashlib.file_digest(raw, "sha256").hexdigest() != identity["snapshot"]["sha256"]):
263
+ raise Refused()
264
+ # Bound the central directory read before ZipFile allocates entries.
265
+ class Bounded:
266
+ def __init__(self, stream):
267
+ self.stream = stream
268
+ def __getattr__(self, name):
269
+ return getattr(self.stream, name)
270
+ def read(self, size=-1):
271
+ maximum = identity["capacity"]["directory_bytes"]
272
+ if size > maximum:
273
+ raise Refused()
274
+ body = self.stream.read(maximum + 1 if size < 0 else size)
275
+ if len(body) > maximum:
276
+ raise Refused()
277
+ if size >= 46 and body.startswith(b"PK\x01\x02"):
278
+ at, count = 0, 0
279
+ while body[at:at + 4] == b"PK\x01\x02":
280
+ if len(body) - at < 46:
281
+ raise Refused()
282
+ name, extra, comment = struct.unpack_from("<HHH", body, at + 28)
283
+ at += 46 + name + extra + comment
284
+ count += 1
285
+ if at > len(body) or count > len(files):
286
+ raise Refused()
287
+ return body
288
+ raw.seek(0)
289
+ with zipfile.ZipFile(Bounded(raw)) as opened, tempfile.TemporaryDirectory(prefix=".firedrill-candidate-", dir=parent) as temporary:
290
+ members = opened.infolist()
291
+ if len(members) != len(files):
292
+ raise Refused()
293
+ staging = Path(temporary)
294
+ for member in members:
295
+ files.observe_archive(member.filename)
296
+ expected = files.get(member.filename)
297
+ mode = member.external_attr >> 16
298
+ if (expected is None or member.flag_bits & 1 or stat.S_IFMT(mode) not in {0, stat.S_IFREG}
299
+ or (0o755 if mode & 0o111 else 0o644) != expected["mode"] or member.file_size != expected["size"]):
300
+ raise Refused()
301
+ target = staging / safe_path(member.filename)
302
+ target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
303
+ hashed, size = hashlib.sha256(), 0
304
+ with opened.open(member) as incoming, target.open("xb") as output:
305
+ while chunk := incoming.read(1024 * 1024):
306
+ size += len(chunk)
307
+ if size > expected["size"]:
308
+ raise Refused()
309
+ hashed.update(chunk)
310
+ output.write(chunk)
311
+ output.flush()
312
+ os.fsync(output.fileno())
313
+ if size != expected["size"] or hashed.hexdigest() != expected["sha256"]:
314
+ raise Refused()
315
+ target.chmod(expected["mode"])
316
+ parentfd = os.open(parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
317
+ try:
318
+ receipt_path = cache / "copy-receipt.json"
319
+ proof = {"schema": "firedrill.candidate-copy-receipt.v1", "request_key": intent["key"],
320
+ "destination": str(destination), "source_id": identity["source_id"], "snapshot_sha256": identity["snapshot"]["sha256"],
321
+ "files_sha256": identity["files_sha256"]}
322
+ if receipt_path.exists():
323
+ previous = private_json(receipt_path, 65536)
324
+ if any(previous.get(key) != value for key, value in proof.items()):
325
+ raise Refused()
326
+ else:
327
+ # Never use exist_ok, checkout, reset, rename or replacement
328
+ # over a destination. A crash before the ownership receipt
329
+ # leaves an unknown folder that must not be claimed later.
330
+ os.mkdir(destination.name, 0o700, dir_fd=parentfd)
331
+ os.fsync(parentfd)
332
+ owned = os.stat(destination.name, dir_fd=parentfd, follow_symlinks=False)
333
+ previous = {**proof, "device": owned.st_dev, "inode": owned.st_ino, "complete": False}
334
+ receipt(receipt_path, previous)
335
+ root = os.open(destination.name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=parentfd)
336
+ try:
337
+ actual = os.fstat(root)
338
+ if (actual.st_dev, actual.st_ino, actual.st_uid) != (previous["device"], previous["inode"], os.getuid()):
339
+ raise Refused()
340
+ inspect_existing(root, files, folders)
341
+ for name, expected in files.items():
342
+ path = PurePosixPath(name)
343
+ containing = directories(root, path.parts[:-1], create=True)
344
+ try:
345
+ try:
346
+ # Unlinking our private staged name immediately
347
+ # removes the second link. No target file is
348
+ # truncated even after interrupted publication.
349
+ os.link(staging / name, path.name, dst_dir_fd=containing, follow_symlinks=False)
350
+ os.unlink(staging / name)
351
+ os.fsync(containing)
352
+ except FileExistsError:
353
+ fd = os.open(path.name, os.O_RDONLY | os.O_NOFOLLOW, dir_fd=containing)
354
+ try:
355
+ matching(fd, expected)
356
+ finally:
357
+ os.close(fd)
358
+ finally:
359
+ os.close(containing)
360
+ if inspect_existing(root, files, folders) != len(files) + len(folders):
361
+ raise Refused()
362
+ if os.stat(destination.name, dir_fd=parentfd, follow_symlinks=False).st_ino != actual.st_ino:
363
+ raise Refused()
364
+ receipt(receipt_path, {**previous, "complete": True})
365
+ finally:
366
+ os.close(root)
367
+ finally:
368
+ os.close(parentfd)
369
+ return {"schema": "firedrill.candidate-copy.v1", "destination": str(destination), "source_id": identity["source_id"],
370
+ "world_id": identity["world_id"], "agent_id": identity["agent_id"], "files_sha256": identity["files_sha256"],
371
+ "file_count": len(files), "target_executed": False, "original_files_changed": False}
372
+
373
+
374
+ def run(args):
375
+ # No retained full source manifest in memory. Private temporary SQLite is
376
+ # an index only, never execution state and never copied into the candidate.
377
+ with tempfile.TemporaryDirectory(prefix="firedrill-copy-index-") as directory:
378
+ with closing(sqlite3.connect(Path(directory) / "index.sqlite")) as db:
379
+ db.execute("PRAGMA cache_size=-2048")
380
+ db.execute("PRAGMA temp_store=FILE")
381
+ db.execute("PRAGMA mmap_size=0")
382
+ return copy(args, db)
383
+
384
+
385
+ if __name__ == "__main__":
386
+ parser = argparse.ArgumentParser()
387
+ parser.add_argument("--cache", required=True)
388
+ args = parser.parse_args()
389
+ try:
390
+ result = run(args)
391
+ print(json.dumps({"ok": True, "data": result}))
392
+ except Exception:
393
+ print(json.dumps({"ok": False, "error": {"code": "candidate_copy_refused",
394
+ "message": "The exact candidate copy could not be confirmed. Existing destination files were not overwritten or removed. Inspect the retained copy request before resuming."}}))
395
+ raise SystemExit(1)