firedrill-run 2.2.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (392) hide show
  1. package/README.md +178 -22
  2. package/bin/firedrill.mjs +29 -5
  3. package/dist/assembly.json +13 -0
  4. package/dist/cli/runtime/candidate_copy.py +395 -0
  5. package/dist/cli/runtime/source_snapshot.py +391 -0
  6. package/dist/cli/src/administration.js +295 -0
  7. package/dist/cli/src/agent-changes.js +496 -0
  8. package/dist/cli/src/assessment-requests.js +67 -0
  9. package/dist/cli/src/auth.js +465 -0
  10. package/dist/cli/src/billing-commerce-contracts.js +1 -0
  11. package/dist/cli/src/billing-usage.js +227 -0
  12. package/dist/cli/src/billing-wallet-contracts.js +1 -0
  13. package/dist/cli/src/billing.js +170 -0
  14. package/dist/cli/src/bootstrap-process-lock.js +136 -0
  15. package/dist/cli/src/browser-links.js +14 -0
  16. package/dist/cli/src/campaign-controls.js +122 -0
  17. package/dist/cli/src/campaign-diagnoses.js +118 -0
  18. package/dist/cli/src/campaign-reports.js +1215 -0
  19. package/dist/cli/src/campaign-world.js +156 -0
  20. package/dist/cli/src/candidate-copy.js +376 -0
  21. package/dist/cli/src/catalog-contracts.js +1 -0
  22. package/dist/cli/src/ci-access-contracts.js +1 -0
  23. package/dist/cli/src/ci-configuration-contracts.js +1 -0
  24. package/dist/cli/src/ci-coverage.js +64 -0
  25. package/dist/cli/src/ci-delivery-contracts.js +1 -0
  26. package/dist/cli/src/ci-result-contracts.js +1 -0
  27. package/dist/cli/src/ci-run-contracts.js +1 -0
  28. package/dist/cli/src/ci-setup-proposals.js +52 -0
  29. package/dist/cli/src/company-policies.js +229 -0
  30. package/dist/cli/src/completion.js +79 -0
  31. package/dist/cli/src/configuration-intents.js +277 -0
  32. package/dist/cli/src/configuration.js +369 -0
  33. package/dist/cli/src/continuous-simulation.js +277 -0
  34. package/dist/cli/src/coordinated-checkpoints.js +133 -0
  35. package/dist/cli/src/credential-mode.js +139 -0
  36. package/dist/cli/src/credentials.js +331 -0
  37. package/dist/cli/src/credit-format.js +8 -0
  38. package/dist/cli/src/dataset-contracts.js +1 -0
  39. package/dist/cli/src/dataset-export.js +154 -0
  40. package/dist/cli/src/dataset-reads.js +37 -0
  41. package/dist/cli/src/dependency-preparation.js +239 -0
  42. package/dist/cli/src/director-tool-setup.js +104 -0
  43. package/dist/cli/src/driver-review.js +225 -0
  44. package/dist/cli/src/evaluation-authoring.js +315 -0
  45. package/dist/cli/src/evidence-bundles.js +367 -0
  46. package/dist/cli/src/execution-proposals.js +36 -0
  47. package/dist/cli/src/http.js +302 -0
  48. package/dist/cli/src/local-container-boundary.js +38 -0
  49. package/dist/cli/src/local-container-policy.js +201 -0
  50. package/dist/cli/src/local-execution-setup.js +81 -0
  51. package/dist/cli/src/local-source-basis.js +73 -0
  52. package/dist/cli/src/local-source.js +66 -0
  53. package/dist/cli/src/local-value-store.js +91 -0
  54. package/dist/cli/src/local-values.js +163 -0
  55. package/dist/cli/src/login-organizations.js +70 -0
  56. package/dist/cli/src/main.js +1622 -0
  57. package/dist/cli/src/mcp-connection.js +109 -0
  58. package/dist/cli/src/native/bindings.js +137 -0
  59. package/dist/cli/src/native/canonical.js +370 -0
  60. package/dist/cli/src/native/cloud-resources.js +942 -0
  61. package/dist/cli/src/native/companion-channel.js +421 -0
  62. package/dist/cli/src/native/companion-ipc.js +473 -0
  63. package/dist/cli/src/native/contracts.js +218 -0
  64. package/dist/cli/src/native/control-plane.js +1354 -0
  65. package/dist/cli/src/native/credit-approval.js +15 -0
  66. package/dist/cli/src/native/dependency-proxy.js +1006 -0
  67. package/dist/cli/src/native/drill-cleanup.js +115 -0
  68. package/dist/cli/src/native/drill-runner.js +1562 -0
  69. package/dist/cli/src/native/drivers/action-trace.js +210 -0
  70. package/dist/cli/src/native/drivers/function-wrapper.js +152 -0
  71. package/dist/cli/src/native/drivers/http.js +442 -0
  72. package/dist/cli/src/native/drivers/index.js +47 -0
  73. package/dist/cli/src/native/drivers/jsonl-process.js +538 -0
  74. package/dist/cli/src/native/drivers/playwright.js +600 -0
  75. package/dist/cli/src/native/drivers/signed-event.js +244 -0
  76. package/dist/cli/src/native/drivers/types.js +578 -0
  77. package/dist/cli/src/native/evidence-filter.js +255 -0
  78. package/dist/cli/src/native/isolated-access.js +241 -0
  79. package/dist/cli/src/native/library-sharing.js +220 -0
  80. package/dist/cli/src/native/local-run.js +667 -0
  81. package/dist/cli/src/native/local-secrets.js +19 -0
  82. package/dist/cli/src/native/mcp-config.js +123 -0
  83. package/dist/cli/src/native/metadata-requests.js +120 -0
  84. package/dist/cli/src/native/observations.js +1159 -0
  85. package/dist/cli/src/native/owned-groups.js +46 -0
  86. package/dist/cli/src/native/plan.js +594 -0
  87. package/dist/cli/src/native/png.js +159 -0
  88. package/dist/cli/src/native/ports.js +43 -0
  89. package/dist/cli/src/native/readiness.js +139 -0
  90. package/dist/cli/src/native/reassessments.js +206 -0
  91. package/dist/cli/src/native/redaction.js +235 -0
  92. package/dist/cli/src/native/registration.js +155 -0
  93. package/dist/cli/src/native/roots.js +110 -0
  94. package/dist/cli/src/native/run-cleanup.js +199 -0
  95. package/dist/cli/src/native/scratch.js +117 -0
  96. package/dist/cli/src/native/screenshots.js +60 -0
  97. package/dist/cli/src/native/session.js +556 -0
  98. package/dist/cli/src/native/standalone-proxy.js +580 -0
  99. package/dist/cli/src/native/standalone-renewals.js +183 -0
  100. package/dist/cli/src/native/standalone-state.js +193 -0
  101. package/dist/cli/src/native/standalone-tools.js +187 -0
  102. package/dist/cli/src/native/supervisor.js +831 -0
  103. package/dist/cli/src/native/target-auth.js +161 -0
  104. package/dist/cli/src/native/target-state.js +330 -0
  105. package/dist/cli/src/native/tool-reuse.js +364 -0
  106. package/dist/cli/src/native/tool-setups.js +336 -0
  107. package/dist/cli/src/native/world-reuse.js +288 -0
  108. package/dist/cli/src/onboarding.js +23 -0
  109. package/dist/cli/src/priority-requests.js +70 -0
  110. package/dist/cli/src/priority.js +223 -0
  111. package/dist/cli/src/prompts.js +184 -0
  112. package/dist/cli/src/real-operations.js +221 -0
  113. package/dist/cli/src/recipe-intents.js +58 -0
  114. package/dist/cli/src/recorded-context-intents.js +59 -0
  115. package/dist/cli/src/recorded-context.js +248 -0
  116. package/dist/cli/src/recordings.js +69 -0
  117. package/dist/cli/src/registration-guide.js +39 -0
  118. package/dist/cli/src/report-exports.js +213 -0
  119. package/dist/cli/src/request-input.js +47 -0
  120. package/dist/cli/src/resource-lifecycle.js +141 -0
  121. package/dist/cli/src/revision-requests.js +55 -0
  122. package/dist/cli/src/runner-abandon.js +53 -0
  123. package/dist/cli/src/runner-campaign-controls.js +262 -0
  124. package/dist/cli/src/runner-claim-reads.js +36 -0
  125. package/dist/cli/src/runner-completion.js +65 -0
  126. package/dist/cli/src/runner-configuration.js +546 -0
  127. package/dist/cli/src/runner-container.js +794 -0
  128. package/dist/cli/src/runner-evidence-erasure.js +351 -0
  129. package/dist/cli/src/runner-evidence-privacy.js +37 -0
  130. package/dist/cli/src/runner-evidence-retention.js +161 -0
  131. package/dist/cli/src/runner-evidence.js +326 -0
  132. package/dist/cli/src/runner-execute.js +92 -0
  133. package/dist/cli/src/runner-execution-deadline.js +25 -0
  134. package/dist/cli/src/runner-grants.js +307 -0
  135. package/dist/cli/src/runner-identity.js +615 -0
  136. package/dist/cli/src/runner-inputs.js +308 -0
  137. package/dist/cli/src/runner-jobs.js +49 -0
  138. package/dist/cli/src/runner-model-transport.js +392 -0
  139. package/dist/cli/src/runner-ownership.js +332 -0
  140. package/dist/cli/src/runner-process-lock.js +175 -0
  141. package/dist/cli/src/runner-runtime.js +396 -0
  142. package/dist/cli/src/runner-session-channel.js +377 -0
  143. package/dist/cli/src/runner-trial-completion.js +34 -0
  144. package/dist/cli/src/runner-trial-journal.js +358 -0
  145. package/dist/cli/src/runner-trial-live.js +393 -0
  146. package/dist/cli/src/runner-trial-protocol.js +289 -0
  147. package/dist/cli/src/runner-trial-sync.js +110 -0
  148. package/dist/cli/src/runner-trial-transfer.js +107 -0
  149. package/dist/cli/src/runner-voice.js +40 -0
  150. package/dist/cli/src/runner-world-claim.js +21 -0
  151. package/dist/cli/src/runner-world-collector.js +159 -0
  152. package/dist/cli/src/runner-world-command-evidence.js +487 -0
  153. package/dist/cli/src/runner-world-commands.js +268 -0
  154. package/dist/cli/src/runner-world-completion.js +76 -0
  155. package/dist/cli/src/runner-world-erasure.js +95 -0
  156. package/dist/cli/src/runner-world-evidence-sync.js +91 -0
  157. package/dist/cli/src/runner-world-journal.js +510 -0
  158. package/dist/cli/src/runner-world-native-filters.js +134 -0
  159. package/dist/cli/src/runner-world-observations.js +50 -0
  160. package/dist/cli/src/runner-world-session-protocol.js +134 -0
  161. package/dist/cli/src/saved-records.js +40 -0
  162. package/dist/cli/src/scenario-authoring.js +212 -0
  163. package/dist/cli/src/scenario-discovery.js +216 -0
  164. package/dist/cli/src/scenario-paths.js +68 -0
  165. package/dist/cli/src/scenario-requests.js +4 -0
  166. package/dist/cli/src/service-receipts.js +105 -0
  167. package/dist/cli/src/simple/authoring.js +270 -0
  168. package/dist/cli/src/simple/credit-approval.js +16 -0
  169. package/dist/cli/src/simple/help.js +98 -0
  170. package/dist/cli/src/simple/init.js +61 -0
  171. package/dist/cli/src/simple/library-sharing.js +414 -0
  172. package/dist/cli/src/simple/metadata-requests.js +46 -0
  173. package/dist/cli/src/simple/native-report.js +841 -0
  174. package/dist/cli/src/simple/native-tools.js +780 -0
  175. package/dist/cli/src/simple/plan-apply.js +506 -0
  176. package/dist/cli/src/simple/reassessment.js +323 -0
  177. package/dist/cli/src/simple/register.js +123 -0
  178. package/dist/cli/src/simple/run.js +122 -0
  179. package/dist/cli/src/simple/status.js +49 -0
  180. package/dist/cli/src/simple/tool-renewal.js +308 -0
  181. package/dist/cli/src/simple/tool-reuse.js +526 -0
  182. package/dist/cli/src/simple/tool-start-catalog.js +100 -0
  183. package/dist/cli/src/simple/tool-state.js +426 -0
  184. package/dist/cli/src/simple/tools-author.js +222 -0
  185. package/dist/cli/src/simple/watch.js +28 -0
  186. package/dist/cli/src/simple/world-reuse.js +586 -0
  187. package/dist/cli/src/simulation-intents.js +406 -0
  188. package/dist/cli/src/simulation-run.js +1058 -0
  189. package/dist/cli/src/source-batch.js +322 -0
  190. package/dist/cli/src/source-connection.js +451 -0
  191. package/dist/cli/src/source-runtime.js +346 -0
  192. package/dist/cli/src/source-selection-intents.js +75 -0
  193. package/dist/cli/src/source-sets.js +214 -0
  194. package/dist/cli/src/target-models.js +109 -0
  195. package/dist/cli/src/tool-catalog.js +224 -0
  196. package/dist/cli/src/tool-command-contracts.js +1 -0
  197. package/dist/cli/src/tool-command-export.js +245 -0
  198. package/dist/cli/src/tool-connection-contracts.js +1 -0
  199. package/dist/cli/src/tool-connection-reads.js +23 -0
  200. package/dist/cli/src/tool-contributions.js +58 -0
  201. package/dist/cli/src/tool-data.js +116 -0
  202. package/dist/cli/src/tool-datasets.js +176 -0
  203. package/dist/cli/src/tool-group-contracts.js +1 -0
  204. package/dist/cli/src/tool-group-reads.js +26 -0
  205. package/dist/cli/src/tool-group-state.js +147 -0
  206. package/dist/cli/src/tool-groups.js +311 -0
  207. package/dist/cli/src/tool-instances.js +186 -0
  208. package/dist/cli/src/tool-lifecycle-contracts.js +1 -0
  209. package/dist/cli/src/tool-lifecycle-reads.js +30 -0
  210. package/dist/cli/src/tool-lifecycle.js +323 -0
  211. package/dist/cli/src/tool-package-contracts.js +1 -0
  212. package/dist/cli/src/tool-package-reads.js +41 -0
  213. package/dist/cli/src/tool-records.js +53 -0
  214. package/dist/cli/src/tool-runtime.js +96 -0
  215. package/dist/cli/src/tool-state-contracts.js +1 -0
  216. package/dist/cli/src/tool-state-reads.js +31 -0
  217. package/dist/cli/src/tools.js +306 -0
  218. package/dist/cli/src/trial-actions.js +93 -0
  219. package/dist/cli/src/trial-checkpoints.js +133 -0
  220. package/dist/cli/src/trial-checks.js +70 -0
  221. package/dist/cli/src/trial-state.js +197 -0
  222. package/dist/cli/src/vault.js +266 -0
  223. package/dist/cli/src/workspace-registration.js +383 -0
  224. package/dist/cli/src/workspace-team-contracts.js +1 -0
  225. package/dist/cli/src/world-access.js +231 -0
  226. package/dist/cli/src/world-agent.js +4 -0
  227. package/dist/cli/src/world-checkpoint-scopes.js +15 -0
  228. package/dist/cli/src/world-contracts.js +6 -0
  229. package/dist/cli/src/world-creation.js +201 -0
  230. package/dist/cli/src/world-datasets.js +207 -0
  231. package/dist/cli/src/world-inspection.js +257 -0
  232. package/dist/cli/src/world-recipes.js +103 -0
  233. package/dist/cli/src/world-requests.js +241 -0
  234. package/dist/cli/src/world-session-checkpoints.js +196 -0
  235. package/dist/cli/src/world-session-lifecycle.js +207 -0
  236. package/dist/cli/src/world-session-requests.js +215 -0
  237. package/dist/cli/src/world-session-state.js +145 -0
  238. package/dist/cli/src/world-session-voice.js +66 -0
  239. package/dist/cli/src/world-sessions.js +288 -0
  240. package/dist/cli/src/world-stream.js +99 -0
  241. package/dist/cli/src/world-time-report.js +58 -0
  242. package/dist/contracts/agent-change-contracts.js +75 -0
  243. package/dist/contracts/agent-execution-contracts.js +38 -0
  244. package/dist/contracts/application-state-contracts.js +26 -0
  245. package/dist/contracts/assessment-aggregate-contracts.js +15 -0
  246. package/dist/contracts/assessment-contracts.js +74 -0
  247. package/dist/contracts/billing-commerce-contracts.js +107 -0
  248. package/dist/contracts/billing-wallet-contracts.js +43 -0
  249. package/dist/contracts/campaign-lifecycle.js +1 -0
  250. package/dist/contracts/campaign-world-contracts.js +80 -0
  251. package/dist/contracts/catalog-contracts.js +73 -0
  252. package/dist/contracts/ci-access-contracts.js +68 -0
  253. package/dist/contracts/ci-configuration-contracts.js +88 -0
  254. package/dist/contracts/ci-coverage-contracts.js +65 -0
  255. package/dist/contracts/ci-coverage-labels.js +25 -0
  256. package/dist/contracts/ci-delivery-contracts.js +61 -0
  257. package/dist/contracts/ci-result-contracts.js +103 -0
  258. package/dist/contracts/ci-run-contracts.js +51 -0
  259. package/dist/contracts/ci-setup-contracts.js +88 -0
  260. package/dist/contracts/clip-contracts.js +32 -0
  261. package/dist/contracts/comparison-contracts.js +53 -0
  262. package/dist/contracts/composite-contracts.js +34 -0
  263. package/dist/contracts/context-contracts.js +76 -0
  264. package/dist/contracts/continuation-report-contracts.js +1 -0
  265. package/dist/contracts/continuity-requirements.js +8 -0
  266. package/dist/contracts/coordinated-checkpoint-contracts.js +34 -0
  267. package/dist/contracts/dataset-contracts.js +88 -0
  268. package/dist/contracts/dependency-decision-contracts.js +36 -0
  269. package/dist/contracts/dependency-protocol-contracts.js +43 -0
  270. package/dist/contracts/director-execution.js +51 -0
  271. package/dist/contracts/driver-review-contracts.js +99 -0
  272. package/dist/contracts/evidence-contracts.js +65 -0
  273. package/dist/contracts/evidence-coverage-contracts.js +10 -0
  274. package/dist/contracts/export-contracts.js +44 -0
  275. package/dist/contracts/github-directory-contracts.js +96 -0
  276. package/dist/contracts/insight-contracts.js +11 -0
  277. package/dist/contracts/inspection-contracts.js +1 -0
  278. package/dist/contracts/local-claim-contracts.js +39 -0
  279. package/dist/contracts/local-configuration-contracts.js +7 -0
  280. package/dist/contracts/local-evidence-contracts.js +32 -0
  281. package/dist/contracts/local-grant-contracts.js +83 -0
  282. package/dist/contracts/local-input-contracts.js +19 -0
  283. package/dist/contracts/local-runner-contracts.js +42 -0
  284. package/dist/contracts/local-trial-live-contracts.js +16 -0
  285. package/dist/contracts/local-value-source-contracts.js +92 -0
  286. package/dist/contracts/local-world-native-contracts.js +8 -0
  287. package/dist/contracts/mcp-contracts.js +42 -0
  288. package/dist/contracts/media-contracts.js +28 -0
  289. package/dist/contracts/native-check-contracts.js +31 -0
  290. package/dist/contracts/native-identity-contracts.js +52 -0
  291. package/dist/contracts/onboarding-contracts.js +32 -0
  292. package/dist/contracts/performance-contracts.js +85 -0
  293. package/dist/contracts/planning-contracts.js +81 -0
  294. package/dist/contracts/real-operation-contracts.js +30 -0
  295. package/dist/contracts/recommendation-contracts.js +54 -0
  296. package/dist/contracts/report-contracts.js +1 -0
  297. package/dist/contracts/resource-lifecycle-contracts.js +57 -0
  298. package/dist/contracts/revision-contracts.js +12 -0
  299. package/dist/contracts/revision-request-contracts.js +39 -0
  300. package/dist/contracts/saved-record-contracts.js +76 -0
  301. package/dist/contracts/scenario-draft-comparison.js +91 -0
  302. package/dist/contracts/scenario-world-contracts.js +14 -0
  303. package/dist/contracts/service-contracts.js +1 -0
  304. package/dist/contracts/session-command-contracts.js +56 -0
  305. package/dist/contracts/session-contracts.js +65 -0
  306. package/dist/contracts/session-state-contracts.js +67 -0
  307. package/dist/contracts/session-voice-contracts.js +45 -0
  308. package/dist/contracts/source-set-contracts.js +26 -0
  309. package/dist/contracts/source-upload-contracts.js +22 -0
  310. package/dist/contracts/tool-command-contracts.js +57 -0
  311. package/dist/contracts/tool-connection-contracts.js +87 -0
  312. package/dist/contracts/tool-contribution-contracts.js +35 -0
  313. package/dist/contracts/tool-group-contracts.js +53 -0
  314. package/dist/contracts/tool-group-state-contracts.js +63 -0
  315. package/dist/contracts/tool-lifecycle-contracts.js +94 -0
  316. package/dist/contracts/tool-package-contracts.js +91 -0
  317. package/dist/contracts/tool-protocol-contracts.js +71 -0
  318. package/dist/contracts/tool-runtime-billing-contracts.js +28 -0
  319. package/dist/contracts/tool-setup-proposal.js +59 -0
  320. package/dist/contracts/tool-state-contracts.js +79 -0
  321. package/dist/contracts/trial-state-contracts.js +30 -0
  322. package/dist/contracts/typescript/agent-change.js +1 -0
  323. package/dist/contracts/typescript/assessment-aggregate.js +1 -0
  324. package/dist/contracts/typescript/assessment.js +1 -0
  325. package/dist/contracts/typescript/billing-commerce.js +1 -0
  326. package/dist/contracts/typescript/campaigns.js +1 -0
  327. package/dist/contracts/typescript/catalog.js +1 -0
  328. package/dist/contracts/typescript/ci-access.js +1 -0
  329. package/dist/contracts/typescript/ci-configuration.js +1 -0
  330. package/dist/contracts/typescript/ci-delivery.js +1 -0
  331. package/dist/contracts/typescript/ci-result.js +1 -0
  332. package/dist/contracts/typescript/ci-runs.js +1 -0
  333. package/dist/contracts/typescript/clips.js +1 -0
  334. package/dist/contracts/typescript/comparison.js +1 -0
  335. package/dist/contracts/typescript/composite.js +1 -0
  336. package/dist/contracts/typescript/connections.js +1 -0
  337. package/dist/contracts/typescript/context.js +1 -0
  338. package/dist/contracts/typescript/datasets.js +1 -0
  339. package/dist/contracts/typescript/evidence-coverage.js +1 -0
  340. package/dist/contracts/typescript/export.js +1 -0
  341. package/dist/contracts/typescript/github-directory.js +1 -0
  342. package/dist/contracts/typescript/insight.js +1 -0
  343. package/dist/contracts/typescript/inspection.js +1 -0
  344. package/dist/contracts/typescript/intake.js +1 -0
  345. package/dist/contracts/typescript/local-claim.js +1 -0
  346. package/dist/contracts/typescript/local-configuration.js +1 -0
  347. package/dist/contracts/typescript/local-evidence.js +1 -0
  348. package/dist/contracts/typescript/local-grant.js +1 -0
  349. package/dist/contracts/typescript/local-input.js +1 -0
  350. package/dist/contracts/typescript/local-runner.js +1 -0
  351. package/dist/contracts/typescript/local-trial-live.js +1 -0
  352. package/dist/contracts/typescript/local-value-source.js +1 -0
  353. package/dist/contracts/typescript/local-world-native.js +1 -0
  354. package/dist/contracts/typescript/mcp.js +1 -0
  355. package/dist/contracts/typescript/media.js +1 -0
  356. package/dist/contracts/typescript/native-check.js +1 -0
  357. package/dist/contracts/typescript/native-identity.js +1 -0
  358. package/dist/contracts/typescript/onboarding.js +1 -0
  359. package/dist/contracts/typescript/performance.js +1 -0
  360. package/dist/contracts/typescript/planning.js +1 -0
  361. package/dist/contracts/typescript/preparation.js +1 -0
  362. package/dist/contracts/typescript/real-operation.js +1 -0
  363. package/dist/contracts/typescript/recommendation.js +1 -0
  364. package/dist/contracts/typescript/reports.js +1 -0
  365. package/dist/contracts/typescript/resource-lifecycle.js +1 -0
  366. package/dist/contracts/typescript/saved-record.js +1 -0
  367. package/dist/contracts/typescript/scenarios.js +1 -0
  368. package/dist/contracts/typescript/services.js +1 -0
  369. package/dist/contracts/typescript/session-command.js +1 -0
  370. package/dist/contracts/typescript/session-state.js +1 -0
  371. package/dist/contracts/typescript/session-voice.js +1 -0
  372. package/dist/contracts/typescript/session.js +1 -0
  373. package/dist/contracts/typescript/tool-command.js +1 -0
  374. package/dist/contracts/typescript/tool-connection.js +1 -0
  375. package/dist/contracts/typescript/tool-contribution.js +1 -0
  376. package/dist/contracts/typescript/tool-groups.js +1 -0
  377. package/dist/contracts/typescript/tool-lifecycle.js +1 -0
  378. package/dist/contracts/typescript/tool-package.js +1 -0
  379. package/dist/contracts/typescript/tool-state.js +1 -0
  380. package/dist/contracts/typescript/trial-state.js +1 -0
  381. package/dist/contracts/typescript/vault.js +1 -0
  382. package/dist/contracts/typescript/workspace-team.js +1 -0
  383. package/dist/contracts/typescript/world-stream.js +1 -0
  384. package/dist/contracts/workspace-team-contracts.js +55 -0
  385. package/dist/contracts/world-command.js +65 -0
  386. package/dist/contracts/world-lifetime-contracts.js +74 -0
  387. package/dist/contracts/world-recipe-contracts.js +27 -0
  388. package/dist/contracts/world-stream-contracts.js +27 -0
  389. package/dist/package.json +1 -0
  390. package/package.json +16 -16
  391. package/LICENSE.md +0 -23
  392. package/PACKAGING.json +0 -50
@@ -0,0 +1,780 @@
1
+ // Hosted tool sessions for the simple CLI: start saved tool versions outside
2
+ // any drill, list and stop those sessions, and connect an agent on this machine
3
+ // to them through a loopback proxy. A session is root's mixed tool setup, the
4
+ // same review, estimate and approval the web app and the remote MCP use. Root
5
+ // decides each selected service's lane: a reviewed catalog service runs on the
6
+ // shared tool pool, and any other version, an uploaded custom package included,
7
+ // runs in its own isolated runtime. Nothing runs on this machine. Starting
8
+ // reserves credits only after the human approves the quoted maximum. Only
9
+ // versions saved in the workspace can start; the interactive picker adopts
10
+ // selected catalog releases without starting them before review. Sessions started before mixed
11
+ // setups stay readable, stoppable and connectable by their id.
12
+ import { randomUUID } from 'node:crypto';
13
+ import { approveCredits } from './credit-approval.js';
14
+ import { CliError } from '../http.js';
15
+ import { selectOne, terminalText } from '../prompts.js';
16
+ import { requestInput } from '../request-input.js';
17
+ import { selectedWorkspace } from '../world-access.js';
18
+ import { errorCode, isTransient, jitteredInterval, retryTransient, sleep } from '../native/control-plane.js';
19
+ import { openIsolatedGrant, revokeGrant } from '../native/isolated-access.js';
20
+ import { STANDALONE_ACCESS_ERRORS, StandaloneProxy } from '../native/standalone-proxy.js';
21
+ import { getStandaloneBatch, stopStandaloneBatch, STANDALONE_ACTIVE, STANDALONE_TERMINAL, } from '../native/standalone-tools.js';
22
+ import { admitSetup, getSetup, getSetupOption, listSetupOptions, listSetups, parseSetupInput, quoteSetup, recoverSetup, replaceSetup, reviewSetup, setupItems, stopSetup, SETUP_CATEGORIES, SETUP_CHILD_ENDED, SETUP_ENDED, SETUP_MAX_SECONDS, SETUP_MAX_SELECTIONS, SETUP_MIN_SECONDS, SETUP_RUNNING, } from '../native/tool-setups.js';
23
+ import { amount } from './plan-apply.js';
24
+ import { checkRenewalArguments, RENEWAL_VERBS, toolRenewalCommand } from './tool-renewal.js';
25
+ import { toolStateCommand } from './tool-state.js';
26
+ import { chooseStartCatalog } from './tool-start-catalog.js';
27
+ /** The commands this module owns under `firedrill tools`. */
28
+ export const NATIVE_TOOL_ACTIONS = new Set(['runnable', 'start', 'sessions', 'session', 'access', 'state']);
29
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
30
+ const DURATIONS = [
31
+ { value: 3_600, label: '1 hour' }, { value: 4 * 3_600, label: '4 hours' },
32
+ { value: 24 * 3_600, label: '1 day' }, { value: 7 * 24 * 3_600, label: '7 days' },
33
+ ];
34
+ /** Option pages read for the interactive picker (32 versions per page). */
35
+ const PICKER_PAGES = 5;
36
+ /** Option pages read to find the versions named on the command line (100 per page). */
37
+ const LOOKUP_PAGES = 20;
38
+ /** An estimate this close to expiry is not approved: a session is quoted only once. */
39
+ const QUOTE_MARGIN_MS = 60_000;
40
+ const FOLLOW_MS = 3_000;
41
+ const FOLLOW_LIMIT_MS = 15 * 60_000;
42
+ /** How often access re-reads the session, so it ends when the session does. */
43
+ const ACCESS_POLL_MS = 30_000;
44
+ /** Time allowed to revoke this access's isolated connections after it stops (Ctrl-C included). */
45
+ const REVOKE_MS = 15_000;
46
+ const tty = (options) => !options.json && !!process.stdin.isTTY && !!process.stderr.isTTY;
47
+ const scopeOf = (options) => selectedWorkspace({ workspace: options.workspace, signal: options.signal,
48
+ interactive: tty(options), openBrowser: options.openBrowser && !options.json });
49
+ function pageNumber(value) {
50
+ if (value === undefined)
51
+ return undefined;
52
+ if (!/^[1-9][0-9]{0,8}$/.test(value))
53
+ throw new CliError('arguments_invalid', 'Use --page with a number of 1 or more.', 2);
54
+ return Number(value);
55
+ }
56
+ function duration(seconds) {
57
+ if (seconds % 86_400 === 0)
58
+ return `${seconds / 86_400} day${seconds === 86_400 ? '' : 's'}`;
59
+ if (seconds % 3_600 === 0)
60
+ return `${seconds / 3_600} hour${seconds === 3_600 ? '' : 's'}`;
61
+ if (seconds % 60 === 0)
62
+ return `${seconds / 60} minutes`;
63
+ return `${seconds} seconds`;
64
+ }
65
+ /** Plain words for root's session, admission and instance states; anything else is shown as given. */
66
+ const STATE_TEXT = {
67
+ reviewed: 'reviewed, not approved (nothing reserved)', admitting: 'approved, starting', admitted: 'approved',
68
+ needs_attention: 'needs attention', queued: 'approved, waiting for capacity', starting: 'starting',
69
+ preparing: 'preparing', effect_pending: 'applying its starting data', prepared: 'prepared', active: 'running', draining: 'stopping',
70
+ closing: 'stopping', reconciling: 'confirming an uncertain step', stopped: 'stopped', cancelled: 'cancelled', canceled: 'cancelled',
71
+ rejected: 'not started: capacity refused', failed: 'failed', expired: 'expired', closed: 'closed',
72
+ };
73
+ const stateText = (state) => STATE_TEXT[state] ?? state;
74
+ const LANE_TEXT = { pooled: 'shared pool', isolated: 'isolated' };
75
+ const running = (item) => item.instance_state === SETUP_RUNNING && item.terminal_at === null;
76
+ /** A child that will not change without the human: running, ended, cancelled or waiting for attention. */
77
+ const childSettled = (item) => running(item) || item.terminal_at !== null || item.admission_state === 'cancelled'
78
+ || item.admission_state === 'needs_attention' || (item.instance_state !== null && SETUP_CHILD_ENDED.has(item.instance_state));
79
+ const settled = (setup, items) => setup.approved_at === null || setup.stop_requested_at !== null
80
+ || SETUP_ENDED.has(setup.state) || items.every(childSettled);
81
+ /**
82
+ * A mixed tool session's summary. `viewer` is the signed-in member when the
83
+ * session may be someone else's: root lists every member's sessions, but only
84
+ * the member who started one can stop or extend it, so those next steps are
85
+ * offered to that member only.
86
+ */
87
+ function setupText(setup, items, viewer) {
88
+ const own = viewer === undefined || setup.actor_id === viewer.toLowerCase();
89
+ return [
90
+ `Tool session ${setup.id}: ${stateText(setup.state)} (${duration(setup.duration_seconds)} per tool)`,
91
+ ...items.map((one) => ` ${one.position + 1}. ${terminalText(one.name)} service ${terminalText(one.service_key)}, ${one.operations.length} operations, `
92
+ + `${LANE_TEXT[one.lane]} ${stateText(one.instance_state ?? one.admission_state)}${one.failure_code ? ` (${terminalText(one.failure_code)})` : ''}`
93
+ + `${one.instance_id ? ` instance ${one.instance_id}` : ''}`),
94
+ items.some(running) ? `Connect an agent on this machine: firedrill tools access ${setup.id}` : null,
95
+ items.some((one) => one.lane === 'pooled' && one.instance_id) ? 'Save or change a shared-pool tool\'s data: firedrill tools state <instance-id>' : null,
96
+ own && items.some((one) => one.lane === 'pooled' && running(one)) && !setup.stop_requested_at
97
+ ? `Extend its shared-pool tools: firedrill tools session ${setup.id} renew --seconds <n>` : null,
98
+ SETUP_ENDED.has(setup.state) || setup.stop_requested_at ? null
99
+ : own ? `Stop it: firedrill tools session ${setup.id} stop` : 'Started by another member; only they can stop or extend it.',
100
+ ].filter((line) => line !== null).join('\n');
101
+ }
102
+ /** A session started before mixed setups (history). */
103
+ function batchText(batch) {
104
+ const until = batch.expires_at ? `, until ${terminalText(batch.expires_at)}` : '';
105
+ return [
106
+ `Tool session ${batch.id}: ${stateText(batch.state)} (${duration(batch.duration_seconds)}${until})`,
107
+ ...batch.instances.map((one) => ` ${one.position + 1}. ${terminalText(one.name)} ${terminalText(one.version)} service ${terminalText(one.service_key)}`
108
+ + `, ${one.operations.length} operations ${stateText(one.state)} instance ${one.id}`),
109
+ batch.state === STANDALONE_ACTIVE ? `Connect an agent on this machine: firedrill tools access ${batch.id}` : null,
110
+ batch.state === STANDALONE_ACTIVE ? 'Save or change a tool\'s data: firedrill tools state <instance-id>' : null,
111
+ batch.state === STANDALONE_ACTIVE && !batch.stop_requested_at ? `Extend it: firedrill tools session ${batch.id} renew --seconds <n>` : null,
112
+ STANDALONE_TERMINAL.has(batch.state) ? null : `Stop it: firedrill tools session ${batch.id} stop`,
113
+ ].filter((line) => line !== null).join('\n');
114
+ }
115
+ function quoteText(quotes, items) {
116
+ const first = quotes.quotes[0];
117
+ const money = (one, units) => amount(units, one.credit_scale, one.denomination);
118
+ // One total only when every quote is in the same unit; otherwise each maximum is shown on its own.
119
+ const sameUnit = quotes.quotes.every((one) => one.denomination === first.denomination && one.credit_scale === first.credit_scale);
120
+ const total = quotes.quotes.reduce((sum, one) => sum + one.maximum_units, 0);
121
+ const label = (one) => one.lane === 'pooled'
122
+ ? (one.component === 'baseline' ? 'shared pool preparation' : 'shared pool running time')
123
+ : `${terminalText(items[one.position ?? 0]?.name ?? 'tool')} (${(one.position ?? 0) + 1}, isolated) running time`;
124
+ const earliest = quotes.quotes.reduce((soonest, one) => Date.parse(one.expires_at) < Date.parse(soonest) ? one.expires_at : soonest, first.expires_at);
125
+ return [
126
+ `Cloud-work estimate (maximum, reserved only once approved)${sameUnit ? `: ${money(first, total)}` : ', every part below:'}`,
127
+ ...quotes.quotes.map((one) => ` ${label(one)}: up to ${money(one, one.maximum_units)}`),
128
+ `Estimate expires: ${terminalText(earliest)}`,
129
+ ].join('\n');
130
+ }
131
+ const live = (one) => Date.parse(one.expires_at) - Date.now() > QUOTE_MARGIN_MS;
132
+ /** Every version the picker can offer: available ones only, bounded and said when cut. */
133
+ async function pickable(scope, options) {
134
+ const rows = [];
135
+ for (let page = 1; page <= PICKER_PAGES; page++) {
136
+ const result = await listSetupOptions(scope, { page, pageSize: 32, search: options.search, signal: options.signal });
137
+ rows.push(...result.results.filter((one) => one.available));
138
+ if (!result.next)
139
+ return { rows, more: false };
140
+ }
141
+ return { rows, more: true };
142
+ }
143
+ /** The saved versions named on the command line, read from the option pages (bounded). */
144
+ async function optionsFor(scope, ids, options) {
145
+ const found = new Map();
146
+ for (let page = 1; page <= LOOKUP_PAGES && found.size < ids.length; page++) {
147
+ const result = await listSetupOptions(scope, { page, pageSize: 100, signal: options.signal });
148
+ for (const one of result.results)
149
+ if (ids.includes(one.implementation_id))
150
+ found.set(one.implementation_id, one);
151
+ if (!result.next)
152
+ break;
153
+ }
154
+ return found;
155
+ }
156
+ /** One selection per chosen version: its one service (or --service, or a choice) with every operation it has. */
157
+ async function selectionsFor(scope, ids, options, known) {
158
+ const found = known ? new Map(known.map((one) => [one.implementation_id, one])) : await optionsFor(scope, ids, options);
159
+ const selections = [];
160
+ for (const implementationId of ids) {
161
+ const option = found.get(implementationId);
162
+ if (!option)
163
+ throw new CliError('tool_unavailable', `Tool version ${implementationId} is not saved in this workspace. List saved versions with firedrill tools runnable. Nothing was started.`, 2);
164
+ const label = `${option.name} ${option.version}`;
165
+ if (!option.available)
166
+ throw new CliError('tool_unavailable', `${label} cannot start in a tool session. Nothing was started.`, 2);
167
+ let service = options.service !== undefined ? option.services.find((one) => one.service_key === options.service) : undefined;
168
+ if (options.service !== undefined && !service) {
169
+ throw new CliError('arguments_invalid', `${label} has no service "${options.service}". Its services: ${option.services.map((one) => one.service_key).join(', ')}.`, 2);
170
+ }
171
+ if (!service && option.services.length === 1)
172
+ service = option.services[0];
173
+ if (!service && tty(options)) {
174
+ service = await selectOne(`Which service of ${label}?`, option.services.map((one) => ({ value: one, label: one.service_key,
175
+ detail: `${one.operation_count} operations, ${LANE_TEXT[one.lane]}${one.has_preview ? ', with a preview' : ''}` })), options.signal);
176
+ }
177
+ if (!service) {
178
+ throw new CliError('tool_service_required', `${label} has several services (${option.services.map((one) => one.service_key).join(', ')}). Use --service <key>. Nothing was started.`, 2);
179
+ }
180
+ const detail = await getSetupOption(scope, { implementationId, serviceKey: service.service_key, signal: options.signal });
181
+ // Packaged starting data: a seed or saved dataset is chosen in a --file selection.
182
+ selections.push({ implementation_id: implementationId, service_key: service.service_key, operations: [...detail.operations], seed_id: null, dataset_id: null });
183
+ }
184
+ return selections;
185
+ }
186
+ async function chooseSeconds(options, fromFile) {
187
+ if (options.seconds !== undefined) {
188
+ if (!/^[1-9][0-9]{0,8}$/.test(options.seconds) || Number(options.seconds) < SETUP_MIN_SECONDS || Number(options.seconds) > SETUP_MAX_SECONDS) {
189
+ throw new CliError('tool_duration_invalid', `Use --seconds with a whole number from ${SETUP_MIN_SECONDS} to ${SETUP_MAX_SECONDS} (one year). Nothing was started.`, 2);
190
+ }
191
+ if (fromFile !== null && Number(options.seconds) !== fromFile)
192
+ throw new CliError('arguments_invalid', 'The selection file already sets duration_seconds; leave out --seconds.', 2);
193
+ return Number(options.seconds);
194
+ }
195
+ if (fromFile !== null)
196
+ return fromFile;
197
+ if (tty(options))
198
+ return await selectOne('How long should each tool run?', DURATIONS, options.signal);
199
+ throw new CliError('tool_duration_required', 'Use --seconds <n> to say how long the tools run. Nothing was started.', 2);
200
+ }
201
+ /** A session is quoted once, so an expired estimate is never approved: close the unused review, then review again. */
202
+ function expiredReview(setupId) {
203
+ return new CliError('tool_quote_expired', 'This session\'s estimate has expired or expires within a minute, and a session is quoted only once. '
204
+ + `Nothing was reserved or started. Close this unused review with firedrill tools session ${setupId} stop --approve-tool-change, `
205
+ + `then review the same tools again with firedrill tools session ${setupId} replace --approve-tool-change, `
206
+ + 'or choose a new selection with firedrill tools start without --request-key.', 2);
207
+ }
208
+ /**
209
+ * `tools start [<version-id>...] [--service <key>] [--seconds <n>] [--file <selection.json>]
210
+ * [--approve-tool-change] [--request-key <key>] [--usage-quote <key>] [--follow]`.
211
+ */
212
+ async function start(args, options, emit) {
213
+ const ids = args.slice(1).map((one) => one.toLowerCase());
214
+ if (ids.length > SETUP_MAX_SELECTIONS || !ids.every((one) => UUID.test(one)) || new Set(ids).size !== ids.length) {
215
+ throw new CliError('arguments_invalid', `Use tools start with up to ${SETUP_MAX_SELECTIONS} different version ids from firedrill tools runnable, or none to choose.`, 2);
216
+ }
217
+ if (options.file !== undefined && ids.length)
218
+ throw new CliError('arguments_invalid', 'Use either --file or tool version ids, not both.', 2);
219
+ if (options.request !== undefined && !UUID.test(options.request))
220
+ throw new CliError('arguments_invalid', 'Use --request-key with the key tools start printed.', 2);
221
+ if (options.quote !== undefined && (!UUID.test(options.quote) || options.request === undefined)) {
222
+ throw new CliError('arguments_invalid', 'Use --usage-quote with the quote key and --request-key that tools start printed together.', 2);
223
+ }
224
+ const scope = await scopeOf(options);
225
+ const signal = options.signal;
226
+ const requestKey = (options.request ?? randomUUID()).toLowerCase();
227
+ const recovery = `firedrill tools start --request-key ${requestKey}`;
228
+ // A review already made under this key is continued, never made again.
229
+ let setup = options.request ? await recoverSetup(scope, { requestKey, signal }) : null;
230
+ let items = null;
231
+ if (!setup) {
232
+ let durationSeconds;
233
+ let selections;
234
+ if (options.file !== undefined) {
235
+ const input = parseSetupInput(JSON.parse(await requestInput(options.file, signal, 1024 * 1024)));
236
+ durationSeconds = await chooseSeconds(options, input.durationSeconds);
237
+ selections = input.selections;
238
+ }
239
+ else {
240
+ let chosen = ids;
241
+ if (!chosen.length) {
242
+ if (!tty(options))
243
+ throw new CliError('tool_selection_required', 'Use tools start <version-id>... (ids from firedrill tools runnable) or --file <selection.json>. Nothing was started.', 2);
244
+ const { rows, more } = await pickable(scope, options);
245
+ if (more)
246
+ process.stderr.write(`Showing the first ${rows.length} tools; narrow the list with --search.\n`);
247
+ chosen = await chooseStartCatalog(scope, rows, { search: options.search, signal, requestKey });
248
+ // Newly adopted catalog versions require fresh canonical option reads.
249
+ }
250
+ selections = await selectionsFor(scope, chosen, options);
251
+ durationSeconds = await chooseSeconds(options, null);
252
+ }
253
+ emit('tool_start_intent', { request_key: requestKey, duration_seconds: durationSeconds, selections }, `Tool start request: ${requestKey}\nIf interrupted, continue with ${recovery}. Do not start again with a new request.`);
254
+ try {
255
+ ({ setup, items } = await retryTransient(() => reviewSetup(scope, { requestKey, durationSeconds, selections, signal }), { signal, attempts: 3 }));
256
+ }
257
+ catch (error) {
258
+ // Only an unanswered review may have landed; a refusal or a bad local input is reported as it is.
259
+ if (signal.aborted || !isTransient(error))
260
+ throw error;
261
+ setup = await recoverSetup(scope, { requestKey, signal }).catch(() => null);
262
+ if (!setup)
263
+ throw new CliError('tool_start_unconfirmed', `The review was not confirmed. Continue with ${recovery}; nothing new is reviewed or charged twice.`);
264
+ }
265
+ }
266
+ const reviewed = setup;
267
+ let children = items ?? await setupItems(scope, { setup: reviewed, signal });
268
+ if (setup.state === 'reviewed' && setup.approved_at === null && setup.stop_requested_at === null) {
269
+ // Quoting reserves nothing. Root keeps one estimate per session and replays it for its quote key only,
270
+ // so a later run admits exactly the estimate the human saw. An approval holds only for that key.
271
+ const original = setup.quote_request_key;
272
+ const given = options.quote?.toLowerCase();
273
+ if (given !== undefined && original !== null && given !== original)
274
+ throw new CliError('idempotency_conflict', 'That quote key is not this setup’s original estimate. Read the original setup before continuing. Nothing was admitted.', 2);
275
+ const quoteKey = original ?? given ?? randomUUID();
276
+ let notice = '';
277
+ if (given !== undefined && original === null)
278
+ notice = 'No estimate was quoted before under that quote key, so this is a new one. ';
279
+ else if (given !== undefined && given !== original)
280
+ notice = `This session's estimate was quoted under ${original}, not the key given, so it needs its own approval. `;
281
+ const quoted = children;
282
+ const quotes = await retryTransient(() => quoteSetup(scope, { setup: reviewed, items: quoted, requestKey: quoteKey, signal }), { signal, attempts: 3 });
283
+ if (!quotes.quotes.every(live))
284
+ throw expiredReview(setup.id);
285
+ let approved = options.approve === true && (!quotes.quotes.some(quote => quote.credit_approval_required) || (original !== null && given === original));
286
+ const interactive = tty(options);
287
+ if (!approved && interactive)
288
+ process.stderr.write(setupText(setup, children) + '\n');
289
+ emit('tool_start_quote', quotes, `${notice}${quoteText(quotes, children)}`);
290
+ if (!approved && interactive) {
291
+ approved = await selectOne(`Start ${children.length} tool${children.length === 1 ? '' : 's'} for ${duration(setup.duration_seconds)} each?`, [
292
+ { value: false, label: 'Not now', detail: 'Nothing is reserved or started.' },
293
+ { value: true, label: 'Start tools', detail: 'Reserves up to the estimate above. The tools run in the cloud, not on this machine.' },
294
+ ], signal);
295
+ }
296
+ if (!approved) {
297
+ throw new CliError('tool_change_consent_required', `${notice}Nothing was reserved or started. Show the human this estimate; after they approve it: `
298
+ + `${recovery} --usage-quote ${quoteKey} --approve-tool-change`, 2);
299
+ }
300
+ const creditApproval = await approveCredits(quotes.quotes, options, interactive, `${recovery} --usage-quote ${quoteKey} --approve-tool-change`);
301
+ try {
302
+ setup = await retryTransient(() => admitSetup(scope, { quotes, signal, creditApproval }), { signal, attempts: 3 });
303
+ }
304
+ catch (error) {
305
+ if (signal.aborted)
306
+ throw error;
307
+ // An unanswered approval may have landed: the session says, and nothing is approved twice.
308
+ const current = await getSetup(scope, { setupId: quotes.setup_id, signal }).catch(() => null);
309
+ if (!current || current.approved_at === null) {
310
+ // Root admits only a current estimate; an expired one is never approved, so the unused review is closed instead.
311
+ if (errorCode(error) === 'native_tool_setup_quote_expired')
312
+ throw expiredReview(quotes.setup_id);
313
+ if (error instanceof CliError && error.httpStatus !== null && !isTransient(error))
314
+ throw error;
315
+ throw new CliError('tool_start_unconfirmed', `The approval was not confirmed. Check firedrill tools session ${quotes.setup_id}; if it is still reviewed, run ${recovery} --usage-quote ${quoteKey} --approve-tool-change.`);
316
+ }
317
+ setup = current;
318
+ }
319
+ const admitted = setup;
320
+ children = await setupItems(scope, { setup: admitted, signal }).catch(() => children);
321
+ }
322
+ if (!setup)
323
+ throw new CliError('tool_start_unconfirmed', 'The tool session was not confirmed. Recover the original request before starting another.');
324
+ if (options.follow) {
325
+ const deadline = Date.now() + FOLLOW_LIMIT_MS;
326
+ let shown = '';
327
+ while (!settled(setup, children) && Date.now() < deadline) {
328
+ const text = setupText(setup, children);
329
+ if (text !== shown) {
330
+ emit('tool_session', { ...setup, items: children }, text);
331
+ shown = text;
332
+ }
333
+ if (!await sleep(jitteredInterval(FOLLOW_MS), signal))
334
+ return;
335
+ const setupId = setup.id;
336
+ const current = await retryTransient(() => getSetup(scope, { setupId, signal }), { signal, attempts: 4 });
337
+ children = await retryTransient(() => setupItems(scope, { setup: current, signal }), { signal, attempts: 4 });
338
+ setup = current;
339
+ }
340
+ }
341
+ emit('tool_session', { ...setup, items: children }, setupText(setup, children)
342
+ + (settled(setup, children) ? '' : `\nFollow it: firedrill tools session ${setup.id}`));
343
+ }
344
+ /** `tools runnable [--search <text>] [--page <n>]`: saved versions and the lane each service would start in. */
345
+ async function runnable(options, emit) {
346
+ const scope = await scopeOf(options);
347
+ const page = await listSetupOptions(scope, { page: pageNumber(options.page), pageSize: 32, search: options.search, signal: options.signal });
348
+ const lines = page.results.map((one) => `${one.implementation_id} ${terminalText(one.name)} ${terminalText(one.version)} `
349
+ + (one.available ? one.services.map((service) => `${terminalText(service.service_key)} (${LANE_TEXT[service.lane]}, ${service.operation_count} operations`
350
+ + `${service.has_preview ? ', preview' : ''})`).join('; ') : 'cannot start in a tool session'));
351
+ const shownTo = ((page.page ?? 1) - 1) * 32 + page.results.length;
352
+ emit('tool_runnable', page, [
353
+ ...(lines.length ? lines : ['No saved tool version matches. Browse firedrill tools catalog and save one with firedrill tools save <release-id>, '
354
+ + 'or push your own package with firedrill tools push (neither starts anything).']),
355
+ page.count > shownTo ? `${page.count - shownTo} more: add --page ${(page.page ?? 1) + 1}` : null,
356
+ lines.length ? 'Start: firedrill tools start <version-id>... --seconds <n> [--service <key>]' : null,
357
+ ].filter((line) => line !== null).join('\n'));
358
+ }
359
+ /** `tools sessions [--state all|active|preparing|closed] [--search <text>] [--page <n>]` (`pending` still means `preparing`). */
360
+ async function sessions(options, emit) {
361
+ const category = options.state === 'pending' ? 'preparing' : options.state;
362
+ if (category !== undefined && !SETUP_CATEGORIES.includes(category)) {
363
+ throw new CliError('arguments_invalid', `Use --state ${SETUP_CATEGORIES.join('|')}.`, 2);
364
+ }
365
+ const scope = await scopeOf(options);
366
+ const page = await listSetups(scope, { page: pageNumber(options.page), category: category, search: options.search, signal: options.signal });
367
+ const lines = page.results.map((one) => `${one.id} ${stateText(one.state)} ${one.item_count} tool${one.item_count === 1 ? '' : 's'}`
368
+ + `, ${duration(one.duration_seconds)} per tool, created ${terminalText(one.created_at)}`);
369
+ const shownTo = ((page.page ?? 1) - 1) * 25 + page.results.length;
370
+ emit('tool_sessions', page, [
371
+ ...(lines.length ? lines : ['No tool sessions match.']),
372
+ page.count > shownTo ? `${page.count - shownTo} more: add --page ${(page.page ?? 1) + 1}` : null,
373
+ 'Details: firedrill tools session <session-id>',
374
+ ].filter((line) => line !== null).join('\n'));
375
+ }
376
+ /** A session by id: a mixed setup, or a session started before them (read by its batch id, for history). */
377
+ async function sessionById(scope, id, signal) {
378
+ try {
379
+ const setup = await getSetup(scope, { setupId: id, signal });
380
+ return { setup, items: await setupItems(scope, { setup, signal }) };
381
+ }
382
+ catch (error) {
383
+ if (!(error instanceof CliError && error.httpStatus === 404))
384
+ throw error;
385
+ return { batch: await getStandaloneBatch(scope, { batchId: id, signal }) };
386
+ }
387
+ }
388
+ async function stopConsent(text, id, options) {
389
+ let approved = options.approve === true;
390
+ if (!approved && tty(options)) {
391
+ process.stderr.write(text + '\n');
392
+ approved = await selectOne('Stop this tool session?', [
393
+ { value: false, label: 'Keep it running' },
394
+ { value: true, label: 'Stop the session', detail: 'Every tool in it stops and its access ends. Usage is settled by the service.' },
395
+ ], options.signal);
396
+ }
397
+ if (!approved)
398
+ throw new CliError('tool_change_consent_required', `Nothing was stopped. After the human approves: firedrill tools session ${id} stop --approve-tool-change`, 2);
399
+ }
400
+ /**
401
+ * The shared-pool batch a renewal extends: a session started before mixed setups is that batch; a mixed setup's
402
+ * pooled tools run as one batch on the shared pool. Isolated tools keep their own lifetime.
403
+ */
404
+ async function renewalTarget(scope, found, signal) {
405
+ if ('batch' in found)
406
+ return { sessionId: found.batch.id, batch: found.batch, isolatedTools: 0 };
407
+ const { setup, items } = found;
408
+ const pooled = [...new Set(items.flatMap((one) => one.lane === 'pooled' && one.pooled_batch_id ? [one.pooled_batch_id] : []))];
409
+ if (!pooled.length) {
410
+ throw new CliError('tool_renewal_unavailable', 'Renewal extends shared-pool tools, and this session has none on the shared pool. '
411
+ + 'Isolated tools keep their own end time. Nothing was sent.', 2);
412
+ }
413
+ if (pooled.length > 1)
414
+ throw new CliError('native_response_unconfirmed', 'This session\'s shared-pool tools could not be confirmed as one batch. Nothing was sent.');
415
+ const batch = await getStandaloneBatch(scope, { batchId: pooled[0], signal });
416
+ return { sessionId: setup.id, batch, isolatedTools: items.filter((one) => one.lane === 'isolated').length };
417
+ }
418
+ /**
419
+ * `tools session <session-id> replace [--request-key <key>] [--approve-tool-change]`: root copies a stopped,
420
+ * never-approved session's exact tools, seeds and duration into a new review. Nothing is reserved or started;
421
+ * the new review's estimate is approved with tools start --request-key. Every attempt sends the same request key,
422
+ * so root returns the same replacement and refuses a key that belongs to another original.
423
+ */
424
+ async function replaceReview(scope, found, options, emit) {
425
+ if ('batch' in found) {
426
+ throw new CliError('tool_replacement_unavailable', 'Only a session reviewed with firedrill tools start can be reviewed again this way. '
427
+ + 'Start a new request without --request-key. Nothing was sent.', 2);
428
+ }
429
+ const { setup, items } = found;
430
+ const signal = options.signal;
431
+ if (setup.actor_id !== scope.owner.toLowerCase()) {
432
+ throw new CliError('native_tool_setup_owner_only', `Tool session ${setup.id} was started by another member, and only they can review it again. Nothing was sent.`, 2);
433
+ }
434
+ if (setup.approved_at !== null) {
435
+ throw new CliError('native_tool_setup_replacement_requires_unused_stop', `Tool session ${setup.id} was approved, so it is never reviewed again. `
436
+ + 'Start a new request with firedrill tools start. Nothing was sent.', 2);
437
+ }
438
+ if (setup.stop_requested_at === null) {
439
+ throw new CliError('native_tool_setup_replacement_requires_unused_stop', `Close this unapproved review first: firedrill tools session ${setup.id} stop `
440
+ + `--approve-tool-change. Then review the same tools again with firedrill tools session ${setup.id} replace --approve-tool-change. Nothing was sent.`, 2);
441
+ }
442
+ const requestKey = (options.request ?? randomUUID()).toLowerCase();
443
+ const again = `firedrill tools session ${setup.id} replace --request-key ${requestKey} --approve-tool-change`;
444
+ let approved = options.approve === true;
445
+ if (!approved && tty(options)) {
446
+ process.stderr.write(setupText(setup, items) + '\n');
447
+ approved = await selectOne('Review these tools again as a new session?', [
448
+ { value: false, label: 'Not now', detail: 'Nothing is sent.' },
449
+ { value: true, label: 'Review again', detail: 'A new review with the same tools, seeds and duration. It reserves and starts nothing; its estimate needs approval next.' },
450
+ ], signal);
451
+ }
452
+ if (!approved) {
453
+ throw new CliError('tool_change_consent_required', `Nothing was sent. After the human approves: ${options.request === undefined
454
+ ? `firedrill tools session ${setup.id} replace --approve-tool-change` : again}`, 2);
455
+ }
456
+ emit('tool_replacement_intent', { previous_setup_id: setup.id, request_key: requestKey }, `Replacement request: ${requestKey}\nIf interrupted, continue with ${again}. Do not start again under a new key.`);
457
+ let replacement;
458
+ try {
459
+ replacement = await retryTransient(() => replaceSetup(scope, { setupId: setup.id, requestKey, signal }), { signal, attempts: 3 });
460
+ }
461
+ catch (error) {
462
+ // Only an unanswered request may have landed; a refusal is reported as it is.
463
+ if (signal.aborted || !isTransient(error))
464
+ throw error;
465
+ // A generic lookup by request key does not expose replacement provenance.
466
+ // Only this same-source/key POST proves the exact relationship again.
467
+ throw new CliError('tool_replacement_unconfirmed', `The replacement review was not confirmed. Continue with ${again}; do not submit a different key.`);
468
+ }
469
+ const after = await setupItems(scope, { setup: replacement, signal }).catch(() => []);
470
+ const next = replacement.approved_at !== null || replacement.stop_requested_at !== null
471
+ ? `This replacement already has a later action. Inspect it with firedrill tools session ${replacement.id}. No new start was submitted.`
472
+ : `This review has not been approved. Show the human its estimate and continue with firedrill tools start --request-key ${requestKey}.`;
473
+ emit('tool_session_replacement', { previous_setup_id: setup.id, ...replacement, items: after }, `${setupText(replacement, after)}\nReview of the same tools confirmed. ${next}`);
474
+ }
475
+ /**
476
+ * `tools session <session-id> [stop|replace] [--approve-tool-change]`, and its renewal commands
477
+ * (`renew`, `renewals`, `renewal`, `cancel-renewal`, `renewal-request`, `withdraw-renewal`).
478
+ */
479
+ async function session(args, options, emit) {
480
+ const [, sessionId, verb, value] = args;
481
+ const renewing = verb !== undefined && RENEWAL_VERBS.has(verb);
482
+ if (!sessionId || !UUID.test(sessionId) || args.length > (renewing ? 4 : 3)
483
+ || (verb !== undefined && verb !== 'stop' && verb !== 'replace' && !renewing)) {
484
+ throw new CliError('arguments_invalid', 'Use tools session <session-id>, tools session <session-id> stop|replace, or tools session <session-id> renew --seconds <n> '
485
+ + '(also renewals, renewal <renewal-id>, cancel-renewal <renewal-id>, renewal-request <key>, withdraw-renewal <key>).', 2);
486
+ }
487
+ const renewalOptions = { json: options.json, signal: options.signal, seconds: options.seconds, request: options.request,
488
+ quote: options.quote, approve: options.approve, approveCredits: options.approveCredits, page: options.page };
489
+ if (renewing)
490
+ checkRenewalArguments(verb, value, renewalOptions);
491
+ else if (verb === 'replace') {
492
+ if ([options.seconds, options.quote, options.page, options.approveCredits].some((one) => one !== undefined)
493
+ || (options.request !== undefined && !UUID.test(options.request))) {
494
+ throw new CliError('arguments_invalid', 'Use tools session <session-id> replace [--request-key <key>] [--approve-tool-change].', 2);
495
+ }
496
+ }
497
+ else if ([options.seconds, options.request, options.quote, options.page, options.approveCredits].some((one) => one !== undefined)) {
498
+ throw new CliError('arguments_invalid', 'Use --seconds, --request-key, --usage-quote and --page only with the renewal and replace commands of tools session.', 2);
499
+ }
500
+ const scope = await scopeOf(options);
501
+ const found = await sessionById(scope, sessionId.toLowerCase(), options.signal);
502
+ if (renewing)
503
+ return toolRenewalCommand(scope, await renewalTarget(scope, found, options.signal), verb, value, renewalOptions, emit);
504
+ if (verb === 'replace')
505
+ return replaceReview(scope, found, options, emit);
506
+ if ('batch' in found) {
507
+ const batch = found.batch;
508
+ if (verb === undefined) {
509
+ emit('tool_session', batch, batchText(batch));
510
+ return;
511
+ }
512
+ if (STANDALONE_TERMINAL.has(batch.state)) {
513
+ emit('tool_session', batch, `${batchText(batch)}\nThis session already ended; nothing was sent.`);
514
+ return;
515
+ }
516
+ await stopConsent(batchText(batch), batch.id, options);
517
+ // Stopping is idempotent, so an unanswered request is simply repeated.
518
+ const stopped = await retryTransient(() => stopStandaloneBatch(scope, { batchId: batch.id, signal: options.signal }), { signal: options.signal, attempts: 3 });
519
+ // This summary has no approval field. The pre-stop state can be stale if
520
+ // another client admitted the original meanwhile, so do not infer funding.
521
+ emit('tool_session', stopped, `${batchText(stopped)}\nStop recorded. Any admitted resources still need cleanup and usage settlement; `
522
+ + `this does not prove storage is gone. Check with firedrill tools session ${stopped.id}.`);
523
+ return;
524
+ }
525
+ const { setup, items } = found;
526
+ if (verb === undefined) {
527
+ emit('tool_session', { ...setup, items }, setupText(setup, items, scope.owner));
528
+ return;
529
+ }
530
+ if (SETUP_ENDED.has(setup.state)) {
531
+ emit('tool_session', { ...setup, items }, `${setupText(setup, items, scope.owner)}\nThis session already ended; nothing was sent.`);
532
+ return;
533
+ }
534
+ // Root stops a mixed session for the member who started it only (others get a generic refusal), so ask nobody else to approve.
535
+ if (setup.actor_id !== scope.owner.toLowerCase()) {
536
+ throw new CliError('native_tool_setup_owner_only', `Tool session ${setup.id} was started by another member, and only they can stop it. It ends on its own at its scheduled time. Nothing was sent.`, 2);
537
+ }
538
+ await stopConsent(setupText(setup, items), setup.id, options);
539
+ const stopped = await retryTransient(() => stopSetup(scope, { setupId: setup.id, signal: options.signal }), { signal: options.signal, attempts: 3 });
540
+ const after = await setupItems(scope, { setup: stopped, signal: options.signal }).catch(() => items);
541
+ // A setup stopped before its approval only cancels its reviewed tools: nothing was admitted or reserved.
542
+ emit('tool_session', { ...stopped, items: after }, stopped.approved_at === null
543
+ ? `${setupText(stopped, after)}\nThis unapproved review is closed. Nothing was reserved or started. Review the same tools again with `
544
+ + `firedrill tools session ${stopped.id} replace --approve-tool-change, or choose new ones with firedrill tools start.`
545
+ : `${setupText(stopped, after)}\nStop requested. The service retires every tool and settles its usage; `
546
+ + `this does not prove storage is gone. Check with firedrill tools session ${stopped.id}.`);
547
+ }
548
+ function servedText(tool) {
549
+ const head = ` ${terminalText(tool.name)} (${terminalText(tool.service_key)}, ${LANE_TEXT[tool.lane]})\n MCP (Streamable HTTP): ${tool.mcp}`;
550
+ if (tool.lane === 'pooled')
551
+ return `${head}\n Operations (POST JSON {operation, arguments, idempotency_key?}): ${tool.invoke}`;
552
+ return `${head}\n Operations: POST ${tool.operations}/<service_key>/<operation_name> with {"arguments": {...}} and an Idempotency-Key UUID;`
553
+ + ` read one back with GET ${tool.requests}/<key>`;
554
+ }
555
+ /**
556
+ * `tools access <session-id> [--allow-writes] [--approve-tool-change]`: serve
557
+ * every running tool of the session on loopback URLs until Ctrl-C or until the
558
+ * session ends. Read-only operations unless --allow-writes (which still needs
559
+ * write access to the workspace). Each isolated tool needs a scoped connection,
560
+ * created with the human's consent and revoked when access ends.
561
+ */
562
+ async function access(args, options, emit) {
563
+ const [, sessionId] = args;
564
+ if (!sessionId || !UUID.test(sessionId) || args.length !== 2)
565
+ throw new CliError('arguments_invalid', 'Use tools access <session-id> [--allow-writes].', 2);
566
+ const scope = await scopeOf(options);
567
+ const found = await sessionById(scope, sessionId.toLowerCase(), options.signal);
568
+ if ('batch' in found)
569
+ return accessBatch(scope, found.batch, options, emit);
570
+ let { setup, items } = found;
571
+ const now = items.filter(running);
572
+ if (!now.length) {
573
+ throw new CliError('native_tool_access_unavailable', `No tool in session ${setup.id} is running (${stateText(setup.state)}), so there is nothing to connect to.`
574
+ + (SETUP_ENDED.has(setup.state) || setup.stop_requested_at ? '' : ` Follow it with firedrill tools session ${setup.id}.`), 2);
575
+ }
576
+ const mode = options.allowWrites ? 'interactive' : 'read';
577
+ const isolated = now.filter((one) => one.lane === 'isolated');
578
+ if (isolated.length) {
579
+ let approved = options.approve === true;
580
+ if (!approved && tty(options)) {
581
+ approved = await selectOne(`Connect ${isolated.length} isolated tool${isolated.length === 1 ? '' : 's'} to this machine?`, [
582
+ { value: false, label: 'Cancel', detail: 'Nothing is connected.' },
583
+ { value: true, label: 'Connect', detail: 'Creates one scoped connection per isolated tool, kept in workspace history and revoked when access ends. Its synthetic tool data comes to this machine.' },
584
+ ], options.signal);
585
+ }
586
+ if (!approved) {
587
+ throw new CliError('tool_change_consent_required', 'Each isolated tool is connected through its own scoped connection, kept in workspace history and revoked when '
588
+ + 'access ends, and its synthetic tool data comes to this machine. Nothing was connected. After the human approves: '
589
+ + `firedrill tools access ${setup.id}${options.allowWrites ? ' --allow-writes' : ''} --approve-tool-change`, 2);
590
+ }
591
+ }
592
+ // The shared pool runs every pooled child of a setup as one batch; its active instances are served with short-lived credentials.
593
+ const pooled = now.filter((one) => one.lane === 'pooled');
594
+ let instances = [];
595
+ if (pooled.length) {
596
+ const batch = await getStandaloneBatch(scope, { batchId: pooled[0].pooled_batch_id, signal: options.signal });
597
+ const ids = new Set(pooled.map((one) => one.instance_id));
598
+ instances = batch.instances.filter((one) => ids.has(one.id) && one.state === STANDALONE_ACTIVE);
599
+ }
600
+ const stop = new AbortController();
601
+ const signal = AbortSignal.any([options.signal, stop.signal]);
602
+ const grants = [];
603
+ let proxy = null;
604
+ let fatal = null;
605
+ try {
606
+ for (const one of isolated) {
607
+ // The original key is reported before anything is sent, so a connection created during a lost answer or Ctrl-C can be found.
608
+ const requestKey = randomUUID();
609
+ emit('tool_connection_prepared', { session_id: setup.id, instance_id: one.instance_id, request_key: requestKey }, `Connecting ${terminalText(one.name)} (connection request ${requestKey}).`);
610
+ try {
611
+ grants.push(await openIsolatedGrant(scope, { instanceId: one.instance_id, serviceKey: one.service_key, operations: one.operations, mode, requestKey, signal }));
612
+ }
613
+ catch (error) {
614
+ // One isolated tool that cannot be connected does not stop the others; the reason is reported, nothing is reissued.
615
+ // An unconfirmed connection is reported even after Ctrl-C: it names the original and whether it was revoked.
616
+ const code = errorCode(error) ?? 'native_tool_access_unavailable';
617
+ if (!signal.aborted || code === 'tool_connection_unconfirmed') {
618
+ emit('tool_access_not_served', { session_id: setup.id, instance_id: one.instance_id, request_key: requestKey, code }, `${terminalText(one.name)} is not served here: ${error instanceof Error ? terminalText(error.message) : 'its connection failed'}`);
619
+ }
620
+ if (signal.aborted)
621
+ throw error;
622
+ }
623
+ }
624
+ if (!instances.length && !grants.length) {
625
+ throw new CliError('native_tool_access_unavailable', 'None of the running tools could be connected here, for the reasons above. Nothing is being served.', 2);
626
+ }
627
+ const started = await StandaloneProxy.start({ scope, instances, isolated: grants, mode, signal,
628
+ onEnded: (instanceId, code) => {
629
+ emit('tool_access_ended', { session_id: setup.id, instance_id: instanceId, code }, code === STANDALONE_ACCESS_ERRORS.connectionEnded
630
+ ? `Tool ${instanceId} is no longer served here: its connection expired or was revoked. It is not renewed; run tools access ${setup.id} again for a new one.`
631
+ : `Tool ${instanceId} is no longer reachable here (${terminalText(code)}).`);
632
+ if (proxy && !proxy.serving.length)
633
+ stop.abort();
634
+ },
635
+ onFatal: (error) => { fatal = error; stop.abort(); } });
636
+ proxy = started;
637
+ const tools = items.filter((one) => one.instance_id !== null && started.serving.includes(one.instance_id)).map((one) => {
638
+ const grant = grants.find((row) => row.instance_id === one.instance_id);
639
+ return { instance_id: one.instance_id, name: one.name, service_key: one.service_key, ...started.urls(one.instance_id),
640
+ ...(grant ? { connection_id: grant.grant_id, connection_expires_at: grant.expires_at } : {}) };
641
+ });
642
+ emit('tool_access_ready', { session_id: setup.id, mode, tools }, [
643
+ `Serving ${tools.length} tool${tools.length === 1 ? '' : 's'} of session ${setup.id} on this machine (${mode === 'read' ? 'read-only operations' : 'reads and writes'}):`,
644
+ ...tools.map(servedText),
645
+ 'Point your agent\'s MCP client at an MCP URL. These URLs work only on this machine and only while this command runs; keep them out of chat, files and reports.',
646
+ 'Leave this running. Ctrl-C ends access and revokes its isolated connections; the tools keep running until their session ends or is stopped.',
647
+ ].join('\n'));
648
+ // Ends with Ctrl-C, when root says the session or every served tool stopped, or when access is refused for good.
649
+ while (!signal.aborted) {
650
+ if (!await sleep(jitteredInterval(ACCESS_POLL_MS), signal))
651
+ break;
652
+ try {
653
+ const current = await getSetup(scope, { setupId: setup.id, signal });
654
+ items = await setupItems(scope, { setup: current, signal });
655
+ setup = current;
656
+ }
657
+ catch (error) {
658
+ if (signal.aborted)
659
+ break;
660
+ if (isTransient(error))
661
+ continue;
662
+ throw error;
663
+ }
664
+ if (SETUP_ENDED.has(setup.state) || setup.stop_requested_at)
665
+ break;
666
+ for (const one of items)
667
+ if (one.instance_id && started.serving.includes(one.instance_id) && !running(one))
668
+ started.end(one.instance_id, STANDALONE_ACCESS_ERRORS.ended);
669
+ if (!started.serving.length)
670
+ break;
671
+ }
672
+ }
673
+ finally {
674
+ await proxy?.close();
675
+ // Ctrl-C aborted the command's own signal, so revocation gets its own bounded time.
676
+ const revoked = await Promise.all(grants.map((grant) => revokeGrant(scope, grant.instance_id, grant.grant_id, AbortSignal.timeout(REVOKE_MS))));
677
+ grants.forEach((grant, index) => {
678
+ if (!revoked[index]) {
679
+ emit('tool_access_revoke_unconfirmed', { session_id: setup.id, instance_id: grant.instance_id, connection_id: grant.grant_id }, `Revoking connection ${grant.grant_id} was not confirmed; it ends with the tool. Revoke it now: firedrill tools disconnect ${grant.instance_id} ${grant.grant_id} --approve-tool-change`);
680
+ }
681
+ });
682
+ }
683
+ if (fatal)
684
+ throw fatal;
685
+ emit('tool_access_closed', { session_id: setup.id, state: setup.state }, SETUP_ENDED.has(setup.state) || setup.stop_requested_at ? `The tool session is ${stateText(setup.state)}; access ended.`
686
+ : 'Access ended on this machine. The tools keep running until their session ends.');
687
+ }
688
+ /** `tools access` for a session started before mixed setups: its pooled instances only. */
689
+ async function accessBatch(scope, initial, options, emit) {
690
+ let batch = initial;
691
+ if (batch.state !== STANDALONE_ACTIVE) {
692
+ throw new CliError('native_tool_access_unavailable', `This tool session is ${stateText(batch.state)}, so there is nothing to connect to yet.`
693
+ + (STANDALONE_TERMINAL.has(batch.state) ? '' : ` Follow it with firedrill tools session ${batch.id}.`), 2);
694
+ }
695
+ const mode = options.allowWrites ? 'interactive' : 'read';
696
+ const stop = new AbortController();
697
+ const signal = AbortSignal.any([options.signal, stop.signal]);
698
+ let fatal = null;
699
+ const proxy = await StandaloneProxy.start({ scope, instances: batch.instances, mode, signal,
700
+ onEnded: (instanceId, code) => {
701
+ emit('tool_access_ended', { session_id: batch.id, instance_id: instanceId, code }, `Tool ${instanceId} is no longer reachable here (${terminalText(code)}).`);
702
+ if (!proxy.serving.length)
703
+ stop.abort();
704
+ },
705
+ onFatal: (error) => { fatal = error; stop.abort(); } });
706
+ const tools = batch.instances.filter((one) => proxy.serving.includes(one.id))
707
+ .map((one) => ({ instance_id: one.id, name: `${one.name} ${one.version}`, service_key: one.service_key, ...proxy.urls(one.id) }));
708
+ emit('tool_access_ready', { session_id: batch.id, mode, tools }, [
709
+ `Serving ${tools.length} tool${tools.length === 1 ? '' : 's'} of session ${batch.id} on this machine (${mode === 'read' ? 'read-only operations' : 'reads and writes'}):`,
710
+ ...tools.map(servedText),
711
+ 'Point your agent\'s MCP client at an MCP URL. These URLs work only on this machine and only while this command runs; keep them out of chat, files and reports.',
712
+ 'Leave this running. Ctrl-C ends access; the tools keep running until their session ends or is stopped.',
713
+ ].join('\n'));
714
+ try {
715
+ while (!signal.aborted) {
716
+ if (!await sleep(jitteredInterval(ACCESS_POLL_MS), signal))
717
+ break;
718
+ try {
719
+ batch = await getStandaloneBatch(scope, { batchId: batch.id, signal });
720
+ }
721
+ catch (error) {
722
+ if (signal.aborted)
723
+ break;
724
+ if (isTransient(error))
725
+ continue;
726
+ throw error;
727
+ }
728
+ if (batch.state !== STANDALONE_ACTIVE)
729
+ break;
730
+ for (const one of batch.instances)
731
+ if (one.state !== STANDALONE_ACTIVE)
732
+ proxy.end(one.id, STANDALONE_ACCESS_ERRORS.ended);
733
+ }
734
+ }
735
+ finally {
736
+ await proxy.close();
737
+ }
738
+ if (fatal)
739
+ throw fatal;
740
+ emit('tool_access_closed', { session_id: batch.id, state: batch.state }, batch.state === STANDALONE_ACTIVE ? 'Access ended on this machine. The tools keep running until their session ends.'
741
+ : `The tool session is ${stateText(batch.state)}; access ended.`);
742
+ }
743
+ export async function nativeToolsCommand(args, options, emit) {
744
+ const action = args[0];
745
+ const allowed = {
746
+ runnable: ['search', 'page'], start: ['file', 'seconds', 'service', 'request', 'quote', 'approve', 'approveCredits', 'follow', 'search'],
747
+ sessions: ['state', 'search', 'page'], session: ['approve', 'approveCredits', 'seconds', 'request', 'quote', 'page'], access: ['allowWrites', 'approve'],
748
+ state: ['file', 'request', 'approve', 'approveCredits', 'page', 'seconds', 'quote'],
749
+ };
750
+ const flags = { file: '--file', seconds: '--seconds', service: '--service',
751
+ request: '--request-key', quote: '--usage-quote', approve: '--approve-tool-change', approveCredits: '--approve-credits', follow: '--follow', state: '--state', search: '--search', page: '--page', allowWrites: '--allow-writes' };
752
+ const extra = Object.keys(flags)
753
+ .filter((flag) => options[flag] !== undefined && options[flag] !== false && !allowed[action]?.includes(flag));
754
+ if (extra.length)
755
+ throw new CliError('arguments_invalid', `tools ${action} does not take ${extra.map((flag) => flags[flag]).join(', ')}.`, 2);
756
+ if (action === 'runnable') {
757
+ if (args.length !== 1)
758
+ throw new CliError('arguments_invalid', 'Use tools runnable [--search <text>] [--page <n>].', 2);
759
+ return runnable(options, emit);
760
+ }
761
+ if (action === 'start')
762
+ return start(args, options, emit);
763
+ if (action === 'sessions') {
764
+ if (args.length !== 1)
765
+ throw new CliError('arguments_invalid', 'Use tools sessions [--state all|active|preparing|closed] [--search <text>] [--page <n>].', 2);
766
+ return sessions(options, emit);
767
+ }
768
+ if (action === 'session')
769
+ return session(args, options, emit);
770
+ if (action === 'access')
771
+ return access(args, options, emit);
772
+ if (action === 'state') {
773
+ if (args.length < 2)
774
+ throw new CliError('arguments_invalid', 'Use tools state <instance-id> (instance ids from firedrill tools session <session-id>).', 2);
775
+ return toolStateCommand(await scopeOf(options), args.slice(1), { json: options.json, signal: options.signal, file: options.file,
776
+ request: options.request, approve: options.approve, approveCredits: options.approveCredits, page: options.page, seconds: options.seconds, quote: options.quote }, emit);
777
+ }
778
+ throw new CliError('arguments_invalid', 'Use tools runnable, tools start, tools sessions, tools session <session-id> [stop | renew], tools access <session-id> '
779
+ + 'or tools state <instance-id>.', 2);
780
+ }