firedrill-run 2.2.5 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (392) hide show
  1. package/README.md +178 -22
  2. package/bin/firedrill.mjs +29 -5
  3. package/dist/assembly.json +13 -0
  4. package/dist/cli/runtime/candidate_copy.py +395 -0
  5. package/dist/cli/runtime/source_snapshot.py +391 -0
  6. package/dist/cli/src/administration.js +295 -0
  7. package/dist/cli/src/agent-changes.js +496 -0
  8. package/dist/cli/src/assessment-requests.js +67 -0
  9. package/dist/cli/src/auth.js +465 -0
  10. package/dist/cli/src/billing-commerce-contracts.js +1 -0
  11. package/dist/cli/src/billing-usage.js +227 -0
  12. package/dist/cli/src/billing-wallet-contracts.js +1 -0
  13. package/dist/cli/src/billing.js +170 -0
  14. package/dist/cli/src/bootstrap-process-lock.js +136 -0
  15. package/dist/cli/src/browser-links.js +14 -0
  16. package/dist/cli/src/campaign-controls.js +122 -0
  17. package/dist/cli/src/campaign-diagnoses.js +118 -0
  18. package/dist/cli/src/campaign-reports.js +1215 -0
  19. package/dist/cli/src/campaign-world.js +156 -0
  20. package/dist/cli/src/candidate-copy.js +376 -0
  21. package/dist/cli/src/catalog-contracts.js +1 -0
  22. package/dist/cli/src/ci-access-contracts.js +1 -0
  23. package/dist/cli/src/ci-configuration-contracts.js +1 -0
  24. package/dist/cli/src/ci-coverage.js +64 -0
  25. package/dist/cli/src/ci-delivery-contracts.js +1 -0
  26. package/dist/cli/src/ci-result-contracts.js +1 -0
  27. package/dist/cli/src/ci-run-contracts.js +1 -0
  28. package/dist/cli/src/ci-setup-proposals.js +52 -0
  29. package/dist/cli/src/company-policies.js +229 -0
  30. package/dist/cli/src/completion.js +79 -0
  31. package/dist/cli/src/configuration-intents.js +277 -0
  32. package/dist/cli/src/configuration.js +369 -0
  33. package/dist/cli/src/continuous-simulation.js +277 -0
  34. package/dist/cli/src/coordinated-checkpoints.js +133 -0
  35. package/dist/cli/src/credential-mode.js +139 -0
  36. package/dist/cli/src/credentials.js +331 -0
  37. package/dist/cli/src/credit-format.js +8 -0
  38. package/dist/cli/src/dataset-contracts.js +1 -0
  39. package/dist/cli/src/dataset-export.js +154 -0
  40. package/dist/cli/src/dataset-reads.js +37 -0
  41. package/dist/cli/src/dependency-preparation.js +239 -0
  42. package/dist/cli/src/director-tool-setup.js +104 -0
  43. package/dist/cli/src/driver-review.js +225 -0
  44. package/dist/cli/src/evaluation-authoring.js +315 -0
  45. package/dist/cli/src/evidence-bundles.js +367 -0
  46. package/dist/cli/src/execution-proposals.js +36 -0
  47. package/dist/cli/src/http.js +302 -0
  48. package/dist/cli/src/local-container-boundary.js +38 -0
  49. package/dist/cli/src/local-container-policy.js +201 -0
  50. package/dist/cli/src/local-execution-setup.js +81 -0
  51. package/dist/cli/src/local-source-basis.js +73 -0
  52. package/dist/cli/src/local-source.js +66 -0
  53. package/dist/cli/src/local-value-store.js +91 -0
  54. package/dist/cli/src/local-values.js +163 -0
  55. package/dist/cli/src/login-organizations.js +70 -0
  56. package/dist/cli/src/main.js +1622 -0
  57. package/dist/cli/src/mcp-connection.js +109 -0
  58. package/dist/cli/src/native/bindings.js +137 -0
  59. package/dist/cli/src/native/canonical.js +370 -0
  60. package/dist/cli/src/native/cloud-resources.js +942 -0
  61. package/dist/cli/src/native/companion-channel.js +421 -0
  62. package/dist/cli/src/native/companion-ipc.js +473 -0
  63. package/dist/cli/src/native/contracts.js +218 -0
  64. package/dist/cli/src/native/control-plane.js +1354 -0
  65. package/dist/cli/src/native/credit-approval.js +15 -0
  66. package/dist/cli/src/native/dependency-proxy.js +1006 -0
  67. package/dist/cli/src/native/drill-cleanup.js +115 -0
  68. package/dist/cli/src/native/drill-runner.js +1562 -0
  69. package/dist/cli/src/native/drivers/action-trace.js +210 -0
  70. package/dist/cli/src/native/drivers/function-wrapper.js +152 -0
  71. package/dist/cli/src/native/drivers/http.js +442 -0
  72. package/dist/cli/src/native/drivers/index.js +47 -0
  73. package/dist/cli/src/native/drivers/jsonl-process.js +538 -0
  74. package/dist/cli/src/native/drivers/playwright.js +600 -0
  75. package/dist/cli/src/native/drivers/signed-event.js +244 -0
  76. package/dist/cli/src/native/drivers/types.js +578 -0
  77. package/dist/cli/src/native/evidence-filter.js +255 -0
  78. package/dist/cli/src/native/isolated-access.js +241 -0
  79. package/dist/cli/src/native/library-sharing.js +220 -0
  80. package/dist/cli/src/native/local-run.js +667 -0
  81. package/dist/cli/src/native/local-secrets.js +19 -0
  82. package/dist/cli/src/native/mcp-config.js +123 -0
  83. package/dist/cli/src/native/metadata-requests.js +120 -0
  84. package/dist/cli/src/native/observations.js +1159 -0
  85. package/dist/cli/src/native/owned-groups.js +46 -0
  86. package/dist/cli/src/native/plan.js +594 -0
  87. package/dist/cli/src/native/png.js +159 -0
  88. package/dist/cli/src/native/ports.js +43 -0
  89. package/dist/cli/src/native/readiness.js +139 -0
  90. package/dist/cli/src/native/reassessments.js +206 -0
  91. package/dist/cli/src/native/redaction.js +235 -0
  92. package/dist/cli/src/native/registration.js +155 -0
  93. package/dist/cli/src/native/roots.js +110 -0
  94. package/dist/cli/src/native/run-cleanup.js +199 -0
  95. package/dist/cli/src/native/scratch.js +117 -0
  96. package/dist/cli/src/native/screenshots.js +60 -0
  97. package/dist/cli/src/native/session.js +556 -0
  98. package/dist/cli/src/native/standalone-proxy.js +580 -0
  99. package/dist/cli/src/native/standalone-renewals.js +183 -0
  100. package/dist/cli/src/native/standalone-state.js +193 -0
  101. package/dist/cli/src/native/standalone-tools.js +187 -0
  102. package/dist/cli/src/native/supervisor.js +831 -0
  103. package/dist/cli/src/native/target-auth.js +161 -0
  104. package/dist/cli/src/native/target-state.js +330 -0
  105. package/dist/cli/src/native/tool-reuse.js +364 -0
  106. package/dist/cli/src/native/tool-setups.js +336 -0
  107. package/dist/cli/src/native/world-reuse.js +288 -0
  108. package/dist/cli/src/onboarding.js +23 -0
  109. package/dist/cli/src/priority-requests.js +70 -0
  110. package/dist/cli/src/priority.js +223 -0
  111. package/dist/cli/src/prompts.js +184 -0
  112. package/dist/cli/src/real-operations.js +221 -0
  113. package/dist/cli/src/recipe-intents.js +58 -0
  114. package/dist/cli/src/recorded-context-intents.js +59 -0
  115. package/dist/cli/src/recorded-context.js +248 -0
  116. package/dist/cli/src/recordings.js +69 -0
  117. package/dist/cli/src/registration-guide.js +39 -0
  118. package/dist/cli/src/report-exports.js +213 -0
  119. package/dist/cli/src/request-input.js +47 -0
  120. package/dist/cli/src/resource-lifecycle.js +141 -0
  121. package/dist/cli/src/revision-requests.js +55 -0
  122. package/dist/cli/src/runner-abandon.js +53 -0
  123. package/dist/cli/src/runner-campaign-controls.js +262 -0
  124. package/dist/cli/src/runner-claim-reads.js +36 -0
  125. package/dist/cli/src/runner-completion.js +65 -0
  126. package/dist/cli/src/runner-configuration.js +546 -0
  127. package/dist/cli/src/runner-container.js +794 -0
  128. package/dist/cli/src/runner-evidence-erasure.js +351 -0
  129. package/dist/cli/src/runner-evidence-privacy.js +37 -0
  130. package/dist/cli/src/runner-evidence-retention.js +161 -0
  131. package/dist/cli/src/runner-evidence.js +326 -0
  132. package/dist/cli/src/runner-execute.js +92 -0
  133. package/dist/cli/src/runner-execution-deadline.js +25 -0
  134. package/dist/cli/src/runner-grants.js +307 -0
  135. package/dist/cli/src/runner-identity.js +615 -0
  136. package/dist/cli/src/runner-inputs.js +308 -0
  137. package/dist/cli/src/runner-jobs.js +49 -0
  138. package/dist/cli/src/runner-model-transport.js +392 -0
  139. package/dist/cli/src/runner-ownership.js +332 -0
  140. package/dist/cli/src/runner-process-lock.js +175 -0
  141. package/dist/cli/src/runner-runtime.js +396 -0
  142. package/dist/cli/src/runner-session-channel.js +377 -0
  143. package/dist/cli/src/runner-trial-completion.js +34 -0
  144. package/dist/cli/src/runner-trial-journal.js +358 -0
  145. package/dist/cli/src/runner-trial-live.js +393 -0
  146. package/dist/cli/src/runner-trial-protocol.js +289 -0
  147. package/dist/cli/src/runner-trial-sync.js +110 -0
  148. package/dist/cli/src/runner-trial-transfer.js +107 -0
  149. package/dist/cli/src/runner-voice.js +40 -0
  150. package/dist/cli/src/runner-world-claim.js +21 -0
  151. package/dist/cli/src/runner-world-collector.js +159 -0
  152. package/dist/cli/src/runner-world-command-evidence.js +487 -0
  153. package/dist/cli/src/runner-world-commands.js +268 -0
  154. package/dist/cli/src/runner-world-completion.js +76 -0
  155. package/dist/cli/src/runner-world-erasure.js +95 -0
  156. package/dist/cli/src/runner-world-evidence-sync.js +91 -0
  157. package/dist/cli/src/runner-world-journal.js +510 -0
  158. package/dist/cli/src/runner-world-native-filters.js +134 -0
  159. package/dist/cli/src/runner-world-observations.js +50 -0
  160. package/dist/cli/src/runner-world-session-protocol.js +134 -0
  161. package/dist/cli/src/saved-records.js +40 -0
  162. package/dist/cli/src/scenario-authoring.js +212 -0
  163. package/dist/cli/src/scenario-discovery.js +216 -0
  164. package/dist/cli/src/scenario-paths.js +68 -0
  165. package/dist/cli/src/scenario-requests.js +4 -0
  166. package/dist/cli/src/service-receipts.js +105 -0
  167. package/dist/cli/src/simple/authoring.js +270 -0
  168. package/dist/cli/src/simple/credit-approval.js +16 -0
  169. package/dist/cli/src/simple/help.js +98 -0
  170. package/dist/cli/src/simple/init.js +61 -0
  171. package/dist/cli/src/simple/library-sharing.js +414 -0
  172. package/dist/cli/src/simple/metadata-requests.js +46 -0
  173. package/dist/cli/src/simple/native-report.js +841 -0
  174. package/dist/cli/src/simple/native-tools.js +780 -0
  175. package/dist/cli/src/simple/plan-apply.js +506 -0
  176. package/dist/cli/src/simple/reassessment.js +323 -0
  177. package/dist/cli/src/simple/register.js +123 -0
  178. package/dist/cli/src/simple/run.js +122 -0
  179. package/dist/cli/src/simple/status.js +49 -0
  180. package/dist/cli/src/simple/tool-renewal.js +308 -0
  181. package/dist/cli/src/simple/tool-reuse.js +526 -0
  182. package/dist/cli/src/simple/tool-start-catalog.js +100 -0
  183. package/dist/cli/src/simple/tool-state.js +426 -0
  184. package/dist/cli/src/simple/tools-author.js +222 -0
  185. package/dist/cli/src/simple/watch.js +28 -0
  186. package/dist/cli/src/simple/world-reuse.js +586 -0
  187. package/dist/cli/src/simulation-intents.js +406 -0
  188. package/dist/cli/src/simulation-run.js +1058 -0
  189. package/dist/cli/src/source-batch.js +322 -0
  190. package/dist/cli/src/source-connection.js +451 -0
  191. package/dist/cli/src/source-runtime.js +346 -0
  192. package/dist/cli/src/source-selection-intents.js +75 -0
  193. package/dist/cli/src/source-sets.js +214 -0
  194. package/dist/cli/src/target-models.js +109 -0
  195. package/dist/cli/src/tool-catalog.js +224 -0
  196. package/dist/cli/src/tool-command-contracts.js +1 -0
  197. package/dist/cli/src/tool-command-export.js +245 -0
  198. package/dist/cli/src/tool-connection-contracts.js +1 -0
  199. package/dist/cli/src/tool-connection-reads.js +23 -0
  200. package/dist/cli/src/tool-contributions.js +58 -0
  201. package/dist/cli/src/tool-data.js +116 -0
  202. package/dist/cli/src/tool-datasets.js +176 -0
  203. package/dist/cli/src/tool-group-contracts.js +1 -0
  204. package/dist/cli/src/tool-group-reads.js +26 -0
  205. package/dist/cli/src/tool-group-state.js +147 -0
  206. package/dist/cli/src/tool-groups.js +311 -0
  207. package/dist/cli/src/tool-instances.js +186 -0
  208. package/dist/cli/src/tool-lifecycle-contracts.js +1 -0
  209. package/dist/cli/src/tool-lifecycle-reads.js +30 -0
  210. package/dist/cli/src/tool-lifecycle.js +323 -0
  211. package/dist/cli/src/tool-package-contracts.js +1 -0
  212. package/dist/cli/src/tool-package-reads.js +41 -0
  213. package/dist/cli/src/tool-records.js +53 -0
  214. package/dist/cli/src/tool-runtime.js +96 -0
  215. package/dist/cli/src/tool-state-contracts.js +1 -0
  216. package/dist/cli/src/tool-state-reads.js +31 -0
  217. package/dist/cli/src/tools.js +306 -0
  218. package/dist/cli/src/trial-actions.js +93 -0
  219. package/dist/cli/src/trial-checkpoints.js +133 -0
  220. package/dist/cli/src/trial-checks.js +70 -0
  221. package/dist/cli/src/trial-state.js +197 -0
  222. package/dist/cli/src/vault.js +266 -0
  223. package/dist/cli/src/workspace-registration.js +383 -0
  224. package/dist/cli/src/workspace-team-contracts.js +1 -0
  225. package/dist/cli/src/world-access.js +231 -0
  226. package/dist/cli/src/world-agent.js +4 -0
  227. package/dist/cli/src/world-checkpoint-scopes.js +15 -0
  228. package/dist/cli/src/world-contracts.js +6 -0
  229. package/dist/cli/src/world-creation.js +201 -0
  230. package/dist/cli/src/world-datasets.js +207 -0
  231. package/dist/cli/src/world-inspection.js +257 -0
  232. package/dist/cli/src/world-recipes.js +103 -0
  233. package/dist/cli/src/world-requests.js +241 -0
  234. package/dist/cli/src/world-session-checkpoints.js +196 -0
  235. package/dist/cli/src/world-session-lifecycle.js +207 -0
  236. package/dist/cli/src/world-session-requests.js +215 -0
  237. package/dist/cli/src/world-session-state.js +145 -0
  238. package/dist/cli/src/world-session-voice.js +66 -0
  239. package/dist/cli/src/world-sessions.js +288 -0
  240. package/dist/cli/src/world-stream.js +99 -0
  241. package/dist/cli/src/world-time-report.js +58 -0
  242. package/dist/contracts/agent-change-contracts.js +75 -0
  243. package/dist/contracts/agent-execution-contracts.js +38 -0
  244. package/dist/contracts/application-state-contracts.js +26 -0
  245. package/dist/contracts/assessment-aggregate-contracts.js +15 -0
  246. package/dist/contracts/assessment-contracts.js +74 -0
  247. package/dist/contracts/billing-commerce-contracts.js +107 -0
  248. package/dist/contracts/billing-wallet-contracts.js +43 -0
  249. package/dist/contracts/campaign-lifecycle.js +1 -0
  250. package/dist/contracts/campaign-world-contracts.js +80 -0
  251. package/dist/contracts/catalog-contracts.js +73 -0
  252. package/dist/contracts/ci-access-contracts.js +68 -0
  253. package/dist/contracts/ci-configuration-contracts.js +88 -0
  254. package/dist/contracts/ci-coverage-contracts.js +65 -0
  255. package/dist/contracts/ci-coverage-labels.js +25 -0
  256. package/dist/contracts/ci-delivery-contracts.js +61 -0
  257. package/dist/contracts/ci-result-contracts.js +103 -0
  258. package/dist/contracts/ci-run-contracts.js +51 -0
  259. package/dist/contracts/ci-setup-contracts.js +88 -0
  260. package/dist/contracts/clip-contracts.js +32 -0
  261. package/dist/contracts/comparison-contracts.js +53 -0
  262. package/dist/contracts/composite-contracts.js +34 -0
  263. package/dist/contracts/context-contracts.js +76 -0
  264. package/dist/contracts/continuation-report-contracts.js +1 -0
  265. package/dist/contracts/continuity-requirements.js +8 -0
  266. package/dist/contracts/coordinated-checkpoint-contracts.js +34 -0
  267. package/dist/contracts/dataset-contracts.js +88 -0
  268. package/dist/contracts/dependency-decision-contracts.js +36 -0
  269. package/dist/contracts/dependency-protocol-contracts.js +43 -0
  270. package/dist/contracts/director-execution.js +51 -0
  271. package/dist/contracts/driver-review-contracts.js +99 -0
  272. package/dist/contracts/evidence-contracts.js +65 -0
  273. package/dist/contracts/evidence-coverage-contracts.js +10 -0
  274. package/dist/contracts/export-contracts.js +44 -0
  275. package/dist/contracts/github-directory-contracts.js +96 -0
  276. package/dist/contracts/insight-contracts.js +11 -0
  277. package/dist/contracts/inspection-contracts.js +1 -0
  278. package/dist/contracts/local-claim-contracts.js +39 -0
  279. package/dist/contracts/local-configuration-contracts.js +7 -0
  280. package/dist/contracts/local-evidence-contracts.js +32 -0
  281. package/dist/contracts/local-grant-contracts.js +83 -0
  282. package/dist/contracts/local-input-contracts.js +19 -0
  283. package/dist/contracts/local-runner-contracts.js +42 -0
  284. package/dist/contracts/local-trial-live-contracts.js +16 -0
  285. package/dist/contracts/local-value-source-contracts.js +92 -0
  286. package/dist/contracts/local-world-native-contracts.js +8 -0
  287. package/dist/contracts/mcp-contracts.js +42 -0
  288. package/dist/contracts/media-contracts.js +28 -0
  289. package/dist/contracts/native-check-contracts.js +31 -0
  290. package/dist/contracts/native-identity-contracts.js +52 -0
  291. package/dist/contracts/onboarding-contracts.js +32 -0
  292. package/dist/contracts/performance-contracts.js +85 -0
  293. package/dist/contracts/planning-contracts.js +81 -0
  294. package/dist/contracts/real-operation-contracts.js +30 -0
  295. package/dist/contracts/recommendation-contracts.js +54 -0
  296. package/dist/contracts/report-contracts.js +1 -0
  297. package/dist/contracts/resource-lifecycle-contracts.js +57 -0
  298. package/dist/contracts/revision-contracts.js +12 -0
  299. package/dist/contracts/revision-request-contracts.js +39 -0
  300. package/dist/contracts/saved-record-contracts.js +76 -0
  301. package/dist/contracts/scenario-draft-comparison.js +91 -0
  302. package/dist/contracts/scenario-world-contracts.js +14 -0
  303. package/dist/contracts/service-contracts.js +1 -0
  304. package/dist/contracts/session-command-contracts.js +56 -0
  305. package/dist/contracts/session-contracts.js +65 -0
  306. package/dist/contracts/session-state-contracts.js +67 -0
  307. package/dist/contracts/session-voice-contracts.js +45 -0
  308. package/dist/contracts/source-set-contracts.js +26 -0
  309. package/dist/contracts/source-upload-contracts.js +22 -0
  310. package/dist/contracts/tool-command-contracts.js +57 -0
  311. package/dist/contracts/tool-connection-contracts.js +87 -0
  312. package/dist/contracts/tool-contribution-contracts.js +35 -0
  313. package/dist/contracts/tool-group-contracts.js +53 -0
  314. package/dist/contracts/tool-group-state-contracts.js +63 -0
  315. package/dist/contracts/tool-lifecycle-contracts.js +94 -0
  316. package/dist/contracts/tool-package-contracts.js +91 -0
  317. package/dist/contracts/tool-protocol-contracts.js +71 -0
  318. package/dist/contracts/tool-runtime-billing-contracts.js +28 -0
  319. package/dist/contracts/tool-setup-proposal.js +59 -0
  320. package/dist/contracts/tool-state-contracts.js +79 -0
  321. package/dist/contracts/trial-state-contracts.js +30 -0
  322. package/dist/contracts/typescript/agent-change.js +1 -0
  323. package/dist/contracts/typescript/assessment-aggregate.js +1 -0
  324. package/dist/contracts/typescript/assessment.js +1 -0
  325. package/dist/contracts/typescript/billing-commerce.js +1 -0
  326. package/dist/contracts/typescript/campaigns.js +1 -0
  327. package/dist/contracts/typescript/catalog.js +1 -0
  328. package/dist/contracts/typescript/ci-access.js +1 -0
  329. package/dist/contracts/typescript/ci-configuration.js +1 -0
  330. package/dist/contracts/typescript/ci-delivery.js +1 -0
  331. package/dist/contracts/typescript/ci-result.js +1 -0
  332. package/dist/contracts/typescript/ci-runs.js +1 -0
  333. package/dist/contracts/typescript/clips.js +1 -0
  334. package/dist/contracts/typescript/comparison.js +1 -0
  335. package/dist/contracts/typescript/composite.js +1 -0
  336. package/dist/contracts/typescript/connections.js +1 -0
  337. package/dist/contracts/typescript/context.js +1 -0
  338. package/dist/contracts/typescript/datasets.js +1 -0
  339. package/dist/contracts/typescript/evidence-coverage.js +1 -0
  340. package/dist/contracts/typescript/export.js +1 -0
  341. package/dist/contracts/typescript/github-directory.js +1 -0
  342. package/dist/contracts/typescript/insight.js +1 -0
  343. package/dist/contracts/typescript/inspection.js +1 -0
  344. package/dist/contracts/typescript/intake.js +1 -0
  345. package/dist/contracts/typescript/local-claim.js +1 -0
  346. package/dist/contracts/typescript/local-configuration.js +1 -0
  347. package/dist/contracts/typescript/local-evidence.js +1 -0
  348. package/dist/contracts/typescript/local-grant.js +1 -0
  349. package/dist/contracts/typescript/local-input.js +1 -0
  350. package/dist/contracts/typescript/local-runner.js +1 -0
  351. package/dist/contracts/typescript/local-trial-live.js +1 -0
  352. package/dist/contracts/typescript/local-value-source.js +1 -0
  353. package/dist/contracts/typescript/local-world-native.js +1 -0
  354. package/dist/contracts/typescript/mcp.js +1 -0
  355. package/dist/contracts/typescript/media.js +1 -0
  356. package/dist/contracts/typescript/native-check.js +1 -0
  357. package/dist/contracts/typescript/native-identity.js +1 -0
  358. package/dist/contracts/typescript/onboarding.js +1 -0
  359. package/dist/contracts/typescript/performance.js +1 -0
  360. package/dist/contracts/typescript/planning.js +1 -0
  361. package/dist/contracts/typescript/preparation.js +1 -0
  362. package/dist/contracts/typescript/real-operation.js +1 -0
  363. package/dist/contracts/typescript/recommendation.js +1 -0
  364. package/dist/contracts/typescript/reports.js +1 -0
  365. package/dist/contracts/typescript/resource-lifecycle.js +1 -0
  366. package/dist/contracts/typescript/saved-record.js +1 -0
  367. package/dist/contracts/typescript/scenarios.js +1 -0
  368. package/dist/contracts/typescript/services.js +1 -0
  369. package/dist/contracts/typescript/session-command.js +1 -0
  370. package/dist/contracts/typescript/session-state.js +1 -0
  371. package/dist/contracts/typescript/session-voice.js +1 -0
  372. package/dist/contracts/typescript/session.js +1 -0
  373. package/dist/contracts/typescript/tool-command.js +1 -0
  374. package/dist/contracts/typescript/tool-connection.js +1 -0
  375. package/dist/contracts/typescript/tool-contribution.js +1 -0
  376. package/dist/contracts/typescript/tool-groups.js +1 -0
  377. package/dist/contracts/typescript/tool-lifecycle.js +1 -0
  378. package/dist/contracts/typescript/tool-package.js +1 -0
  379. package/dist/contracts/typescript/tool-state.js +1 -0
  380. package/dist/contracts/typescript/trial-state.js +1 -0
  381. package/dist/contracts/typescript/vault.js +1 -0
  382. package/dist/contracts/typescript/workspace-team.js +1 -0
  383. package/dist/contracts/typescript/world-stream.js +1 -0
  384. package/dist/contracts/workspace-team-contracts.js +55 -0
  385. package/dist/contracts/world-command.js +65 -0
  386. package/dist/contracts/world-lifetime-contracts.js +74 -0
  387. package/dist/contracts/world-recipe-contracts.js +27 -0
  388. package/dist/contracts/world-stream-contracts.js +27 -0
  389. package/dist/package.json +1 -0
  390. package/package.json +16 -16
  391. package/LICENSE.md +0 -23
  392. package/PACKAGING.json +0 -50
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Process groups this CLI started and still owns (POSIX only).
3
+ *
4
+ * A detached child leads its own process group, so it is not interrupted with
5
+ * the terminal's foreground group. Ordinary shutdown ends each group through its
6
+ * owner (the supervisor, a reset hook, a driver). This registry exists for the
7
+ * one case that cannot wait for that: a second interrupt, which force-exits the
8
+ * CLI. `killOwnedGroups` then ends every group still registered, so nothing the
9
+ * drill started outlives it.
10
+ *
11
+ * A group is registered once its leader has started and is released as soon as
12
+ * its leader exits and its members are swept. A registered id is therefore
13
+ * always a live leader this process spawned, never a reused id, so nothing
14
+ * unrelated is ever signalled.
15
+ */
16
+ const OWNED = new Set();
17
+ const POSIX = process.platform !== 'win32';
18
+ /** Record a group whose detached leader `pid` this process just started. */
19
+ export function ownGroup(pid) {
20
+ if (POSIX && typeof pid === 'number' && Number.isSafeInteger(pid) && pid > 1)
21
+ OWNED.add(pid);
22
+ }
23
+ /**
24
+ * The leader `pid` exited: SIGKILL the members it left in its group, then stop
25
+ * tracking it. Called right at the leader's exit, while no other process can
26
+ * hold that group id (members still in it keep the id reserved).
27
+ */
28
+ export function sweepGroup(pid) {
29
+ if (!POSIX || typeof pid !== 'number' || !OWNED.has(pid))
30
+ return;
31
+ try {
32
+ process.kill(-pid, 'SIGKILL');
33
+ }
34
+ catch { /* group already empty */ }
35
+ OWNED.delete(pid);
36
+ }
37
+ /** SIGKILL every group still owned. Synchronous, so it is safe in a signal handler before exit. */
38
+ export function killOwnedGroups() {
39
+ for (const pid of OWNED) {
40
+ try {
41
+ process.kill(-pid, 'SIGKILL');
42
+ }
43
+ catch { /* group already empty */ }
44
+ }
45
+ OWNED.clear();
46
+ }
@@ -0,0 +1,594 @@
1
+ /**
2
+ * Local approved plan: parsing, validation, digest and approval (Plan20 W09).
3
+ *
4
+ * The plan is a session input written by the operator (a coding agent or the
5
+ * user) and supplied by path or stdin. It names the exact argv, roots, hooks and
6
+ * environment variable names that may run on this machine. Only its digest over
7
+ * the portable form (root paths replaced by alias + identity) is ever sent to
8
+ * the cloud; commands are never accepted from a cloud message.
9
+ *
10
+ * Approved processes run natively with the user's permissions. Native mode is
11
+ * not an OS sandbox and nothing here claims otherwise.
12
+ */
13
+ import { createHash } from 'node:crypto';
14
+ import { createReadStream } from 'node:fs';
15
+ import { z } from 'zod';
16
+ import { CliError } from '../http.js';
17
+ import { selectOne, terminalText } from '../prompts.js';
18
+ import { canonicalJson, normalizeDescriptor, sha256Hex } from './canonical.js';
19
+ import { ALIAS, DRIVER_PROFILES, FRAME_INTERVAL_MAX_MS, FRAME_INTERVAL_MIN_MS, MAX_FRAMES_PER_STEP, SHA256_HEX, nativeError } from './contracts.js';
20
+ import { insideRoot, isSafeRelativePath } from './roots.js';
21
+ import { MASK_SELECTOR_MAX_CHARS, MAX_MASK_SELECTORS, describeScreenshots, describeTrace, maskingPolicy } from './screenshots.js';
22
+ // ---------------------------------------------------------------------------
23
+ // Shared limits and patterns.
24
+ /** Environment variable name accepted in plans and bindings. */
25
+ export const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
26
+ /** Inherited names that would switch off TLS verification (the user's own proxy and CA settings may still be inherited). */
27
+ const TLS_OFF_ENV = new Set(['NODE_TLS_REJECT_UNAUTHORIZED', 'GIT_SSL_NO_VERIFY', 'PYTHONHTTPSVERIFY']);
28
+ /**
29
+ * Names a fixed plan value may never set: TLS switches, added certificate
30
+ * authorities, proxies and loader preloads. Firedrill never changes the target's
31
+ * network trust; simulated dependencies are reached through declared routes. The
32
+ * comparison is case-insensitive because Windows env names are.
33
+ */
34
+ const TRUST_ENV = new Set([...TLS_OFF_ENV, 'NODE_OPTIONS', 'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE', 'SSL_CERT_DIR', 'REQUESTS_CA_BUNDLE',
35
+ 'CURL_CA_BUNDLE', 'GIT_SSL_CAINFO', 'GIT_SSL_CAPATH', 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'FTP_PROXY',
36
+ 'GLOBAL_AGENT_HTTP_PROXY', 'GLOBAL_AGENT_HTTPS_PROXY']);
37
+ const TRUST_ENV_PREFIXES = ['LD_', 'DYLD_', 'FIREDRILL_'];
38
+ /** Known platform limits of process control, shown before approval and in status. */
39
+ export function processControlLimitations(platform = process.platform) {
40
+ return platform === 'win32'
41
+ ? ['Process groups are not used on Windows: only each started process is stopped, and processes it started may keep running.']
42
+ : [];
43
+ }
44
+ function isTrustOverride(name) {
45
+ const upper = name.toUpperCase();
46
+ return TRUST_ENV.has(upper) || TRUST_ENV_PREFIXES.some((prefix) => upper.startsWith(prefix));
47
+ }
48
+ /** Names that look like credentials; refused as fixed plan values. */
49
+ export const SECRET_LIKE_NAME = /(?:^|_)(?:SECRET|SECRETS|TOKEN|TOKENS|PASSWORD|PASSWD|PASSPHRASE|PWD|APIKEY|API_KEY|KEY|PRIVATE_KEY|ACCESS_KEY|CREDENTIAL|CREDENTIALS|AUTH|AUTHORIZATION|COOKIE|SESSION)(?:_|$)/i;
50
+ const HEADER_NAME = /^[A-Za-z][A-Za-z0-9-]{0,63}$/;
51
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);
52
+ const MAX_PLAN_BYTES = 1_048_576;
53
+ const MAX_FILE_BYTES = 16 * 1024 * 1024;
54
+ const planInvalid = (where) => new CliError('native_plan_invalid', `The local plan is not valid${where ? ` at ${where}` : ''}. Nothing was approved or started.`, 2);
55
+ const planMismatch = (message) => new CliError('native_plan_invalid', `${message} Nothing was approved or started.`, 2);
56
+ // ---------------------------------------------------------------------------
57
+ // Parsing (closed fields, bounded sizes).
58
+ const noNul = (max) => z.string().max(max).regex(/^[^\u0000]*$/);
59
+ const aliasSchema = z.string().max(64).regex(ALIAS);
60
+ const shaSchema = z.string().regex(SHA256_HEX);
61
+ const envNameSchema = z.string().regex(ENV_NAME);
62
+ const relativePath = noNul(4096).refine(value => isSafeRelativePath(value));
63
+ const argvSchema = z.object({
64
+ executable: noNul(4096).min(1),
65
+ args: z.array(noNul(32_768)).max(256),
66
+ }).strict();
67
+ const timeoutSchema = z.number().int().min(1).max(600_000);
68
+ const readinessSchema = z.discriminatedUnion('kind', [
69
+ z.object({ kind: z.literal('http'), url: z.string().min(1).max(2048), expect_status: z.array(z.number().int().min(100).max(599)).min(1).max(32) }).strict(),
70
+ z.object({ kind: z.literal('tcp'), port: z.number().int().min(0).max(65_535) }).strict(),
71
+ z.object({ kind: z.literal('stdout'), pattern: noNul(512).min(1) }).strict(),
72
+ z.object({ kind: z.literal('none') }).strict(),
73
+ ]);
74
+ const serviceSchema = z.object({
75
+ alias: aliasSchema,
76
+ root_alias: aliasSchema,
77
+ mode: z.enum(['launch', 'attach']),
78
+ command: argvSchema.nullable(),
79
+ cwd: relativePath,
80
+ env_passthrough: z.array(envNameSchema).max(256),
81
+ env_static: z.record(envNameSchema, noNul(32_768)).refine(value => Object.keys(value).length <= 256),
82
+ readiness: readinessSchema,
83
+ readiness_timeout_ms: timeoutSchema,
84
+ stop_timeout_ms: z.number().int().min(0).max(120_000),
85
+ per_drill: z.boolean(),
86
+ port_env: envNameSchema.nullable(),
87
+ local_secret_env: z.array(envNameSchema).max(32).optional(),
88
+ mcp_config_env: envNameSchema.nullable().optional(),
89
+ /** Env names that receive this service's loopback dependency URLs (the companion proxy; never a credential). */
90
+ dependency_env: z.array(z.object({ alias: aliasSchema, name: envNameSchema, url: z.enum(['invoke', 'mcp']) }).strict()).max(64).optional(),
91
+ }).strict();
92
+ const driverSchema = z.discriminatedUnion('profile', [
93
+ z.object({
94
+ profile: z.literal('http.v1'), service_alias: aliasSchema, path: noNul(2048), method: z.enum(['POST', 'PUT']),
95
+ stream: z.enum(['none', 'sse', 'ndjson']), session_header: z.string().regex(HEADER_NAME).nullable(),
96
+ request_template: z.enum(['message-text', 'task-json']), response_pointer: noNul(1024).nullable(),
97
+ }).strict(),
98
+ z.object({ profile: z.literal('jsonl-process.v1'), service_alias: aliasSchema }).strict(),
99
+ z.object({ profile: z.literal('function-wrapper.v1'), root_alias: aliasSchema, wrapper: relativePath, runtime: argvSchema }).strict(),
100
+ z.object({ profile: z.literal('playwright.v1'), root_alias: aliasSchema, flow: relativePath, service_alias: aliasSchema, start_path: noNul(2048),
101
+ // Masked PNG screenshots: on only when the plan says so; the selectors add to the form fields always masked.
102
+ // `frames` adds masked frames while each step runs (captured frames, never a video).
103
+ screenshots: z.object({
104
+ mask_selectors: z.array(noNul(MASK_SELECTOR_MAX_CHARS).min(1)).max(MAX_MASK_SELECTORS),
105
+ frames: z.object({
106
+ interval_ms: z.number().int().min(FRAME_INTERVAL_MIN_MS).max(FRAME_INTERVAL_MAX_MS),
107
+ max_per_step: z.number().int().min(1).max(MAX_FRAMES_PER_STEP),
108
+ }).strict().optional(),
109
+ }).strict().optional(),
110
+ // Root's sanitized action trace: operation kinds, outcomes and times only. Raw Playwright traces, HAR and
111
+ // video are not supported: they would carry values, network bodies or unmasked frames.
112
+ trace: z.literal('sanitized-actions').optional() }).strict(),
113
+ // Every event root dispatches names the signature header, so the receiver's signing secret is required.
114
+ z.object({ profile: z.literal('signed-http-event.v1'), service_alias: aliasSchema, path_prefix: noNul(2048),
115
+ signing_secret_env: envNameSchema }).strict(),
116
+ ]);
117
+ const hookSchema = z.object({
118
+ domain_alias: aliasSchema, root_alias: aliasSchema, path: relativePath, sha256: shaSchema,
119
+ runtime: argvSchema, timeout_ms: timeoutSchema,
120
+ }).strict();
121
+ const planSchema = z.object({
122
+ version: z.literal(1),
123
+ descriptor_sha256: shaSchema,
124
+ roots: z.array(z.object({ alias: aliasSchema, path: noNul(4096).min(1), identity_sha256: shaSchema }).strict()).min(1).max(128),
125
+ services: z.array(serviceSchema).min(1).max(128),
126
+ driver: driverSchema,
127
+ state_hooks: z.array(hookSchema).max(256),
128
+ max_parallel_drills: z.number().int().min(1).max(64),
129
+ }).strict();
130
+ /** Bound size, depth and node count, and refuse prototype-bearing keys, before schema work. */
131
+ function boundedDocument(value) {
132
+ const pending = [[value, 0]];
133
+ let nodes = 0;
134
+ while (pending.length) {
135
+ const [item, depth] = pending.pop();
136
+ if (++nodes > 65_536 || depth > 16)
137
+ throw planInvalid();
138
+ if (Array.isArray(item)) {
139
+ if (item.length > 1024)
140
+ throw planInvalid();
141
+ for (const child of item)
142
+ pending.push([child, depth + 1]);
143
+ }
144
+ else if (item && typeof item === 'object') {
145
+ const keys = Object.keys(item);
146
+ if (keys.length > 512 || keys.some(key => key === '__proto__' || key === 'constructor' || key === 'prototype' || key.length > 128))
147
+ throw planInvalid();
148
+ for (const key of keys)
149
+ pending.push([item[key], depth + 1]);
150
+ }
151
+ }
152
+ }
153
+ /** Describe an issue location using only schema field names and indices, never values. */
154
+ function issueLocation(path) {
155
+ if (!path.length)
156
+ return undefined;
157
+ return path.map(part => typeof part === 'number' ? String(part)
158
+ : typeof part === 'string' && /^[a-z_]{1,40}$/.test(part) ? part : '*').join('.');
159
+ }
160
+ /**
161
+ * Parse a local plan JSON document strictly: closed fields, bounded sizes, no
162
+ * NUL characters, safe relative paths. Throws `native_plan_invalid` (exit 2)
163
+ * naming the failing field location, never the rejected value.
164
+ */
165
+ export function parsePlan(raw) {
166
+ if (typeof raw !== 'string' || Buffer.byteLength(raw, 'utf8') > MAX_PLAN_BYTES)
167
+ throw planInvalid();
168
+ let value;
169
+ try {
170
+ value = JSON.parse(raw);
171
+ }
172
+ catch {
173
+ throw planInvalid();
174
+ }
175
+ boundedDocument(value);
176
+ const parsed = planSchema.safeParse(value);
177
+ if (!parsed.success)
178
+ throw planInvalid(issueLocation(parsed.error.issues[0]?.path ?? []));
179
+ return parsed.data;
180
+ }
181
+ /** Replace session-local absolute root paths with `{alias, identity_sha256}`. */
182
+ export function portablePlan(plan) {
183
+ return {
184
+ version: plan.version,
185
+ descriptor_sha256: plan.descriptor_sha256,
186
+ roots: plan.roots.map(root => ({ alias: root.alias, identity_sha256: root.identity_sha256 })),
187
+ services: plan.services,
188
+ driver: plan.driver,
189
+ state_hooks: plan.state_hooks,
190
+ max_parallel_drills: plan.max_parallel_drills,
191
+ };
192
+ }
193
+ /** sha256 of the canonical JSON of the portable plan. The only plan value the cloud learns. */
194
+ export function planDigest(plan) {
195
+ return sha256Hex(canonicalJson(portablePlan(plan)));
196
+ }
197
+ /** Parse and check a loopback readiness URL; returns null when it is not acceptable. */
198
+ export function loopbackUrl(raw) {
199
+ let url;
200
+ try {
201
+ url = new URL(raw);
202
+ }
203
+ catch {
204
+ return null;
205
+ }
206
+ if (url.protocol !== 'http:' || !LOOPBACK_HOSTS.has(url.hostname) || url.username || url.password || url.hash)
207
+ return null;
208
+ return url;
209
+ }
210
+ /** True when a service's port is known before or at start (allocated, or fixed in readiness). */
211
+ export function serviceHasPort(service) {
212
+ return service.port_env !== null || service.readiness.kind === 'http' || service.readiness.kind === 'tcp';
213
+ }
214
+ function checkReadiness(service, readiness) {
215
+ const allocated = service.port_env !== null;
216
+ if (readiness.kind === 'http') {
217
+ const url = loopbackUrl(readiness.url);
218
+ if (!url)
219
+ throw planMismatch('A readiness URL must be plain http on 127.0.0.1, localhost or [::1].');
220
+ // Port 0 stands for the allocated port and is valid only with port_env.
221
+ if (allocated ? url.port !== '0' : !url.port || url.port === '0') {
222
+ throw planMismatch('A readiness URL must use port 0 when the port is allocated (port_env), and an explicit port otherwise.');
223
+ }
224
+ }
225
+ else if (readiness.kind === 'tcp') {
226
+ if (allocated ? readiness.port !== 0 : readiness.port < 1) {
227
+ throw planMismatch('A readiness TCP port must be 0 when the port is allocated (port_env), and a real port otherwise.');
228
+ }
229
+ }
230
+ }
231
+ function checkPath(value, label) {
232
+ if (!value.startsWith('/') || value.startsWith('//') || value.includes('\\') || value.includes('#') || /[\u0000-\u001f]/.test(value)) {
233
+ throw planMismatch(`The driver ${label} must be an absolute request path starting with a single "/".`);
234
+ }
235
+ }
236
+ function checkArgv(command) {
237
+ // argv only (spawned with shell:false). Batch files would be run through a
238
+ // command interpreter on Windows, so they are refused everywhere.
239
+ if (/\.(?:bat|cmd)$/i.test(command.executable.trim())) {
240
+ throw planMismatch('Use a real executable, not a batch file, for approved commands.');
241
+ }
242
+ }
243
+ /**
244
+ * Validate a parsed plan against the (re-normalized) descriptor: every
245
+ * descriptor service has exactly one plan entry; launch services have argv,
246
+ * attach services none and are never per-drill; roots, hooks and driver profile
247
+ * match the descriptor; env names are identifiers; no secret-named configuration
248
+ * appears in `env_static`, and descriptor secrets are never passed through from
249
+ * the user environment. Returns the plan digest. Does not touch the filesystem;
250
+ * see `verifyPlanFiles` for on-disk checks at use time.
251
+ */
252
+ export function validatePlan(plan, descriptor) {
253
+ const normalized = normalizeDescriptor(descriptor);
254
+ const document = normalized.descriptor;
255
+ if (plan.version !== 1)
256
+ throw planInvalid('version');
257
+ if (plan.descriptor_sha256 !== normalized.descriptor_sha256)
258
+ throw planMismatch('The plan was written for a different local descriptor.');
259
+ const roots = new Map(plan.roots.map(root => [root.alias, root]));
260
+ if (roots.size !== plan.roots.length)
261
+ throw planMismatch('Use distinct root aliases in the plan.');
262
+ if (roots.size !== document.source_roots.length || document.source_roots.some(root => roots.get(root.alias)?.identity_sha256 !== root.identity_sha256)) {
263
+ throw planMismatch('The plan roots must match the descriptor source roots and their identities.');
264
+ }
265
+ for (const root of plan.roots) {
266
+ if (!/^(?:\/|[A-Za-z]:[\\/]|\\\\)/.test(root.path))
267
+ throw planMismatch('Plan root paths must be absolute.');
268
+ }
269
+ const services = new Map(plan.services.map(service => [service.alias, service]));
270
+ if (services.size !== plan.services.length || services.size !== document.services.length) {
271
+ throw planMismatch('The plan must contain exactly one entry for each descriptor service.');
272
+ }
273
+ for (const declared of document.services) {
274
+ const service = services.get(declared.alias);
275
+ if (!service)
276
+ throw planMismatch('The plan must contain exactly one entry for each descriptor service.');
277
+ if (service.root_alias !== declared.root_alias)
278
+ throw planMismatch('Each plan service must use its descriptor root.');
279
+ if (!isSafeRelativePath(service.cwd))
280
+ throw planInvalid('services.cwd');
281
+ if (service.mode === 'launch') {
282
+ if (!service.command)
283
+ throw planMismatch('Each launched service needs an approved command in argv form.');
284
+ checkArgv(service.command);
285
+ if (service.per_drill && service.port_env === null && serviceHasPort(service)) {
286
+ throw planMismatch('A per-drill network service needs port_env for its own loopback port.');
287
+ }
288
+ if (service.port_env !== null && isTrustOverride(service.port_env))
289
+ throw planMismatch(`${service.port_env} cannot carry the allocated port.`);
290
+ }
291
+ else {
292
+ if (service.command !== null)
293
+ throw planMismatch('An attached service is already running and cannot carry a command.');
294
+ if (service.per_drill || service.port_env !== null)
295
+ throw planMismatch('An attached service cannot be per-drill or receive an allocated port.');
296
+ }
297
+ checkReadiness(service, service.readiness);
298
+ const passthrough = new Set(service.env_passthrough);
299
+ if (passthrough.size !== service.env_passthrough.length)
300
+ throw planMismatch('List each passed-through environment name once.');
301
+ const secretNames = new Set(declared.configuration.filter(item => item.secret).map(item => item.name));
302
+ const localSecrets = service.local_secret_env ?? [];
303
+ const folded = (name) => name.toUpperCase();
304
+ const occupied = new Set([...service.env_passthrough, ...Object.keys(service.env_static),
305
+ ...[service.port_env, service.mcp_config_env].filter((name) => typeof name === 'string'),
306
+ ...(service.dependency_env ?? []).map(route => route.name)].map(folded));
307
+ if (new Set(localSecrets.map(folded)).size !== localSecrets.length || localSecrets.some(name => service.mode !== 'launch' || !service.per_drill || occupied.has(folded(name)) || isTrustOverride(name)
308
+ || !declared.configuration.some(item => item.name === name && item.secret && item.phase !== 'build'))) {
309
+ throw planMismatch('Local secret bindings must be distinct declared runtime secrets on a fresh per-drill process. They cannot override routing or other configuration.');
310
+ }
311
+ for (const name of Object.keys(service.env_static)) {
312
+ if (!ENV_NAME.test(name))
313
+ throw planInvalid('services.env_static');
314
+ if (secretNames.has(name) || SECRET_LIKE_NAME.test(name)) {
315
+ throw planMismatch('Secret configuration cannot be a fixed plan value. Supply it through a binding instead.');
316
+ }
317
+ if (isTrustOverride(name)) {
318
+ throw planMismatch(`${name} would change how the target trusts or routes network traffic, or what it preloads. A plan never disables TLS verification, adds a certificate authority, sets a proxy or injects a loader.`);
319
+ }
320
+ }
321
+ for (const name of passthrough) {
322
+ if (!ENV_NAME.test(name))
323
+ throw planInvalid('services.env_passthrough');
324
+ if (secretNames.has(name))
325
+ throw planMismatch('Descriptor secret configuration is never inherited from your environment. Supply it through a binding instead.');
326
+ if (TLS_OFF_ENV.has(name.toUpperCase()) || name.toUpperCase().startsWith('FIREDRILL_')) {
327
+ throw planMismatch(`${name} is never inherited: it would switch off TLS verification or impersonate a Firedrill value.`);
328
+ }
329
+ }
330
+ }
331
+ const driver = plan.driver;
332
+ if (!DRIVER_PROFILES.includes(driver.profile) || driver.profile !== document.driver.profile) {
333
+ throw planMismatch('The plan driver profile must equal the descriptor driver profile.');
334
+ }
335
+ const serviceFor = (alias) => {
336
+ const service = services.get(alias);
337
+ if (!service)
338
+ throw planMismatch('The driver must use a service declared in the plan.');
339
+ return service;
340
+ };
341
+ const rootFor = (alias) => {
342
+ if (!roots.has(alias))
343
+ throw planMismatch('The driver must use a root declared in the plan.');
344
+ };
345
+ switch (driver.profile) {
346
+ case 'http.v1': {
347
+ if (!serviceHasPort(serviceFor(driver.service_alias)))
348
+ throw planMismatch('The driver service needs a known loopback port.');
349
+ checkPath(driver.path, 'path');
350
+ if (driver.response_pointer !== null && driver.response_pointer !== '' && !driver.response_pointer.startsWith('/')) {
351
+ throw planMismatch('The response pointer must be a JSON pointer.');
352
+ }
353
+ break;
354
+ }
355
+ case 'jsonl-process.v1': {
356
+ const service = serviceFor(driver.service_alias);
357
+ // The JSON-lines driver itself starts and owns this process (its stdin/stdout).
358
+ if (service.mode !== 'launch' || (service.readiness.kind !== 'none' && service.readiness.kind !== 'stdout')) {
359
+ throw planMismatch('A JSON-lines service must be launched by the plan and use "none" or "stdout" readiness.');
360
+ }
361
+ break;
362
+ }
363
+ case 'function-wrapper.v1':
364
+ rootFor(driver.root_alias);
365
+ checkArgv(driver.runtime);
366
+ break;
367
+ case 'playwright.v1':
368
+ rootFor(driver.root_alias);
369
+ if (!serviceHasPort(serviceFor(driver.service_alias)))
370
+ throw planMismatch('The driver service needs a known loopback port.');
371
+ checkPath(driver.start_path, 'start path');
372
+ break;
373
+ case 'signed-http-event.v1':
374
+ if (!serviceHasPort(serviceFor(driver.service_alias)))
375
+ throw planMismatch('The driver service needs a known loopback port.');
376
+ checkPath(driver.path_prefix, 'path prefix');
377
+ // The receiver's signing secret is read from this environment by name; it is never a trust or Firedrill setting.
378
+ if (driver.signing_secret_env !== undefined && isTrustOverride(driver.signing_secret_env)) {
379
+ throw planMismatch(`${driver.signing_secret_env} cannot hold the event signing secret.`);
380
+ }
381
+ break;
382
+ }
383
+ const hooked = new Map(document.state_domains.filter(domain => domain.reset === 'approved-hook').map(domain => [domain.alias, domain]));
384
+ const hookDomains = new Set();
385
+ for (const hook of plan.state_hooks) {
386
+ const domain = hooked.get(hook.domain_alias);
387
+ if (!domain || hookDomains.has(hook.domain_alias))
388
+ throw planMismatch('Each state hook must belong to one approved-hook state domain.');
389
+ if (domain.hook_sha256 !== hook.sha256)
390
+ throw planMismatch('A state hook digest does not match the descriptor.');
391
+ if (!roots.has(hook.root_alias) || !isSafeRelativePath(hook.path))
392
+ throw planMismatch('A state hook must be a file inside a plan root.');
393
+ checkArgv(hook.runtime);
394
+ hookDomains.add(hook.domain_alias);
395
+ }
396
+ if (hookDomains.size !== hooked.size)
397
+ throw planMismatch('Every approved-hook state domain needs exactly one plan hook.');
398
+ return { plan_sha256: planDigest(plan), normalized };
399
+ }
400
+ // ---------------------------------------------------------------------------
401
+ // On-disk checks at use time.
402
+ /** sha256 of a file's bytes (bounded size). */
403
+ export async function hashFile(path, maxBytes = MAX_FILE_BYTES) {
404
+ const hash = createHash('sha256');
405
+ let size = 0;
406
+ for await (const chunk of createReadStream(path)) {
407
+ size += chunk.length;
408
+ if (size > maxBytes)
409
+ throw new CliError('native_file_too_large', 'An approved file exceeds the accepted size. Nothing was run from it.', 2);
410
+ hash.update(chunk);
411
+ }
412
+ return hash.digest('hex');
413
+ }
414
+ const fileChanged = () => nativeError('plan_changed', 'An approved file changed since the plan was approved. Review and approve the plan again; nothing was run from it.', 2);
415
+ /**
416
+ * Re-hash an approved file inside its root and return its real path. A digest
417
+ * mismatch is `native_plan_changed`. Call immediately before each use; the hash
418
+ * is evidence of what was approved, not a lock against later edits.
419
+ */
420
+ export async function verifyRootFile(roots, rootAlias, relative, expectedSha256) {
421
+ const root = roots.get(rootAlias);
422
+ if (!root)
423
+ throw nativeError('root_outside', 'An approved file names a root that is not part of this session. Nothing was run from it.', 2);
424
+ const path = await insideRoot(root, relative);
425
+ let digest;
426
+ try {
427
+ digest = await hashFile(path);
428
+ }
429
+ catch (error) {
430
+ if (error instanceof CliError)
431
+ throw error;
432
+ throw fileChanged();
433
+ }
434
+ if (digest !== expectedSha256)
435
+ throw fileChanged();
436
+ return path;
437
+ }
438
+ /** Re-hash a state hook file (descriptor `hook_sha256`) and return its real path. */
439
+ export function verifyHookFile(roots, hook) {
440
+ return verifyRootFile(roots, hook.root_alias, hook.path, hook.sha256);
441
+ }
442
+ /**
443
+ * Verify every on-disk input the plan pins: state hook files, and for the
444
+ * function-wrapper and playwright profiles the wrapper or flow module against
445
+ * the descriptor `adapter_sha256`. Throws `native_plan_changed` on any mismatch.
446
+ */
447
+ export async function verifyPlanFiles(plan, normalized, roots) {
448
+ for (const hook of plan.state_hooks)
449
+ await verifyHookFile(roots, hook);
450
+ const driver = plan.driver;
451
+ if (driver.profile === 'function-wrapper.v1') {
452
+ await verifyRootFile(roots, driver.root_alias, driver.wrapper, normalized.descriptor.driver.adapter_sha256);
453
+ }
454
+ else if (driver.profile === 'playwright.v1') {
455
+ await verifyRootFile(roots, driver.root_alias, driver.flow, normalized.descriptor.driver.adapter_sha256);
456
+ }
457
+ }
458
+ const minted = new WeakSet();
459
+ const quote = (value) => terminalText(JSON.stringify(value));
460
+ const argvText = (command) => [command.executable, ...command.args].map(quote).join(' ');
461
+ function readinessText(readiness, timeout) {
462
+ const within = `within ${timeout} ms`;
463
+ switch (readiness.kind) {
464
+ case 'http': return `HTTP GET ${terminalText(readiness.url)} expecting ${readiness.expect_status.join('/')} ${within}`;
465
+ case 'tcp': return `TCP connect to 127.0.0.1:${readiness.port === 0 ? '<allocated>' : readiness.port} ${within}`;
466
+ case 'stdout': return `output contains ${quote(readiness.pattern)} ${within}`;
467
+ case 'none': return 'none';
468
+ }
469
+ }
470
+ /**
471
+ * Human-readable summary of everything approval allows: roots, exact argv,
472
+ * working directories, environment variable names (never values), readiness,
473
+ * driver and hooks. Port 0 in readiness means the allocated port.
474
+ */
475
+ export function describePlan(plan, validated) {
476
+ const lines = [];
477
+ lines.push(`Local plan ${validated.plan_sha256}`);
478
+ lines.push(`Descriptor ${validated.normalized.descriptor_sha256}`);
479
+ lines.push('These commands run as trusted processes with your user permissions. Native mode is not an OS sandbox.');
480
+ lines.push(...processControlLimitations());
481
+ lines.push('Roots:');
482
+ for (const root of plan.roots)
483
+ lines.push(` ${root.alias}: ${terminalText(root.path)}`);
484
+ lines.push('Services, in startup order:');
485
+ const byAlias = new Map(plan.services.map(service => [service.alias, service]));
486
+ for (const alias of validated.normalized.startup_order) {
487
+ const service = byAlias.get(alias);
488
+ if (!service)
489
+ continue;
490
+ if (service.mode === 'attach') {
491
+ lines.push(` ${alias}: attach to an already running service (readiness check only; never signalled)`);
492
+ }
493
+ else {
494
+ lines.push(` ${alias}: launch${service.per_drill ? ' one copy per drill' : ' one shared copy'}`);
495
+ if (service.command)
496
+ lines.push(` argv: ${argvText(service.command)}`);
497
+ }
498
+ lines.push(` cwd: ${service.root_alias}:${terminalText(service.cwd || '.')}`);
499
+ if (service.env_passthrough.length)
500
+ lines.push(` inherits from your environment: ${service.env_passthrough.join(', ')}`);
501
+ const fixed = Object.keys(service.env_static);
502
+ if (fixed.length)
503
+ lines.push(` fixed environment names: ${fixed.join(', ')}`);
504
+ if (service.port_env)
505
+ lines.push(` allocated loopback port in: ${service.port_env}`);
506
+ if (service.local_secret_env?.length)
507
+ lines.push(` local secret bindings: ${service.local_secret_env.join(', ')} (values stay on this machine)`);
508
+ lines.push(` readiness: ${readinessText(service.readiness, service.readiness_timeout_ms)}`);
509
+ }
510
+ const driver = plan.driver;
511
+ switch (driver.profile) {
512
+ case 'http.v1':
513
+ lines.push(`Driver: ${driver.profile} ${driver.method} ${terminalText(driver.path)} on ${driver.service_alias}`);
514
+ break;
515
+ case 'jsonl-process.v1':
516
+ lines.push(`Driver: ${driver.profile} over the standard input and output of ${driver.service_alias}`);
517
+ break;
518
+ case 'function-wrapper.v1':
519
+ lines.push(`Driver: ${driver.profile} ${driver.root_alias}:${terminalText(driver.wrapper)} with argv ${argvText(driver.runtime)}`);
520
+ break;
521
+ case 'playwright.v1':
522
+ lines.push(`Driver: ${driver.profile} flow ${driver.root_alias}:${terminalText(driver.flow)} on ${driver.service_alias}`);
523
+ lines.push(` screenshots: ${terminalText(describeScreenshots(driver.screenshots))}`);
524
+ if (driver.screenshots)
525
+ lines.push(` masking policy sha256 ${maskingPolicy(driver.screenshots).sha256}`);
526
+ lines.push(` action trace: ${terminalText(describeTrace(driver.trace))}`);
527
+ break;
528
+ case 'signed-http-event.v1':
529
+ lines.push(`Driver: ${driver.profile} ${terminalText(driver.path_prefix)} on ${driver.service_alias}`);
530
+ lines.push(` signs each event with the secret in ${driver.signing_secret_env} from your environment (the value is never shown or uploaded)`);
531
+ break;
532
+ }
533
+ if (plan.state_hooks.length) {
534
+ lines.push('State reset hooks:');
535
+ for (const hook of plan.state_hooks) {
536
+ lines.push(` ${hook.domain_alias}: ${hook.root_alias}:${terminalText(hook.path)} sha256 ${hook.sha256}`);
537
+ lines.push(` argv: ${argvText(hook.runtime)} (timeout ${hook.timeout_ms} ms)`);
538
+ }
539
+ }
540
+ lines.push(`Parallel drills: at most ${validated.normalized.requires_serial_execution ? 1 : plan.max_parallel_drills}`);
541
+ return lines.join('\n');
542
+ }
543
+ /**
544
+ * Validate, show and approve a plan. With `approve: true` (for agents, the
545
+ * `--approve` flag) consent is explicit and no prompt is shown. Otherwise an
546
+ * interactive terminal choice is required; without one this throws
547
+ * `native_plan_unapproved` (exit 2). The approval lives in memory only; the
548
+ * cloud learns just `plan_sha256`. Any later change voids it (see
549
+ * `assertPlanApproved`).
550
+ */
551
+ export async function approvePlan(plan, descriptor, options) {
552
+ const validated = validatePlan(plan, descriptor);
553
+ const write = options.output ?? ((text) => { process.stderr.write(text); });
554
+ write(describePlan(plan, validated) + '\n');
555
+ let method;
556
+ if (options.approve)
557
+ method = 'flag';
558
+ else if (!options.interactive) {
559
+ throw nativeError('plan_unapproved', 'The local plan was not approved, so nothing was started. Review it and pass --approve, or approve it in an interactive terminal.', 2);
560
+ }
561
+ else {
562
+ const accepted = await selectOne('Approve running these commands on this machine?', [
563
+ { value: false, label: 'Do not approve', detail: 'Nothing is started.' },
564
+ { value: true, label: 'Approve this plan', detail: `Digest ${validated.plan_sha256.slice(0, 16)}` },
565
+ ], options.signal);
566
+ if (!accepted)
567
+ throw nativeError('plan_unapproved', 'The local plan was not approved, so nothing was started.', 2);
568
+ method = 'interactive';
569
+ }
570
+ const approval = Object.freeze({
571
+ plan_sha256: validated.plan_sha256,
572
+ descriptor_sha256: validated.normalized.descriptor_sha256,
573
+ startup_order: Object.freeze([...validated.normalized.startup_order]),
574
+ approved_at: Date.now(),
575
+ method,
576
+ });
577
+ minted.add(approval);
578
+ return approval;
579
+ }
580
+ /**
581
+ * Throw unless `approval` was issued by `approvePlan` in this process and the
582
+ * plan still has the approved digest (`native_plan_unapproved` /
583
+ * `native_plan_changed`). Call before any start and when a cloud message
584
+ * references a plan digest.
585
+ */
586
+ export function assertPlanApproved(approval, plan, expectedSha256) {
587
+ if (!approval || !minted.has(approval)) {
588
+ throw nativeError('plan_unapproved', 'The local plan has not been approved in this session, so nothing was started.', 2);
589
+ }
590
+ const digest = planDigest(plan);
591
+ if (digest !== approval.plan_sha256 || (expectedSha256 !== undefined && expectedSha256 !== digest)) {
592
+ throw nativeError('plan_changed', 'The local plan differs from the approved plan. Review and approve it again; nothing was started.', 2);
593
+ }
594
+ }