@cuna_labs/cli 0.1.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 (624) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +9 -0
  3. package/README.md +169 -0
  4. package/THIRD_PARTY_NOTICES.md +29 -0
  5. package/dist/api/capability-evidence.d.ts +19 -0
  6. package/dist/api/capability-evidence.d.ts.map +1 -0
  7. package/dist/api/capability-evidence.js +38 -0
  8. package/dist/api/capability-evidence.js.map +1 -0
  9. package/dist/api/client.d.ts +167 -0
  10. package/dist/api/client.d.ts.map +1 -0
  11. package/dist/api/client.js +937 -0
  12. package/dist/api/client.js.map +1 -0
  13. package/dist/api/contracts.d.ts +345 -0
  14. package/dist/api/contracts.d.ts.map +1 -0
  15. package/dist/api/contracts.js +976 -0
  16. package/dist/api/contracts.js.map +1 -0
  17. package/dist/api/execution-workspaces.d.ts +17 -0
  18. package/dist/api/execution-workspaces.d.ts.map +1 -0
  19. package/dist/api/execution-workspaces.js +46 -0
  20. package/dist/api/execution-workspaces.js.map +1 -0
  21. package/dist/api/http.d.ts +76 -0
  22. package/dist/api/http.d.ts.map +1 -0
  23. package/dist/api/http.js +838 -0
  24. package/dist/api/http.js.map +1 -0
  25. package/dist/api/managed-executions.d.ts +37 -0
  26. package/dist/api/managed-executions.d.ts.map +1 -0
  27. package/dist/api/managed-executions.js +80 -0
  28. package/dist/api/managed-executions.js.map +1 -0
  29. package/dist/api/observer-v2-schema.d.ts +206 -0
  30. package/dist/api/observer-v2-schema.d.ts.map +1 -0
  31. package/dist/api/observer-v2-schema.js +264 -0
  32. package/dist/api/observer-v2-schema.js.map +1 -0
  33. package/dist/api/observer-v2.d.ts +33 -0
  34. package/dist/api/observer-v2.d.ts.map +1 -0
  35. package/dist/api/observer-v2.js +60 -0
  36. package/dist/api/observer-v2.js.map +1 -0
  37. package/dist/api/owner-observe-grants-v2-schema.d.ts +678 -0
  38. package/dist/api/owner-observe-grants-v2-schema.d.ts.map +1 -0
  39. package/dist/api/owner-observe-grants-v2-schema.js +887 -0
  40. package/dist/api/owner-observe-grants-v2-schema.js.map +1 -0
  41. package/dist/api/owner-observe-grants-v2.d.ts +268 -0
  42. package/dist/api/owner-observe-grants-v2.d.ts.map +1 -0
  43. package/dist/api/owner-observe-grants-v2.js +604 -0
  44. package/dist/api/owner-observe-grants-v2.js.map +1 -0
  45. package/dist/api/provider-v2-schema.d.ts +293 -0
  46. package/dist/api/provider-v2-schema.d.ts.map +1 -0
  47. package/dist/api/provider-v2-schema.js +381 -0
  48. package/dist/api/provider-v2-schema.js.map +1 -0
  49. package/dist/api/provider-v2.d.ts +55 -0
  50. package/dist/api/provider-v2.d.ts.map +1 -0
  51. package/dist/api/provider-v2.js +54 -0
  52. package/dist/api/provider-v2.js.map +1 -0
  53. package/dist/api/remote-workspace.d.ts +29 -0
  54. package/dist/api/remote-workspace.d.ts.map +1 -0
  55. package/dist/api/remote-workspace.js +62 -0
  56. package/dist/api/remote-workspace.js.map +1 -0
  57. package/dist/api/supervisor-live-update.d.ts +248 -0
  58. package/dist/api/supervisor-live-update.d.ts.map +1 -0
  59. package/dist/api/supervisor-live-update.js +434 -0
  60. package/dist/api/supervisor-live-update.js.map +1 -0
  61. package/dist/api/terminal-reason.d.ts +5 -0
  62. package/dist/api/terminal-reason.d.ts.map +1 -0
  63. package/dist/api/terminal-reason.js +12 -0
  64. package/dist/api/terminal-reason.js.map +1 -0
  65. package/dist/auth/browser-handoff.d.ts +59 -0
  66. package/dist/auth/browser-handoff.d.ts.map +1 -0
  67. package/dist/auth/browser-handoff.js +33 -0
  68. package/dist/auth/browser-handoff.js.map +1 -0
  69. package/dist/auth/browser.d.ts +11 -0
  70. package/dist/auth/browser.d.ts.map +1 -0
  71. package/dist/auth/browser.js +47 -0
  72. package/dist/auth/browser.js.map +1 -0
  73. package/dist/auth/human-client.d.ts +39 -0
  74. package/dist/auth/human-client.d.ts.map +1 -0
  75. package/dist/auth/human-client.js +66 -0
  76. package/dist/auth/human-client.js.map +1 -0
  77. package/dist/auth/human-contracts.d.ts +52 -0
  78. package/dist/auth/human-contracts.d.ts.map +1 -0
  79. package/dist/auth/human-contracts.js +229 -0
  80. package/dist/auth/human-contracts.js.map +1 -0
  81. package/dist/auth/human-session.d.ts +48 -0
  82. package/dist/auth/human-session.d.ts.map +1 -0
  83. package/dist/auth/human-session.js +902 -0
  84. package/dist/auth/human-session.js.map +1 -0
  85. package/dist/auth/onboarding-state.d.ts +31 -0
  86. package/dist/auth/onboarding-state.d.ts.map +1 -0
  87. package/dist/auth/onboarding-state.js +52 -0
  88. package/dist/auth/onboarding-state.js.map +1 -0
  89. package/dist/auth/pkce.d.ts +15 -0
  90. package/dist/auth/pkce.d.ts.map +1 -0
  91. package/dist/auth/pkce.js +19 -0
  92. package/dist/auth/pkce.js.map +1 -0
  93. package/dist/bin/cuna.d.ts +3 -0
  94. package/dist/bin/cuna.d.ts.map +1 -0
  95. package/dist/bin/cuna.js +4 -0
  96. package/dist/bin/cuna.js.map +1 -0
  97. package/dist/build-identity.d.ts +20 -0
  98. package/dist/build-identity.d.ts.map +1 -0
  99. package/dist/build-identity.js +77 -0
  100. package/dist/build-identity.js.map +1 -0
  101. package/dist/cli/command-help.d.ts +9 -0
  102. package/dist/cli/command-help.d.ts.map +1 -0
  103. package/dist/cli/command-help.js +324 -0
  104. package/dist/cli/command-help.js.map +1 -0
  105. package/dist/cli/help.d.ts +40 -0
  106. package/dist/cli/help.d.ts.map +1 -0
  107. package/dist/cli/help.js +256 -0
  108. package/dist/cli/help.js.map +1 -0
  109. package/dist/cli/output.d.ts +28 -0
  110. package/dist/cli/output.d.ts.map +1 -0
  111. package/dist/cli/output.js +95 -0
  112. package/dist/cli/output.js.map +1 -0
  113. package/dist/cli/parser.d.ts +65 -0
  114. package/dist/cli/parser.d.ts.map +1 -0
  115. package/dist/cli/parser.js +294 -0
  116. package/dist/cli/parser.js.map +1 -0
  117. package/dist/cli/process-entrypoint.d.ts +18 -0
  118. package/dist/cli/process-entrypoint.d.ts.map +1 -0
  119. package/dist/cli/process-entrypoint.js +26 -0
  120. package/dist/cli/process-entrypoint.js.map +1 -0
  121. package/dist/cli/run.d.ts +114 -0
  122. package/dist/cli/run.d.ts.map +1 -0
  123. package/dist/cli/run.js +1969 -0
  124. package/dist/cli/run.js.map +1 -0
  125. package/dist/commands/commands.d.ts +74 -0
  126. package/dist/commands/commands.d.ts.map +1 -0
  127. package/dist/commands/commands.js +2814 -0
  128. package/dist/commands/commands.js.map +1 -0
  129. package/dist/config/config.d.ts +86 -0
  130. package/dist/config/config.d.ts.map +1 -0
  131. package/dist/config/config.js +323 -0
  132. package/dist/config/config.js.map +1 -0
  133. package/dist/config/infra-contract-witness.d.ts +29 -0
  134. package/dist/config/infra-contract-witness.d.ts.map +1 -0
  135. package/dist/config/infra-contract-witness.js +30 -0
  136. package/dist/config/infra-contract-witness.js.map +1 -0
  137. package/dist/core/browser-url.d.ts +2 -0
  138. package/dist/core/browser-url.d.ts.map +1 -0
  139. package/dist/core/browser-url.js +20 -0
  140. package/dist/core/browser-url.js.map +1 -0
  141. package/dist/core/deployed-wire-compatibility.d.ts +15 -0
  142. package/dist/core/deployed-wire-compatibility.d.ts.map +1 -0
  143. package/dist/core/deployed-wire-compatibility.js +15 -0
  144. package/dist/core/deployed-wire-compatibility.js.map +1 -0
  145. package/dist/core/errors.d.ts +59 -0
  146. package/dist/core/errors.d.ts.map +1 -0
  147. package/dist/core/errors.js +82 -0
  148. package/dist/core/errors.js.map +1 -0
  149. package/dist/core/exit-codes.d.ts +68 -0
  150. package/dist/core/exit-codes.d.ts.map +1 -0
  151. package/dist/core/exit-codes.js +84 -0
  152. package/dist/core/exit-codes.js.map +1 -0
  153. package/dist/core/instant.d.ts +60 -0
  154. package/dist/core/instant.d.ts.map +1 -0
  155. package/dist/core/instant.js +90 -0
  156. package/dist/core/instant.js.map +1 -0
  157. package/dist/core/namespace.d.ts +171 -0
  158. package/dist/core/namespace.d.ts.map +1 -0
  159. package/dist/core/namespace.js +227 -0
  160. package/dist/core/namespace.js.map +1 -0
  161. package/dist/core/observation-budget.d.ts +185 -0
  162. package/dist/core/observation-budget.d.ts.map +1 -0
  163. package/dist/core/observation-budget.js +209 -0
  164. package/dist/core/observation-budget.js.map +1 -0
  165. package/dist/core/product-web.d.ts +81 -0
  166. package/dist/core/product-web.d.ts.map +1 -0
  167. package/dist/core/product-web.js +87 -0
  168. package/dist/core/product-web.js.map +1 -0
  169. package/dist/core/validation.d.ts +90 -0
  170. package/dist/core/validation.d.ts.map +1 -0
  171. package/dist/core/validation.js +199 -0
  172. package/dist/core/validation.js.map +1 -0
  173. package/dist/credentials/contracts.d.ts +82 -0
  174. package/dist/credentials/contracts.d.ts.map +1 -0
  175. package/dist/credentials/contracts.js +2 -0
  176. package/dist/credentials/contracts.js.map +1 -0
  177. package/dist/credentials/errors.d.ts +19 -0
  178. package/dist/credentials/errors.d.ts.map +1 -0
  179. package/dist/credentials/errors.js +16 -0
  180. package/dist/credentials/errors.js.map +1 -0
  181. package/dist/credentials/index.d.ts +6 -0
  182. package/dist/credentials/index.d.ts.map +1 -0
  183. package/dist/credentials/index.js +6 -0
  184. package/dist/credentials/index.js.map +1 -0
  185. package/dist/credentials/local-session.d.ts +101 -0
  186. package/dist/credentials/local-session.d.ts.map +1 -0
  187. package/dist/credentials/local-session.js +839 -0
  188. package/dist/credentials/local-session.js.map +1 -0
  189. package/dist/credentials/secret-material.d.ts +17 -0
  190. package/dist/credentials/secret-material.d.ts.map +1 -0
  191. package/dist/credentials/secret-material.js +49 -0
  192. package/dist/credentials/secret-material.js.map +1 -0
  193. package/dist/credentials/vault.d.ts +41 -0
  194. package/dist/credentials/vault.d.ts.map +1 -0
  195. package/dist/credentials/vault.js +719 -0
  196. package/dist/credentials/vault.js.map +1 -0
  197. package/dist/daemon/endpoint.d.ts +29 -0
  198. package/dist/daemon/endpoint.d.ts.map +1 -0
  199. package/dist/daemon/endpoint.js +66 -0
  200. package/dist/daemon/endpoint.js.map +1 -0
  201. package/dist/daemon/journal.d.ts +62 -0
  202. package/dist/daemon/journal.d.ts.map +1 -0
  203. package/dist/daemon/journal.js +299 -0
  204. package/dist/daemon/journal.js.map +1 -0
  205. package/dist/daemon/lease.d.ts +25 -0
  206. package/dist/daemon/lease.d.ts.map +1 -0
  207. package/dist/daemon/lease.js +94 -0
  208. package/dist/daemon/lease.js.map +1 -0
  209. package/dist/daemon/lifecycle.d.ts +14 -0
  210. package/dist/daemon/lifecycle.d.ts.map +1 -0
  211. package/dist/daemon/lifecycle.js +29 -0
  212. package/dist/daemon/lifecycle.js.map +1 -0
  213. package/dist/daemon/protocol.d.ts +65 -0
  214. package/dist/daemon/protocol.d.ts.map +1 -0
  215. package/dist/daemon/protocol.js +194 -0
  216. package/dist/daemon/protocol.js.map +1 -0
  217. package/dist/daemon/supervisor.d.ts +29 -0
  218. package/dist/daemon/supervisor.d.ts.map +1 -0
  219. package/dist/daemon/supervisor.js +87 -0
  220. package/dist/daemon/supervisor.js.map +1 -0
  221. package/dist/daemon/views.d.ts +31 -0
  222. package/dist/daemon/views.d.ts.map +1 -0
  223. package/dist/daemon/views.js +86 -0
  224. package/dist/daemon/views.js.map +1 -0
  225. package/dist/index.d.ts +38 -0
  226. package/dist/index.d.ts.map +1 -0
  227. package/dist/index.js +38 -0
  228. package/dist/index.js.map +1 -0
  229. package/dist/journey/api-effects.d.ts +59 -0
  230. package/dist/journey/api-effects.d.ts.map +1 -0
  231. package/dist/journey/api-effects.js +386 -0
  232. package/dist/journey/api-effects.js.map +1 -0
  233. package/dist/journey/derived-identity.d.ts +91 -0
  234. package/dist/journey/derived-identity.d.ts.map +1 -0
  235. package/dist/journey/derived-identity.js +123 -0
  236. package/dist/journey/derived-identity.js.map +1 -0
  237. package/dist/journey/index.d.ts +6 -0
  238. package/dist/journey/index.d.ts.map +1 -0
  239. package/dist/journey/index.js +6 -0
  240. package/dist/journey/index.js.map +1 -0
  241. package/dist/journey/intent.d.ts +36 -0
  242. package/dist/journey/intent.d.ts.map +1 -0
  243. package/dist/journey/intent.js +191 -0
  244. package/dist/journey/intent.js.map +1 -0
  245. package/dist/journey/orchestrator.d.ts +140 -0
  246. package/dist/journey/orchestrator.d.ts.map +1 -0
  247. package/dist/journey/orchestrator.js +415 -0
  248. package/dist/journey/orchestrator.js.map +1 -0
  249. package/dist/journey/provider-launch-intent.d.ts +12 -0
  250. package/dist/journey/provider-launch-intent.d.ts.map +1 -0
  251. package/dist/journey/provider-launch-intent.js +40 -0
  252. package/dist/journey/provider-launch-intent.js.map +1 -0
  253. package/dist/journey/remote-workspace.d.ts +34 -0
  254. package/dist/journey/remote-workspace.d.ts.map +1 -0
  255. package/dist/journey/remote-workspace.js +91 -0
  256. package/dist/journey/remote-workspace.js.map +1 -0
  257. package/dist/journey/root-entry.d.ts +56 -0
  258. package/dist/journey/root-entry.d.ts.map +1 -0
  259. package/dist/journey/root-entry.js +19 -0
  260. package/dist/journey/root-entry.js.map +1 -0
  261. package/dist/journey/selection.d.ts +165 -0
  262. package/dist/journey/selection.d.ts.map +1 -0
  263. package/dist/journey/selection.js +565 -0
  264. package/dist/journey/selection.js.map +1 -0
  265. package/dist/journey/session-failure.d.ts +4 -0
  266. package/dist/journey/session-failure.d.ts.map +1 -0
  267. package/dist/journey/session-failure.js +28 -0
  268. package/dist/journey/session-failure.js.map +1 -0
  269. package/dist/journey/workspace-effects.d.ts +30 -0
  270. package/dist/journey/workspace-effects.d.ts.map +1 -0
  271. package/dist/journey/workspace-effects.js +348 -0
  272. package/dist/journey/workspace-effects.js.map +1 -0
  273. package/dist/local-actions/broker.d.ts +32 -0
  274. package/dist/local-actions/broker.d.ts.map +1 -0
  275. package/dist/local-actions/broker.js +400 -0
  276. package/dist/local-actions/broker.js.map +1 -0
  277. package/dist/local-actions/browser-action.d.ts +78 -0
  278. package/dist/local-actions/browser-action.d.ts.map +1 -0
  279. package/dist/local-actions/browser-action.js +307 -0
  280. package/dist/local-actions/browser-action.js.map +1 -0
  281. package/dist/local-actions/callback-relay.d.ts +45 -0
  282. package/dist/local-actions/callback-relay.d.ts.map +1 -0
  283. package/dist/local-actions/callback-relay.js +265 -0
  284. package/dist/local-actions/callback-relay.js.map +1 -0
  285. package/dist/local-actions/clipboard.d.ts +3 -0
  286. package/dist/local-actions/clipboard.d.ts.map +1 -0
  287. package/dist/local-actions/clipboard.js +18 -0
  288. package/dist/local-actions/clipboard.js.map +1 -0
  289. package/dist/local-actions/contracts.d.ts +63 -0
  290. package/dist/local-actions/contracts.d.ts.map +1 -0
  291. package/dist/local-actions/contracts.js +18 -0
  292. package/dist/local-actions/contracts.js.map +1 -0
  293. package/dist/local-actions/desktop-presentation.d.ts +141 -0
  294. package/dist/local-actions/desktop-presentation.d.ts.map +1 -0
  295. package/dist/local-actions/desktop-presentation.js +253 -0
  296. package/dist/local-actions/desktop-presentation.js.map +1 -0
  297. package/dist/local-actions/device-selection.d.ts +43 -0
  298. package/dist/local-actions/device-selection.d.ts.map +1 -0
  299. package/dist/local-actions/device-selection.js +154 -0
  300. package/dist/local-actions/device-selection.js.map +1 -0
  301. package/dist/local-actions/executor.d.ts +49 -0
  302. package/dist/local-actions/executor.d.ts.map +1 -0
  303. package/dist/local-actions/executor.js +329 -0
  304. package/dist/local-actions/executor.js.map +1 -0
  305. package/dist/local-actions/file-transfers.d.ts +141 -0
  306. package/dist/local-actions/file-transfers.d.ts.map +1 -0
  307. package/dist/local-actions/file-transfers.js +446 -0
  308. package/dist/local-actions/file-transfers.js.map +1 -0
  309. package/dist/local-actions/foreground-notifications.d.ts +45 -0
  310. package/dist/local-actions/foreground-notifications.d.ts.map +1 -0
  311. package/dist/local-actions/foreground-notifications.js +163 -0
  312. package/dist/local-actions/foreground-notifications.js.map +1 -0
  313. package/dist/local-actions/git-signing.d.ts +39 -0
  314. package/dist/local-actions/git-signing.d.ts.map +1 -0
  315. package/dist/local-actions/git-signing.js +135 -0
  316. package/dist/local-actions/git-signing.js.map +1 -0
  317. package/dist/local-actions/index.d.ts +19 -0
  318. package/dist/local-actions/index.d.ts.map +1 -0
  319. package/dist/local-actions/index.js +19 -0
  320. package/dist/local-actions/index.js.map +1 -0
  321. package/dist/local-actions/local-service-actions.d.ts +87 -0
  322. package/dist/local-actions/local-service-actions.d.ts.map +1 -0
  323. package/dist/local-actions/local-service-actions.js +347 -0
  324. package/dist/local-actions/local-service-actions.js.map +1 -0
  325. package/dist/local-actions/loopback.d.ts +5 -0
  326. package/dist/local-actions/loopback.d.ts.map +1 -0
  327. package/dist/local-actions/loopback.js +15 -0
  328. package/dist/local-actions/loopback.js.map +1 -0
  329. package/dist/local-actions/policy-store.d.ts +7 -0
  330. package/dist/local-actions/policy-store.d.ts.map +1 -0
  331. package/dist/local-actions/policy-store.js +70 -0
  332. package/dist/local-actions/policy-store.js.map +1 -0
  333. package/dist/local-actions/policy.d.ts +21 -0
  334. package/dist/local-actions/policy.d.ts.map +1 -0
  335. package/dist/local-actions/policy.js +43 -0
  336. package/dist/local-actions/policy.js.map +1 -0
  337. package/dist/local-actions/private-port-forward.d.ts +36 -0
  338. package/dist/local-actions/private-port-forward.d.ts.map +1 -0
  339. package/dist/local-actions/private-port-forward.js +154 -0
  340. package/dist/local-actions/private-port-forward.js.map +1 -0
  341. package/dist/local-actions/provider-mcp-config.d.ts +101 -0
  342. package/dist/local-actions/provider-mcp-config.d.ts.map +1 -0
  343. package/dist/local-actions/provider-mcp-config.js +166 -0
  344. package/dist/local-actions/provider-mcp-config.js.map +1 -0
  345. package/dist/local-actions/providers.d.ts +12 -0
  346. package/dist/local-actions/providers.d.ts.map +1 -0
  347. package/dist/local-actions/providers.js +42 -0
  348. package/dist/local-actions/providers.js.map +1 -0
  349. package/dist/local-actions/schemas.d.ts +4 -0
  350. package/dist/local-actions/schemas.d.ts.map +1 -0
  351. package/dist/local-actions/schemas.js +314 -0
  352. package/dist/local-actions/schemas.js.map +1 -0
  353. package/dist/local-actions/sensitive-consent.d.ts +32 -0
  354. package/dist/local-actions/sensitive-consent.d.ts.map +1 -0
  355. package/dist/local-actions/sensitive-consent.js +47 -0
  356. package/dist/local-actions/sensitive-consent.js.map +1 -0
  357. package/dist/machines/command-launch.d.ts +17 -0
  358. package/dist/machines/command-launch.d.ts.map +1 -0
  359. package/dist/machines/command-launch.js +52 -0
  360. package/dist/machines/command-launch.js.map +1 -0
  361. package/dist/machines/execution-receipt.d.ts +18 -0
  362. package/dist/machines/execution-receipt.d.ts.map +1 -0
  363. package/dist/machines/execution-receipt.js +87 -0
  364. package/dist/machines/execution-receipt.js.map +1 -0
  365. package/dist/machines/executions-screen.d.ts +6 -0
  366. package/dist/machines/executions-screen.d.ts.map +1 -0
  367. package/dist/machines/executions-screen.js +410 -0
  368. package/dist/machines/executions-screen.js.map +1 -0
  369. package/dist/machines/explorer.d.ts +75 -0
  370. package/dist/machines/explorer.d.ts.map +1 -0
  371. package/dist/machines/explorer.js +1726 -0
  372. package/dist/machines/explorer.js.map +1 -0
  373. package/dist/machines/live-supervisor-update.d.ts +325 -0
  374. package/dist/machines/live-supervisor-update.d.ts.map +1 -0
  375. package/dist/machines/live-supervisor-update.js +513 -0
  376. package/dist/machines/live-supervisor-update.js.map +1 -0
  377. package/dist/machines/machine-first.d.ts +123 -0
  378. package/dist/machines/machine-first.d.ts.map +1 -0
  379. package/dist/machines/machine-first.js +119 -0
  380. package/dist/machines/machine-first.js.map +1 -0
  381. package/dist/machines/opencode-supervisor.d.ts +51 -0
  382. package/dist/machines/opencode-supervisor.d.ts.map +1 -0
  383. package/dist/machines/opencode-supervisor.js +124 -0
  384. package/dist/machines/opencode-supervisor.js.map +1 -0
  385. package/dist/machines/pagination.d.ts +4 -0
  386. package/dist/machines/pagination.d.ts.map +1 -0
  387. package/dist/machines/pagination.js +7 -0
  388. package/dist/machines/pagination.js.map +1 -0
  389. package/dist/machines/provider-availability.d.ts +42 -0
  390. package/dist/machines/provider-availability.d.ts.map +1 -0
  391. package/dist/machines/provider-availability.js +82 -0
  392. package/dist/machines/provider-availability.js.map +1 -0
  393. package/dist/machines/provider-screen.d.ts +12 -0
  394. package/dist/machines/provider-screen.d.ts.map +1 -0
  395. package/dist/machines/provider-screen.js +129 -0
  396. package/dist/machines/provider-screen.js.map +1 -0
  397. package/dist/machines/session-actionability.d.ts +86 -0
  398. package/dist/machines/session-actionability.d.ts.map +1 -0
  399. package/dist/machines/session-actionability.js +125 -0
  400. package/dist/machines/session-actionability.js.map +1 -0
  401. package/dist/machines/session-visibility.d.ts +139 -0
  402. package/dist/machines/session-visibility.d.ts.map +1 -0
  403. package/dist/machines/session-visibility.js +109 -0
  404. package/dist/machines/session-visibility.js.map +1 -0
  405. package/dist/platform/adapter.d.ts +62 -0
  406. package/dist/platform/adapter.d.ts.map +1 -0
  407. package/dist/platform/adapter.js +240 -0
  408. package/dist/platform/adapter.js.map +1 -0
  409. package/dist/platform/support.d.ts +12 -0
  410. package/dist/platform/support.d.ts.map +1 -0
  411. package/dist/platform/support.js +25 -0
  412. package/dist/platform/support.js.map +1 -0
  413. package/dist/pty/contracts.d.ts +45 -0
  414. package/dist/pty/contracts.d.ts.map +1 -0
  415. package/dist/pty/contracts.js +2 -0
  416. package/dist/pty/contracts.js.map +1 -0
  417. package/dist/pty/evidence-gate.d.ts +14 -0
  418. package/dist/pty/evidence-gate.d.ts.map +1 -0
  419. package/dist/pty/evidence-gate.js +35 -0
  420. package/dist/pty/evidence-gate.js.map +1 -0
  421. package/dist/pty/index.d.ts +5 -0
  422. package/dist/pty/index.d.ts.map +1 -0
  423. package/dist/pty/index.js +5 -0
  424. package/dist/pty/index.js.map +1 -0
  425. package/dist/pty/node-host-terminal.d.ts +14 -0
  426. package/dist/pty/node-host-terminal.d.ts.map +1 -0
  427. package/dist/pty/node-host-terminal.js +173 -0
  428. package/dist/pty/node-host-terminal.js.map +1 -0
  429. package/dist/pty/node-process.d.ts +26 -0
  430. package/dist/pty/node-process.d.ts.map +1 -0
  431. package/dist/pty/node-process.js +102 -0
  432. package/dist/pty/node-process.js.map +1 -0
  433. package/dist/runtime/api-terminal-control-plane.d.ts +7 -0
  434. package/dist/runtime/api-terminal-control-plane.d.ts.map +1 -0
  435. package/dist/runtime/api-terminal-control-plane.js +95 -0
  436. package/dist/runtime/api-terminal-control-plane.js.map +1 -0
  437. package/dist/runtime/backoff.d.ts +17 -0
  438. package/dist/runtime/backoff.d.ts.map +1 -0
  439. package/dist/runtime/backoff.js +61 -0
  440. package/dist/runtime/backoff.js.map +1 -0
  441. package/dist/runtime/boundary.d.ts +171 -0
  442. package/dist/runtime/boundary.d.ts.map +1 -0
  443. package/dist/runtime/boundary.js +1858 -0
  444. package/dist/runtime/boundary.js.map +1 -0
  445. package/dist/runtime/capability-gate.d.ts +25 -0
  446. package/dist/runtime/capability-gate.d.ts.map +1 -0
  447. package/dist/runtime/capability-gate.js +77 -0
  448. package/dist/runtime/capability-gate.js.map +1 -0
  449. package/dist/runtime/contracts.d.ts +25 -0
  450. package/dist/runtime/contracts.d.ts.map +1 -0
  451. package/dist/runtime/contracts.js +66 -0
  452. package/dist/runtime/contracts.js.map +1 -0
  453. package/dist/runtime/errors.d.ts +20 -0
  454. package/dist/runtime/errors.d.ts.map +1 -0
  455. package/dist/runtime/errors.js +22 -0
  456. package/dist/runtime/errors.js.map +1 -0
  457. package/dist/runtime/index.d.ts +9 -0
  458. package/dist/runtime/index.d.ts.map +1 -0
  459. package/dist/runtime/index.js +9 -0
  460. package/dist/runtime/index.js.map +1 -0
  461. package/dist/runtime/node-foreground-session.d.ts +50 -0
  462. package/dist/runtime/node-foreground-session.d.ts.map +1 -0
  463. package/dist/runtime/node-foreground-session.js +431 -0
  464. package/dist/runtime/node-foreground-session.js.map +1 -0
  465. package/dist/runtime/node-websocket-connector.d.ts +6 -0
  466. package/dist/runtime/node-websocket-connector.d.ts.map +1 -0
  467. package/dist/runtime/node-websocket-connector.js +344 -0
  468. package/dist/runtime/node-websocket-connector.js.map +1 -0
  469. package/dist/runtime/observer-screen.d.ts +6 -0
  470. package/dist/runtime/observer-screen.d.ts.map +1 -0
  471. package/dist/runtime/observer-screen.js +144 -0
  472. package/dist/runtime/observer-screen.js.map +1 -0
  473. package/dist/runtime/owner-grant-operations.d.ts +136 -0
  474. package/dist/runtime/owner-grant-operations.d.ts.map +1 -0
  475. package/dist/runtime/owner-grant-operations.js +216 -0
  476. package/dist/runtime/owner-grant-operations.js.map +1 -0
  477. package/dist/runtime/owner-grants-screen.d.ts +40 -0
  478. package/dist/runtime/owner-grants-screen.d.ts.map +1 -0
  479. package/dist/runtime/owner-grants-screen.js +987 -0
  480. package/dist/runtime/owner-grants-screen.js.map +1 -0
  481. package/dist/runtime/terminal-transport.d.ts +85 -0
  482. package/dist/runtime/terminal-transport.d.ts.map +1 -0
  483. package/dist/runtime/terminal-transport.js +152 -0
  484. package/dist/runtime/terminal-transport.js.map +1 -0
  485. package/dist/sync/continuous-sync-supervisor.d.ts +107 -0
  486. package/dist/sync/continuous-sync-supervisor.d.ts.map +1 -0
  487. package/dist/sync/continuous-sync-supervisor.js +1117 -0
  488. package/dist/sync/continuous-sync-supervisor.js.map +1 -0
  489. package/dist/sync/index.d.ts +9 -0
  490. package/dist/sync/index.d.ts.map +1 -0
  491. package/dist/sync/index.js +9 -0
  492. package/dist/sync/index.js.map +1 -0
  493. package/dist/sync/journal.d.ts +57 -0
  494. package/dist/sync/journal.d.ts.map +1 -0
  495. package/dist/sync/journal.js +685 -0
  496. package/dist/sync/journal.js.map +1 -0
  497. package/dist/sync/revisions.d.ts +118 -0
  498. package/dist/sync/revisions.d.ts.map +1 -0
  499. package/dist/sync/revisions.js +364 -0
  500. package/dist/sync/revisions.js.map +1 -0
  501. package/dist/sync/supervisor.d.ts +107 -0
  502. package/dist/sync/supervisor.d.ts.map +1 -0
  503. package/dist/sync/supervisor.js +246 -0
  504. package/dist/sync/supervisor.js.map +1 -0
  505. package/dist/sync/workspace-sync-client.d.ts +18 -0
  506. package/dist/sync/workspace-sync-client.d.ts.map +1 -0
  507. package/dist/sync/workspace-sync-client.js +150 -0
  508. package/dist/sync/workspace-sync-client.js.map +1 -0
  509. package/dist/sync/workspace-sync-coordinator.d.ts +76 -0
  510. package/dist/sync/workspace-sync-coordinator.d.ts.map +1 -0
  511. package/dist/sync/workspace-sync-coordinator.js +621 -0
  512. package/dist/sync/workspace-sync-coordinator.js.map +1 -0
  513. package/dist/sync/workspace-sync-product-service.d.ts +79 -0
  514. package/dist/sync/workspace-sync-product-service.d.ts.map +1 -0
  515. package/dist/sync/workspace-sync-product-service.js +398 -0
  516. package/dist/sync/workspace-sync-product-service.js.map +1 -0
  517. package/dist/sync/workspace-sync-protocol.d.ts +145 -0
  518. package/dist/sync/workspace-sync-protocol.d.ts.map +1 -0
  519. package/dist/sync/workspace-sync-protocol.js +347 -0
  520. package/dist/sync/workspace-sync-protocol.js.map +1 -0
  521. package/dist/terminal/appbar.d.ts +40 -0
  522. package/dist/terminal/appbar.d.ts.map +1 -0
  523. package/dist/terminal/appbar.js +55 -0
  524. package/dist/terminal/appbar.js.map +1 -0
  525. package/dist/terminal/cell-width.d.ts +3 -0
  526. package/dist/terminal/cell-width.d.ts.map +1 -0
  527. package/dist/terminal/cell-width.js +53 -0
  528. package/dist/terminal/cell-width.js.map +1 -0
  529. package/dist/terminal/codec.d.ts +107 -0
  530. package/dist/terminal/codec.d.ts.map +1 -0
  531. package/dist/terminal/codec.js +538 -0
  532. package/dist/terminal/codec.js.map +1 -0
  533. package/dist/terminal/foreground.d.ts +135 -0
  534. package/dist/terminal/foreground.d.ts.map +1 -0
  535. package/dist/terminal/foreground.js +2097 -0
  536. package/dist/terminal/foreground.js.map +1 -0
  537. package/dist/terminal/generated/terminal-wire-v1.d.ts +26 -0
  538. package/dist/terminal/generated/terminal-wire-v1.d.ts.map +1 -0
  539. package/dist/terminal/generated/terminal-wire-v1.js +27 -0
  540. package/dist/terminal/generated/terminal-wire-v1.js.map +1 -0
  541. package/dist/terminal/mode.d.ts +33 -0
  542. package/dist/terminal/mode.d.ts.map +1 -0
  543. package/dist/terminal/mode.js +99 -0
  544. package/dist/terminal/mode.js.map +1 -0
  545. package/dist/terminal/passthrough.d.ts +47 -0
  546. package/dist/terminal/passthrough.d.ts.map +1 -0
  547. package/dist/terminal/passthrough.js +666 -0
  548. package/dist/terminal/passthrough.js.map +1 -0
  549. package/dist/terminal/resume.d.ts +58 -0
  550. package/dist/terminal/resume.d.ts.map +1 -0
  551. package/dist/terminal/resume.js +185 -0
  552. package/dist/terminal/resume.js.map +1 -0
  553. package/dist/terminal/viewport.d.ts +93 -0
  554. package/dist/terminal/viewport.d.ts.map +1 -0
  555. package/dist/terminal/viewport.js +266 -0
  556. package/dist/terminal/viewport.js.map +1 -0
  557. package/dist/terminal/workbench.d.ts +32 -0
  558. package/dist/terminal/workbench.d.ts.map +1 -0
  559. package/dist/terminal/workbench.js +316 -0
  560. package/dist/terminal/workbench.js.map +1 -0
  561. package/dist/terminal/xterm-vte.d.ts +49 -0
  562. package/dist/terminal/xterm-vte.d.ts.map +1 -0
  563. package/dist/terminal/xterm-vte.js +602 -0
  564. package/dist/terminal/xterm-vte.js.map +1 -0
  565. package/dist/version.d.ts +3 -0
  566. package/dist/version.d.ts.map +1 -0
  567. package/dist/version.js +3 -0
  568. package/dist/version.js.map +1 -0
  569. package/dist/workspace/binding-path.d.ts +17 -0
  570. package/dist/workspace/binding-path.d.ts.map +1 -0
  571. package/dist/workspace/binding-path.js +91 -0
  572. package/dist/workspace/binding-path.js.map +1 -0
  573. package/dist/workspace/binding-store.d.ts +107 -0
  574. package/dist/workspace/binding-store.d.ts.map +1 -0
  575. package/dist/workspace/binding-store.js +783 -0
  576. package/dist/workspace/binding-store.js.map +1 -0
  577. package/dist/workspace/binding.d.ts +35 -0
  578. package/dist/workspace/binding.d.ts.map +1 -0
  579. package/dist/workspace/binding.js +70 -0
  580. package/dist/workspace/binding.js.map +1 -0
  581. package/dist/workspace/errors.d.ts +4 -0
  582. package/dist/workspace/errors.d.ts.map +1 -0
  583. package/dist/workspace/errors.js +19 -0
  584. package/dist/workspace/errors.js.map +1 -0
  585. package/dist/workspace/exclusion.d.ts +21 -0
  586. package/dist/workspace/exclusion.d.ts.map +1 -0
  587. package/dist/workspace/exclusion.js +243 -0
  588. package/dist/workspace/exclusion.js.map +1 -0
  589. package/dist/workspace/index.d.ts +8 -0
  590. package/dist/workspace/index.d.ts.map +1 -0
  591. package/dist/workspace/index.js +8 -0
  592. package/dist/workspace/index.js.map +1 -0
  593. package/dist/workspace/lifecycle.d.ts +56 -0
  594. package/dist/workspace/lifecycle.d.ts.map +1 -0
  595. package/dist/workspace/lifecycle.js +94 -0
  596. package/dist/workspace/lifecycle.js.map +1 -0
  597. package/dist/workspace/manifest.d.ts +47 -0
  598. package/dist/workspace/manifest.d.ts.map +1 -0
  599. package/dist/workspace/manifest.js +243 -0
  600. package/dist/workspace/manifest.js.map +1 -0
  601. package/dist/workspace/paths.d.ts +14 -0
  602. package/dist/workspace/paths.d.ts.map +1 -0
  603. package/dist/workspace/paths.js +67 -0
  604. package/dist/workspace/paths.js.map +1 -0
  605. package/dist/workspace/schema.d.ts +15 -0
  606. package/dist/workspace/schema.d.ts.map +1 -0
  607. package/dist/workspace/schema.js +33 -0
  608. package/dist/workspace/schema.js.map +1 -0
  609. package/dist/workspace/selection-screen.d.ts +4 -0
  610. package/dist/workspace/selection-screen.d.ts.map +1 -0
  611. package/dist/workspace/selection-screen.js +216 -0
  612. package/dist/workspace/selection-screen.js.map +1 -0
  613. package/dist/workspace/selection-service.d.ts +16 -0
  614. package/dist/workspace/selection-service.d.ts.map +1 -0
  615. package/dist/workspace/selection-service.js +65 -0
  616. package/dist/workspace/selection-service.js.map +1 -0
  617. package/node_modules/@xterm/headless/README.md +31 -0
  618. package/node_modules/@xterm/headless/lib-headless/xterm-headless.js +2 -0
  619. package/node_modules/@xterm/headless/lib-headless/xterm-headless.js.map +1 -0
  620. package/node_modules/@xterm/headless/lib-headless/xterm-headless.mjs +46 -0
  621. package/node_modules/@xterm/headless/lib-headless/xterm-headless.mjs.map +7 -0
  622. package/node_modules/@xterm/headless/package.json +29 -0
  623. package/node_modules/@xterm/headless/typings/xterm-headless.d.ts +1385 -0
  624. package/package.json +75 -0
@@ -0,0 +1,2814 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { requireCapability, } from "../api/client.js";
3
+ import { ARTIFACT_CHANNEL, packageBuildDigest, PROTOCOL_RANGE } from "../build-identity.js";
4
+ import { DEFAULT_BASE_URL, environmentCredentialState, publicConfig } from "../config/config.js";
5
+ import { EXIT_CODES, CunaError, unsupportedError, usageError } from "../core/errors.js";
6
+ import { REMOTE_CONVERGENCE_BUDGET_MS, REMOTE_CONVERGENCE_POLL_INTERVAL_MS, observationBudgetElapsed, } from "../core/observation-budget.js";
7
+ import { OFF_CONTRACT_RESPONSE_HINT, SUPPORT_URL, automationCredentialHint } from "../core/product-web.js";
8
+ import { assertCanonicalUuid, assertIdempotencyKey, assertMachineId, assertPublicId, assertSafeDisplayText, ContractViolation, integerArgument, } from "../core/validation.js";
9
+ import { preflightAgentJourneyInvocation } from "../journey/intent.js";
10
+ import { listAllMachines } from "../machines/pagination.js";
11
+ import { classifyLiveSupervisorUpdateFailure, liveSupervisorInstallationEvidenceLabel, liveSupervisorInstallerOutcomeLabel, liveSupervisorInstallerReachLines, liveSupervisorSessionOutcomeLabel, liveSupervisorUpdateNotes, liveSupervisorUpdatePhaseLabel, liveSupervisorUpdateRecordSurvives, readLiveSupervisorUpdateOperation, summarizeLiveSupervisorUpdate, } from "../machines/live-supervisor-update.js";
12
+ import { isOpenCodeRuntimeUnverifiedCapabilityRejection, isOpenCodeSupervisorRepairCapabilityRejection, isOpenCodeSupervisorUpgradeCapabilityRejection, openCodeRuntimeUnverified, openCodeSupervisorUpgradeRequired, } from "../machines/opencode-supervisor.js";
13
+ import { machineProviderAvailability, machineSupportsProvider, providerDisplayName, providerVerdict, } from "../machines/provider-availability.js";
14
+ import { classifySessionActionability, displaySessionActionability } from "../machines/session-actionability.js";
15
+ import { agentSessionProcessObservation, AGENT_SESSION_OBSERVATION_NOTE, isAgentSessionIntendedActive, } from "../machines/session-visibility.js";
16
+ import { loadWorkspaceBindingIntent } from "../workspace/binding-store.js";
17
+ import { INITIAL_RUNTIME_GATES } from "../runtime/contracts.js";
18
+ import { evaluateRuntimeSupport } from "../platform/support.js";
19
+ import { CLI_VERSION } from "../version.js";
20
+ import { assertRegisteredCliRoute, booleanOption, rejectUnknownOptions, stringOption, } from "../cli/parser.js";
21
+ function productionConvergencePoller() {
22
+ return Object.freeze({
23
+ now: () => Date.now(),
24
+ sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
25
+ });
26
+ }
27
+ /**
28
+ * Read back until an accepted mutation is visible, or until OUR budget elapses.
29
+ *
30
+ * D2, and the whole difference between the two answers this fixes. A single
31
+ * immediate read cannot distinguish "the change did not happen" from "the change
32
+ * has not arrived yet", and the CLI was reporting the first for the second with
33
+ * `retryable: false` — telling the user not to retry AND implying the mutation
34
+ * had not landed, both false, for a deletion that had already succeeded.
35
+ *
36
+ * On elapse this raises the budget refusal from `core/observation-budget.ts`,
37
+ * which is retryable and names the read-only command that settles it. It is NOT
38
+ * `postcondition_unverified`: that code is reserved for an observation that
39
+ * CONTRADICTS the write and that no amount of waiting repairs.
40
+ */
41
+ async function convergeOnRemoteState(context, input) {
42
+ const poller = context.convergencePoller ?? productionConvergencePoller();
43
+ const deadline = poller.now() + REMOTE_CONVERGENCE_BUDGET_MS;
44
+ let probe = await input.probe();
45
+ while (!probe.settled) {
46
+ const remaining = deadline - poller.now();
47
+ if (remaining <= 0) {
48
+ throw observationBudgetElapsed({
49
+ kind: "convergence",
50
+ operation: input.operation,
51
+ settleWith: input.settleWith,
52
+ budgetMs: REMOTE_CONVERGENCE_BUDGET_MS,
53
+ details: probe.details,
54
+ });
55
+ }
56
+ await poller.sleep(Math.min(REMOTE_CONVERGENCE_POLL_INTERVAL_MS, remaining));
57
+ probe = await input.probe();
58
+ }
59
+ return probe.observation;
60
+ }
61
+ /**
62
+ * The process states in which the child is no longer running. The producer
63
+ * chooses among all three: a signalled exit, an exit of the process's own
64
+ * accord, and an ending it could not classify.
65
+ */
66
+ const AGENT_SESSION_ENDED_PROCESS_STATES = new Set(["terminated", "exited", "failed"]);
67
+ /**
68
+ * Whether a termination has actually landed.
69
+ *
70
+ * Matching one value of a vocabulary the producer chooses from cannot settle:
71
+ * any ended process state satisfies this. The conjunction over the other two
72
+ * fields stays, so a process that merely died on its own is not read as a
73
+ * completed termination.
74
+ */
75
+ function agentSessionTerminationConfirmed(session) {
76
+ return session.desiredState === "terminated" &&
77
+ session.requestState === "terminal" &&
78
+ AGENT_SESSION_ENDED_PROCESS_STATES.has(session.processState);
79
+ }
80
+ /**
81
+ * An AgentSession carries three states and this CLI's own invariants are
82
+ * conjunctions of all three — see `agentSessionTerminationConfirmed` above, and
83
+ * the termination timeout, which reports all three because one is not enough to
84
+ * explain anything.
85
+ *
86
+ * The human rendering used to print `processState` alone, so a session that was
87
+ * `terminated / termination_pending / running` displayed as plain `running`:
88
+ * exactly the disagreement a person needs to see, replaced by the one word that
89
+ * hides it. Settled sessions still show a single word, because a triple on every
90
+ * healthy row is noise; anything unsettled shows all three, in the fixed order
91
+ * desired/request/process.
92
+ *
93
+ * The provenance of `processState` is the second thing this line hid. Every
94
+ * authority above is a CLAIM, and the producer publishes who established the
95
+ * process one; a row whose runtime lease kept moving while nothing observed the
96
+ * child printed the byte-identical line to a row a supervisor had just looked
97
+ * at. So whenever the state asserts a live process, the note from
98
+ * `session-visibility.ts` is printed beside it — `observed` adds nothing, which
99
+ * is why it has no note. It qualifies the state and never replaces it: a
100
+ * confirmed termination returns above this and can never acquire one.
101
+ */
102
+ function agentSessionStateLabel(session) {
103
+ if (agentSessionTerminationConfirmed(session))
104
+ return "terminated";
105
+ const note = session.processState === "running"
106
+ ? AGENT_SESSION_OBSERVATION_NOTE[agentSessionProcessObservation(session)]
107
+ : undefined;
108
+ const qualifier = note === undefined ? "" : ` (${note})`;
109
+ if (session.desiredState === "running" &&
110
+ session.requestState === "launched" &&
111
+ session.processState === "running") {
112
+ return `running${qualifier}`;
113
+ }
114
+ return `${session.desiredState}/${session.requestState}/${session.processState}${qualifier}`;
115
+ }
116
+ function requireCredential(context) {
117
+ if (context.credentialMode !== undefined)
118
+ return;
119
+ throw new CunaError({
120
+ code: "cuna.auth.required",
121
+ message: "This command requires a Cuna credential.",
122
+ exitCode: EXIT_CODES.auth,
123
+ hint: `Run \`cuna login\` for interactive use, or use an automation credential. ${automationCredentialHint()}`,
124
+ });
125
+ }
126
+ function requireOperand(operands, index, label) {
127
+ const value = operands[index];
128
+ if (value === undefined)
129
+ throw usageError(`Missing ${label}.`);
130
+ return value;
131
+ }
132
+ /**
133
+ * A required option's value, or a refusal that says the option is missing.
134
+ * Absent and malformed are different mistakes: reading an absent option as `""`
135
+ * and handing it to a shape validator reports the wrong one.
136
+ */
137
+ /**
138
+ * The workspace binding this folder is already bound to, for the account this
139
+ * invocation authenticates as. Read-only: creating a binding means choosing a
140
+ * local root, an exclusion policy and a project identity, which are the
141
+ * journey's decisions, so an unbound folder is refused rather than bound here.
142
+ */
143
+ async function resolveLocalWorkspaceBinding(context) {
144
+ const identity = await context.client.getIdentity();
145
+ const workspaceId = identity.workspaceId;
146
+ const unbound = (reason, hint) => new CunaError({
147
+ code: "cuna.workspace.binding_required",
148
+ message: "This folder is not bound to a Cuna Machine.",
149
+ exitCode: EXIT_CODES.usage,
150
+ hint,
151
+ details: { reason, folder: context.workspaceRoot ?? "." },
152
+ });
153
+ if (workspaceId === undefined) {
154
+ throw unbound("workspace_unassigned", "No Cuna workspace is assigned to this account yet. Run `cuna workspace show` to see where you stand.");
155
+ }
156
+ const loaded = await loadWorkspaceBindingIntent({
157
+ startPath: context.workspaceRoot ?? ".",
158
+ profileId: context.config.profile,
159
+ userId: identity.id,
160
+ workspaceId,
161
+ });
162
+ if (loaded === undefined) {
163
+ throw unbound("folder_not_bound", "Run `cuna opencode` (or `cuna claude`, `cuna codex`) in this folder once: that binds the folder to a Machine and records the workspace binding this command needs. Or pass --workspace-binding-id and --workspace-generation yourself.");
164
+ }
165
+ const record = loaded.record;
166
+ // A folder that has moved is bound to a path that is no longer this one. The
167
+ // journey knows how to relocate it; a batch mutation must not pretend the
168
+ // binding still describes where it is running.
169
+ if (loaded.relocationRequired) {
170
+ throw unbound("relocation_required", "This folder has moved since it was bound. Run `cuna opencode` in it once so Cuna can relocate the binding.");
171
+ }
172
+ if (record.generation < 1) {
173
+ throw unbound("generation_uncommitted", "This folder is bound but has no committed workspace generation yet. Run `cuna opencode` in this folder once to commit one.");
174
+ }
175
+ return Object.freeze({ bindingId: record.bindingId, generation: record.generation });
176
+ }
177
+ function requireOption(parsed, name, hint) {
178
+ const value = stringOption(parsed, name);
179
+ if (value === undefined) {
180
+ throw usageError(`Option --${name} is required.`, hint);
181
+ }
182
+ return value;
183
+ }
184
+ function integerOption(parsed, name, minimum, maximum) {
185
+ const raw = stringOption(parsed, name);
186
+ if (raw === undefined)
187
+ return undefined;
188
+ return integerArgument(raw, name, minimum, maximum);
189
+ }
190
+ function agentOption(parsed, required) {
191
+ const raw = stringOption(parsed, "agent");
192
+ if (raw === undefined) {
193
+ if (required)
194
+ throw usageError("Option --agent is required.");
195
+ return undefined;
196
+ }
197
+ if (raw !== "claude-code" && raw !== "codex" && raw !== "opencode") {
198
+ throw usageError("Option --agent must be claude-code, codex, or opencode.");
199
+ }
200
+ return raw;
201
+ }
202
+ function normalizedAgentSessionAuthMode(rawAuthMode, credentialBinding) {
203
+ let authMode;
204
+ if (rawAuthMode === undefined) {
205
+ authMode = undefined;
206
+ }
207
+ else if (rawAuthMode === "interactive_login" || rawAuthMode === "credential_binding") {
208
+ authMode = rawAuthMode;
209
+ }
210
+ else {
211
+ throw usageError("Option --auth-mode must be interactive_login or credential_binding.");
212
+ }
213
+ if (authMode === "credential_binding" && credentialBinding === undefined) {
214
+ throw usageError("Option --credential-binding is required for credential_binding auth mode.");
215
+ }
216
+ if (authMode !== "credential_binding" && credentialBinding !== undefined) {
217
+ throw usageError("Option --credential-binding requires --auth-mode credential_binding.");
218
+ }
219
+ return authMode;
220
+ }
221
+ function requireConfirmation(parsed, command) {
222
+ if (booleanOption(parsed, "yes"))
223
+ return;
224
+ throw new CunaError({
225
+ code: "cuna.confirmation.required",
226
+ message: `The ${command} mutation requires explicit confirmation in this initial build.`,
227
+ exitCode: EXIT_CODES.policy,
228
+ hint: `Review the target and repeat with --yes.`,
229
+ });
230
+ }
231
+ /**
232
+ * The read-back CONTRADICTED the write, and waiting will not repair it.
233
+ *
234
+ * Narrowed 2026-08-19. This used to be raised for two unrelated observations:
235
+ * a genuine contradiction (a rename that observed a different name, a create
236
+ * that observed a different id — an identity the producer can never converge
237
+ * to), and a state that had simply not arrived yet. Only the first belongs
238
+ * here. The second goes through `convergeOnRemoteState` and, if the CLI's own
239
+ * budget runs out first, reports that as the CLI's budget rather than as the
240
+ * server's failure.
241
+ */
242
+ function postconditionUnverified(operation, details) {
243
+ throw new CunaError({
244
+ code: "cuna.remote.postcondition_unverified",
245
+ message: `Cuna accepted ${operation}, but the CLI could not verify the resulting remote state.`,
246
+ exitCode: EXIT_CODES.conflict,
247
+ hint: "Inspect the target with a read-only command before retrying the mutation.",
248
+ details,
249
+ });
250
+ }
251
+ function apiKeyCreateInput(parsed, now = Date.now()) {
252
+ const rawName = stringOption(parsed, "name");
253
+ if (rawName === undefined || rawName.length < 1 || rawName.length > 80 || rawName.trim() !== rawName) {
254
+ throw usageError("Option --name is required and must contain 1 through 80 non-padding characters.");
255
+ }
256
+ const name = assertSafeDisplayText(rawName, "API key name");
257
+ const expiresAt = stringOption(parsed, "expires-at");
258
+ if (expiresAt !== undefined) {
259
+ const utc = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/u;
260
+ const expiryMs = Date.parse(expiresAt);
261
+ const oneHour = 60 * 60 * 1_000;
262
+ const oneYear = 365 * 24 * oneHour;
263
+ if (!utc.test(expiresAt) || !Number.isFinite(expiryMs) || expiryMs < now + oneHour || expiryMs > now + oneYear) {
264
+ throw usageError("Option --expires-at must be a UTC instant between 1 hour and 365 days from now.");
265
+ }
266
+ return Object.freeze({ name, expiresAt: new Date(expiryMs).toISOString() });
267
+ }
268
+ return Object.freeze({ name });
269
+ }
270
+ /**
271
+ * Whether the key on this row still opens the door, from both timestamps.
272
+ *
273
+ * `expiresAt` and `revokedAt` are independent nullable instants
274
+ * (`ApiKeyMetadata`), and `--expires-at` mints keys up to 365 days out. The
275
+ * human line derived one word from `revokedAt` alone, so a key past its expiry
276
+ * but never revoked printed `active` while the JSON on the same invocation said
277
+ * otherwise. Expiry is now a state the word can carry, and the instant it turns
278
+ * on is printed rather than left to a second command.
279
+ */
280
+ function apiKeyStatusLabel(key, now) {
281
+ if (key.revokedAt !== null)
282
+ return `revoked ${key.revokedAt}`;
283
+ if (key.expiresAt === null)
284
+ return "active (no expiry)";
285
+ const expiryMs = Date.parse(key.expiresAt);
286
+ if (!Number.isFinite(expiryMs))
287
+ return `active until ${key.expiresAt}`;
288
+ return expiryMs <= now ? `expired ${key.expiresAt}` : `active until ${key.expiresAt}`;
289
+ }
290
+ /**
291
+ * The reconciliation key for one create operation.
292
+ *
293
+ * Demanding this from the user made every `machines create` fail until they
294
+ * discovered a flag whose purpose only matters when a create outcome is
295
+ * uncertain — and the layer below already defaulted it, so the requirement
296
+ * bought nothing. It is now generated per invocation and the flag remains as an
297
+ * override, which is the case that actually needs a caller-known value: reusing
298
+ * the same key to reconcile a create whose result was never observed.
299
+ */
300
+ function idempotencyKey(parsed) {
301
+ const value = stringOption(parsed, "idempotency-key");
302
+ return value === undefined ? randomUUID() : assertIdempotencyKey(value);
303
+ }
304
+ function machineRecord(machine) {
305
+ const provider = machineProviderAvailability(machine);
306
+ return Object.freeze({
307
+ id: machine.id,
308
+ name: machine.name,
309
+ state: machine.state,
310
+ ...(machine.agent === undefined ? {} : { agent: machine.agent }),
311
+ ...(machine.vcpus === undefined ? {} : { vcpus: machine.vcpus }),
312
+ ...(machine.memoryMiB === undefined ? {} : { memory_mib: machine.memoryMiB }),
313
+ ...(machine.createdAt === undefined ? {} : { created_at: machine.createdAt }),
314
+ ...(machine.updatedAt === undefined ? {} : { updated_at: machine.updatedAt }),
315
+ provider_availability: Object.freeze({
316
+ ...(provider.declaredId === undefined ? {} : { declared_id: provider.declaredId }),
317
+ display_name: provider.displayName,
318
+ usability: provider.usability,
319
+ actionable: provider.actionable,
320
+ ...(provider.reasonCode === undefined ? {} : { reason_code: provider.reasonCode }),
321
+ ...(provider.observationVersion === undefined ? {} : { observation_version: provider.observationVersion }),
322
+ }),
323
+ });
324
+ }
325
+ function agentSessionRecord(session, machine, now) {
326
+ const actionability = machine === undefined || now === undefined
327
+ ? undefined
328
+ : classifySessionActionability({ session, machine, now });
329
+ return Object.freeze({
330
+ id: session.id,
331
+ machine_id: session.machineId,
332
+ ...(session.workspaceBindingId === undefined
333
+ ? {}
334
+ : {
335
+ workspace_binding_id: session.workspaceBindingId,
336
+ workspace_generation: session.workspaceGeneration,
337
+ }),
338
+ name: session.name,
339
+ agent: session.agent,
340
+ cwd: session.cwd,
341
+ auth_mode: session.authMode,
342
+ desired_state: session.desiredState,
343
+ request_state: session.requestState,
344
+ process_state: session.processState,
345
+ // The provenance of the line above, and never omitted. A caller reading
346
+ // `--json` must not have to infer it from a lease: absence on the wire is
347
+ // folded to `unknown` once, in `session-visibility.ts`, and emitted here so
348
+ // that "no supervisor established this" is a value rather than a silence.
349
+ process_observation: agentSessionProcessObservation(session),
350
+ ...(session.terminalReason === undefined ? {} : { terminal_reason: session.terminalReason }),
351
+ ...(session.processEpoch === undefined ? {} : { process_epoch: session.processEpoch }),
352
+ ...(session.runtimeObservedAt === undefined ? {} : { runtime_observed_at: session.runtimeObservedAt }),
353
+ ...(session.runtimeExpiresAt === undefined ? {} : { runtime_expires_at: session.runtimeExpiresAt }),
354
+ ...(session.terminationRequestedAt === undefined
355
+ ? {}
356
+ : { termination_requested_at: session.terminationRequestedAt }),
357
+ row_version: session.rowVersion,
358
+ created_at: session.createdAt,
359
+ updated_at: session.updatedAt,
360
+ ...(actionability === undefined ? {} : {
361
+ base_state: actionability.baseState,
362
+ refresh_status: actionability.refreshStatus,
363
+ can_attach: actionability.canAttach,
364
+ recovery_action: actionability.recoveryAction,
365
+ reason_code: actionability.reasonCode,
366
+ observation_revision: actionability.observationRevision,
367
+ }),
368
+ });
369
+ }
370
+ function agentDisplayName(agent) {
371
+ return providerDisplayName(agent);
372
+ }
373
+ function machineSessionCounts(machine, sessions, now) {
374
+ const count = (agent) => {
375
+ const matching = sessions.filter((session) => session.agent === agent);
376
+ return Object.freeze({
377
+ running: matching.filter((session) => session.processState === "running" &&
378
+ classifySessionActionability({ session, machine, now }).canAttach).length,
379
+ total: matching.length,
380
+ });
381
+ };
382
+ return Object.freeze({
383
+ claude: count("claude-code"),
384
+ codex: count("codex"),
385
+ opencode: count("opencode"),
386
+ });
387
+ }
388
+ async function listAllMachineAgentSessions(client, machineId) {
389
+ const items = [];
390
+ const cursors = new Set();
391
+ let cursor;
392
+ do {
393
+ const page = await client.listAgentSessions(machineId, {
394
+ limit: 100,
395
+ ...(cursor === undefined ? {} : { cursor }),
396
+ });
397
+ items.push(...page.items);
398
+ cursor = page.nextCursor;
399
+ if (cursor !== undefined && cursors.has(cursor)) {
400
+ throw new Error("AgentSession pagination repeated a cursor.");
401
+ }
402
+ if (cursor !== undefined)
403
+ cursors.add(cursor);
404
+ } while (cursor !== undefined);
405
+ return Object.freeze(items);
406
+ }
407
+ /**
408
+ * One flat record as one `key\tvalue` line per field.
409
+ *
410
+ * `config get` had no human rendering at all: a person at a terminal got the
411
+ * JSON record pretty-printed, braces and quotes included, for nine scalar
412
+ * fields. Nothing was hidden, which is why this is a rendering and not a new
413
+ * projection — every key and every value survives, in declaration order.
414
+ */
415
+ function renderScalarRecord(record) {
416
+ return Object.entries(record)
417
+ .map(([key, value]) => `${key}\t${value === null || value === undefined ? "null" : String(value)}`)
418
+ .join("\n");
419
+ }
420
+ /**
421
+ * A page footer that exists only when the page is not the whole answer.
422
+ *
423
+ * Without it "no such session" and "that session is on page 2" print the same
424
+ * bytes. The continuation is named as the exact next command option, because a
425
+ * truncation notice that does not say how to continue is only half the fact.
426
+ */
427
+ function truncationFooter(nextCursor) {
428
+ return nextCursor === undefined ? [] : [`-- more results; continue with --cursor ${nextCursor}`];
429
+ }
430
+ /**
431
+ * A bounded, safe token naming WHY a machine's AgentSessions could not be read.
432
+ *
433
+ * The overview used to report the constant `sessions_unavailable` for every
434
+ * cause. On 2026-09-07 an Edge release added a field this CLI's decoder did not
435
+ * know, so every read failed `no_unknown_fields` — and the overview said only
436
+ * "unavailable", which named nothing and pointed nowhere. Diagnosing it needed a
437
+ * different command. A refusal that cannot be told apart from any other refusal
438
+ * is not a report.
439
+ *
440
+ * Only vocabulary the CLI itself mints is rendered: an error code, or a
441
+ * contract predicate and the key path that failed. No message text, no server
442
+ * body, no field values — the reason must never become a channel for data the
443
+ * server chose. The result is capped so a hostile or absurd token cannot flood
444
+ * a terminal row.
445
+ */
446
+ function safeSessionsErrorReason(error) {
447
+ const bound = (value) => value.slice(0, 64);
448
+ if (error instanceof ContractViolation) {
449
+ return bound(error.field === undefined
450
+ ? `contract:${error.predicate}`
451
+ : `contract:${error.predicate}:${error.field}`);
452
+ }
453
+ if (error instanceof CunaError)
454
+ return bound(error.code);
455
+ return "unknown";
456
+ }
457
+ function renderMachineOverview(items, now) {
458
+ if (items.length === 0)
459
+ return "No machines found.";
460
+ return items.flatMap(({ machine, sessions, sessionsError }) => {
461
+ const counts = machineSessionCounts(machine, sessions, now);
462
+ const provider = machineProviderAvailability(machine);
463
+ const claude = `Claude ${counts.claude.running}/${counts.claude.total} running`;
464
+ const codex = `Codex ${counts.codex.running}/${counts.codex.total} running`;
465
+ const opencode = `OpenCode ${counts.opencode.running}/${counts.opencode.total} running`;
466
+ const providerCounts = `${opencode} · ${claude} · ${codex}`;
467
+ const header = `▾ ${machine.name} ${machine.state} ${provider.displayName} ${providerVerdict(provider)} ${providerCounts}`;
468
+ if (sessionsError !== undefined)
469
+ return [header, ` └─ AgentSessions unavailable (${sessionsError})`];
470
+ if (sessions.length === 0)
471
+ return [header, " └─ No AgentSessions"];
472
+ return [
473
+ header,
474
+ ...sessions.map((session, index) => {
475
+ const branch = index === sessions.length - 1 ? "└─" : "├─";
476
+ const actionability = classifySessionActionability({ session, machine, now });
477
+ return ` ${branch} ${agentDisplayName(session.agent)} ${session.name} ${displaySessionActionability(actionability)} ${session.id}`;
478
+ }),
479
+ ];
480
+ }).join("\n");
481
+ }
482
+ function capabilityRecord(snapshot) {
483
+ return Object.freeze({
484
+ schema_version: snapshot.schemaVersion,
485
+ subject_scope: snapshot.subjectScope,
486
+ ...(snapshot.subjectId === undefined ? {} : { subject_id: snapshot.subjectId }),
487
+ observed_at: snapshot.observedAt,
488
+ expires_at: snapshot.expiresAt,
489
+ etag: snapshot.etag,
490
+ capabilities: snapshot.capabilities.map((capability) => ({
491
+ id: capability.id,
492
+ availability: capability.availability,
493
+ interaction: capability.interaction,
494
+ mutation_class: capability.mutationClass,
495
+ surfaces: capability.surfaces,
496
+ required_permissions: capability.requiredPermissions,
497
+ ...(capability.reasonCode === undefined ? {} : { reason_code: capability.reasonCode }),
498
+ })),
499
+ });
500
+ }
501
+ export function preflightInvocation(parsed, now = Date.now()) {
502
+ assertRegisteredCliRoute(parsed);
503
+ switch (parsed.command) {
504
+ case "config":
505
+ rejectUnknownOptions(parsed, []);
506
+ if (parsed.operands.length !== 1 || parsed.operands[0] !== "get") {
507
+ throw unsupportedError("configuration mutation", "config_writes_not_implemented");
508
+ }
509
+ return;
510
+ case "observe":
511
+ rejectUnknownOptions(parsed, ["project"]);
512
+ if (parsed.operands.length !== 0)
513
+ throw usageError("observe accepts no operands.");
514
+ assertCanonicalUuid(stringOption(parsed, "project") ?? "", "Project ID");
515
+ return;
516
+ case "share": {
517
+ rejectUnknownOptions(parsed, ["project", "grant"]);
518
+ if (parsed.operands.length !== 0)
519
+ throw usageError("share accepts no operands.");
520
+ assertCanonicalUuid(stringOption(parsed, "project") ?? "", "Project ID");
521
+ const grant = stringOption(parsed, "grant");
522
+ if (grant !== undefined)
523
+ assertCanonicalUuid(grant, "Grant ID");
524
+ return;
525
+ }
526
+ case "capabilities": {
527
+ rejectUnknownOptions(parsed, ["scope", "resource-id"]);
528
+ if (parsed.operands.length !== 0)
529
+ throw usageError("capabilities accepts no operands.");
530
+ const scope = stringOption(parsed, "scope") ?? "account";
531
+ if (scope !== "account" && scope !== "machine" && scope !== "agent_session") {
532
+ throw usageError("Option --scope must be account, machine, or agent_session.");
533
+ }
534
+ const resourceId = stringOption(parsed, "resource-id");
535
+ if (scope === "account" && resourceId !== undefined) {
536
+ throw usageError("Option --resource-id is not valid for account scope.");
537
+ }
538
+ if (scope !== "account") {
539
+ // A resource-scoped query names a Machine or an AgentSession, and both
540
+ // are canonical UUIDs. Refusing the shape here keeps a typo from
541
+ // travelling to the server and coming back as a contract complaint.
542
+ assertCanonicalUuid(requireOption(parsed, "resource-id", `A ${scope} capability query is scoped to one resource.`), scope === "machine" ? "machine ID" : "AgentSession ID");
543
+ }
544
+ return;
545
+ }
546
+ case "machines":
547
+ preflightMachines(parsed);
548
+ return;
549
+ case "records":
550
+ rejectUnknownOptions(parsed, []);
551
+ if (parsed.operands.length !== 1 || parsed.operands[0] !== "list") {
552
+ throw usageError("records requires the list action.");
553
+ }
554
+ return;
555
+ case "authorizations":
556
+ rejectUnknownOptions(parsed, ["machine"]);
557
+ if (parsed.operands.length !== 1 || parsed.operands[0] !== "list") {
558
+ throw usageError("authorizations requires the list action.");
559
+ }
560
+ assertCanonicalUuid(requireOption(parsed, "machine", "Run `cuna machines list` to find a machine ID."), "machine ID");
561
+ return;
562
+ case "account":
563
+ case "workspace": {
564
+ rejectUnknownOptions(parsed, []);
565
+ const action = requireOperand(parsed.operands, 0, `${parsed.command} action`);
566
+ if (action !== "show" || parsed.operands.length !== 1) {
567
+ throw usageError(`${parsed.command} requires the show action.`);
568
+ }
569
+ return;
570
+ }
571
+ case "usage":
572
+ rejectUnknownOptions(parsed, []);
573
+ if (parsed.operands.length !== 1 || parsed.operands[0] !== "show") {
574
+ throw usageError("usage requires the show action.");
575
+ }
576
+ return;
577
+ case "api-keys": {
578
+ const action = requireOperand(parsed.operands, 0, "api-keys action");
579
+ if (action === "list") {
580
+ rejectUnknownOptions(parsed, []);
581
+ if (parsed.operands.length !== 1)
582
+ throw usageError("api-keys list accepts no operands.");
583
+ return;
584
+ }
585
+ if (action === "revoke") {
586
+ rejectUnknownOptions(parsed, ["yes"]);
587
+ if (parsed.operands.length !== 2)
588
+ throw usageError("api-keys revoke requires exactly one API key ID.");
589
+ requireConfirmation(parsed, "api-keys.revoke");
590
+ assertCanonicalUuid(requireOperand(parsed.operands, 1, "API key ID"), "API key ID");
591
+ return;
592
+ }
593
+ if (action === "create") {
594
+ rejectUnknownOptions(parsed, ["name", "expires-at", "yes"]);
595
+ if (parsed.operands.length !== 1)
596
+ throw usageError("api-keys create accepts no operands.");
597
+ requireConfirmation(parsed, "api-keys.create");
598
+ // Preflight and execution must read one clock; validating here with
599
+ // the wall clock while execution used the injected one let a fixed
600
+ // test date turn red the day the calendar caught up with it.
601
+ apiKeyCreateInput(parsed, now);
602
+ return;
603
+ }
604
+ throw usageError(`Unknown api-keys action ${action}.`);
605
+ }
606
+ case "agent-sessions":
607
+ preflightAgentSessions(parsed);
608
+ return;
609
+ case "executions":
610
+ preflightExecutions(parsed);
611
+ return;
612
+ case "agent": {
613
+ rejectUnknownOptions(parsed, ["agent-session", "yes"]);
614
+ if (parsed.operands.length !== 1 || parsed.operands[0] !== "logout") {
615
+ throw usageError("agent requires the logout action.");
616
+ }
617
+ requireConfirmation(parsed, "agent.logout");
618
+ assertCanonicalUuid(requireOption(parsed, "agent-session", "Run `cuna agent-sessions list --machine <id>` to find an AgentSession ID."), "AgentSession ID");
619
+ return;
620
+ }
621
+ case "login":
622
+ case "logout":
623
+ case "whoami":
624
+ rejectUnknownOptions(parsed, []);
625
+ if (parsed.operands.length !== 0)
626
+ throw usageError(`${parsed.command} accepts no operands.`);
627
+ return;
628
+ case "access":
629
+ rejectUnknownOptions(parsed, []);
630
+ if (parsed.operands.length !== 1 || parsed.operands[0] !== "status") {
631
+ throw usageError("access requires the status action.");
632
+ }
633
+ return;
634
+ case "signup":
635
+ rejectUnknownOptions(parsed, []);
636
+ if (parsed.operands.length !== 0)
637
+ throw usageError("signup accepts no operands.");
638
+ return;
639
+ case "claude":
640
+ case "codex":
641
+ case "opencode": {
642
+ preflightAgentJourneyInvocation(parsed);
643
+ return;
644
+ }
645
+ case "shell":
646
+ case "sync":
647
+ case "companion":
648
+ rejectUnknownOptions(parsed, []);
649
+ if (parsed.operands.length !== 0)
650
+ throw usageError(`${parsed.command} accepts no operands in this build.`);
651
+ return;
652
+ case "connect":
653
+ rejectUnknownOptions(parsed, []);
654
+ if (parsed.operands.length < 1 || parsed.operands.length > 4) {
655
+ throw usageError("connect requires one through four explicit AgentSession IDs.");
656
+ }
657
+ if (new Set(parsed.operands).size !== parsed.operands.length) {
658
+ throw usageError("connect requires distinct AgentSession IDs.");
659
+ }
660
+ for (const agentSessionId of parsed.operands)
661
+ assertCanonicalUuid(agentSessionId, "AgentSession ID");
662
+ return;
663
+ case "doctor":
664
+ rejectUnknownOptions(parsed, ["check-browser-login"]);
665
+ if (parsed.operands.length !== 0)
666
+ throw usageError("doctor accepts no operands.");
667
+ return;
668
+ case "self-test":
669
+ rejectUnknownOptions(parsed, ["offline"]);
670
+ if (parsed.operands.length !== 0)
671
+ throw usageError("self-test accepts no operands.");
672
+ if (!booleanOption(parsed, "offline")) {
673
+ throw usageError("self-test requires --offline in this release.", "Run `cuna self-test --offline --json`.");
674
+ }
675
+ return;
676
+ case "version":
677
+ rejectUnknownOptions(parsed, ["help", "version"]);
678
+ if (parsed.operands.length !== 0)
679
+ throw usageError("version accepts no operands.");
680
+ return;
681
+ case "help":
682
+ rejectUnknownOptions(parsed, ["help"]);
683
+ if (parsed.operands.length !== 0)
684
+ throw usageError("help accepts no operands.");
685
+ return;
686
+ default:
687
+ throw usageError(`Unknown command ${parsed.command ?? "<none>"}.`, "Run `cuna --help`.");
688
+ }
689
+ }
690
+ function preflightLiveUpdateSupervisor(parsed) {
691
+ rejectUnknownOptions(parsed, ["yes", "resume", "forget-unknown", "operation"]);
692
+ if (parsed.operands.length !== 2) {
693
+ throw usageError("machines live-update-supervisor requires exactly one machine ID.");
694
+ }
695
+ const chosen = ["yes", "resume", "forget-unknown"]
696
+ .filter((option) => booleanOption(parsed, option));
697
+ if (chosen.length > 1) {
698
+ throw usageError("machines live-update-supervisor accepts exactly one of --yes, --resume or --forget-unknown.", "Starting a new update, repeating the one this computer already recorded, and clearing that record are three different decisions. Read `cuna machines live-update-status MACHINE_ID` first; it sends nothing.");
699
+ }
700
+ const intent = chosen[0] === "forget-unknown"
701
+ ? "forget-unknown"
702
+ : chosen[0] === "resume" ? "resume" : "start";
703
+ // `--resume` is its own explicit decision and carries the identity it will
704
+ // re-send in its own name, so it is not additionally gated on `--yes`.
705
+ if (intent === "start")
706
+ requireConfirmation(parsed, "machines.live-update-supervisor");
707
+ assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
708
+ const operation = stringOption(parsed, "operation");
709
+ if (operation !== undefined) {
710
+ // Only the repeat can take an identity. Naming one alongside "start a new
711
+ // update" or "clear the record" would be two decisions in one flag.
712
+ if (intent !== "resume") {
713
+ throw usageError("machines live-update-supervisor accepts --operation only with --resume.", `--operation names an update to finish, which is what --resume does. To read one without sending anything, use \`cuna machines live-update-status MACHINE_ID --operation ${operation}\`.`);
714
+ }
715
+ assertCanonicalUuid(operation, "supervisor update operation ID");
716
+ }
717
+ return Object.freeze({ intent });
718
+ }
719
+ /**
720
+ * `machines live-update-status` — the read that recovers a lost answer.
721
+ *
722
+ * No confirmation, because it confirms nothing: the producer states that this
723
+ * route dispatches no installer, sends nothing to the Machine and changes no
724
+ * state. `--operation` is optional; without it the command reads the identity
725
+ * this computer recorded, which is the whole point of having recorded it.
726
+ */
727
+ function preflightLiveUpdateStatus(parsed) {
728
+ rejectUnknownOptions(parsed, ["operation"]);
729
+ if (parsed.operands.length !== 2) {
730
+ throw usageError("machines live-update-status requires exactly one machine ID.");
731
+ }
732
+ assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
733
+ const operation = stringOption(parsed, "operation");
734
+ if (operation !== undefined)
735
+ assertCanonicalUuid(operation, "supervisor update operation ID");
736
+ }
737
+ function preflightMachines(parsed) {
738
+ if (parsed.operands.length === 0) {
739
+ rejectUnknownOptions(parsed, []);
740
+ return;
741
+ }
742
+ const action = requireOperand(parsed.operands, 0, "machines action");
743
+ if (action === "list") {
744
+ rejectUnknownOptions(parsed, []);
745
+ if (parsed.operands.length !== 1)
746
+ throw usageError("machines list accepts no operands.");
747
+ return;
748
+ }
749
+ if (action === "create") {
750
+ rejectUnknownOptions(parsed, ["name", "agent", "vcpus", "memory-mib", "background", "yes", "idempotency-key"]);
751
+ if (parsed.operands.length !== 1)
752
+ throw usageError("machines create accepts no operands.");
753
+ requireConfirmation(parsed, "machines.create");
754
+ idempotencyKey(parsed);
755
+ const rawName = stringOption(parsed, "name");
756
+ if (rawName === undefined)
757
+ throw usageError("Option --name is required.");
758
+ const name = assertSafeDisplayText(rawName, "machine name");
759
+ if (name.length < 1 || name.length > 80)
760
+ throw usageError("Option --name must contain 1 through 80 characters.");
761
+ agentOption(parsed, false);
762
+ integerOption(parsed, "vcpus", 1, 8);
763
+ integerOption(parsed, "memory-mib", 512, 16_384);
764
+ return;
765
+ }
766
+ if (action === "update-supervisor") {
767
+ rejectUnknownOptions(parsed, ["yes"]);
768
+ if (parsed.operands.length !== 2)
769
+ throw usageError("machines update-supervisor requires exactly one machine ID.");
770
+ requireConfirmation(parsed, "machines.update-supervisor");
771
+ assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
772
+ return;
773
+ }
774
+ if (action === "live-update-supervisor") {
775
+ preflightLiveUpdateSupervisor(parsed);
776
+ return;
777
+ }
778
+ if (action === "live-update-status") {
779
+ preflightLiveUpdateStatus(parsed);
780
+ return;
781
+ }
782
+ if (action === "start" || action === "pause" || action === "resume" || action === "stop" || action === "delete") {
783
+ rejectUnknownOptions(parsed, ["yes"]);
784
+ if (parsed.operands.length !== 2)
785
+ throw usageError(`machines ${action} requires exactly one machine ID.`);
786
+ requireConfirmation(parsed, `machines.${action}`);
787
+ assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
788
+ return;
789
+ }
790
+ throw usageError(`Unknown machines action ${action}.`);
791
+ }
792
+ function preflightAgentSessions(parsed) {
793
+ const action = requireOperand(parsed.operands, 0, "agent-sessions action");
794
+ if (action === "list") {
795
+ rejectUnknownOptions(parsed, ["machine", "limit", "cursor"]);
796
+ if (parsed.operands.length !== 1)
797
+ throw usageError("agent-sessions list accepts no operands.");
798
+ assertMachineId(requireOption(parsed, "machine", "Run `cuna machines list` to find a machine ID."));
799
+ integerOption(parsed, "limit", 1, 100);
800
+ const cursor = stringOption(parsed, "cursor");
801
+ if (cursor !== undefined && (cursor.length > 512 || /[\p{Cc}\p{Cf}]/u.test(cursor))) {
802
+ throw usageError("Option --cursor is malformed.");
803
+ }
804
+ return;
805
+ }
806
+ if (action === "get") {
807
+ rejectUnknownOptions(parsed, []);
808
+ if (parsed.operands.length !== 2)
809
+ throw usageError("agent-sessions get requires exactly one AgentSession ID.");
810
+ assertCanonicalUuid(requireOperand(parsed.operands, 1, "AgentSession ID"), "AgentSession ID");
811
+ return;
812
+ }
813
+ if (action === "create") {
814
+ rejectUnknownOptions(parsed, [
815
+ "machine", "workspace-binding-id", "workspace-generation", "name", "agent", "cwd",
816
+ "auth-mode", "credential-binding", "yes", "idempotency-key",
817
+ ]);
818
+ if (parsed.operands.length !== 1)
819
+ throw usageError("agent-sessions create accepts no operands.");
820
+ requireConfirmation(parsed, "agent-sessions.create");
821
+ assertMachineId(requireOption(parsed, "machine", "Run `cuna machines list` to find a machine ID."));
822
+ // Both workspace options are optional here: absent, they are resolved from
823
+ // the folder's own binding at execution. Preflight still validates the
824
+ // shape of whatever WAS supplied, so a malformed value is refused before
825
+ // any network work — it just no longer refuses their absence.
826
+ const givenBinding = stringOption(parsed, "workspace-binding-id");
827
+ if (givenBinding !== undefined)
828
+ assertCanonicalUuid(givenBinding, "workspace binding ID");
829
+ integerOption(parsed, "workspace-generation", 1, Number.MAX_SAFE_INTEGER);
830
+ const agent = agentOption(parsed, true);
831
+ if (agent === undefined)
832
+ throw usageError("Option --agent is required.");
833
+ const cwd = assertSafeDisplayText(stringOption(parsed, "cwd") ?? "/workspace", "workspace path");
834
+ if (!cwd.startsWith("/workspace") || cwd.split("/").includes("..") || cwd.length > 1024) {
835
+ throw usageError("Option --cwd must be a safe absolute path inside /workspace.");
836
+ }
837
+ const name = stringOption(parsed, "name");
838
+ if (name !== undefined && (assertSafeDisplayText(name, "AgentSession name").length < 1 || name.length > 80)) {
839
+ throw usageError("Option --name must contain 1 through 80 characters.");
840
+ }
841
+ const binding = stringOption(parsed, "credential-binding");
842
+ const rawAuthMode = stringOption(parsed, "auth-mode");
843
+ if (agent === "opencode" && (rawAuthMode === "credential_binding" || binding !== undefined)) {
844
+ throw usageError("OpenCode supports interactive_login only; credential bindings are not accepted.", "Use OpenCode's interactive provider flow and omit --credential-binding.");
845
+ }
846
+ normalizedAgentSessionAuthMode(rawAuthMode, binding);
847
+ if (binding !== undefined)
848
+ assertPublicId(binding, "credential binding ID");
849
+ idempotencyKey(parsed);
850
+ return;
851
+ }
852
+ if (action === "terminate" || action === "rename") {
853
+ rejectUnknownOptions(parsed, action === "rename" ? ["name", "yes"] : ["yes"]);
854
+ if (parsed.operands.length !== 2)
855
+ throw usageError(`agent-sessions ${action} requires exactly one AgentSession ID.`);
856
+ requireConfirmation(parsed, `agent-sessions.${action}`);
857
+ assertCanonicalUuid(requireOperand(parsed.operands, 1, "AgentSession ID"), "AgentSession ID");
858
+ if (action === "rename") {
859
+ const name = stringOption(parsed, "name");
860
+ if (name === undefined || assertSafeDisplayText(name, "AgentSession name").length < 1 || name.length > 80) {
861
+ throw usageError("Option --name must contain 1 through 80 characters.");
862
+ }
863
+ }
864
+ return;
865
+ }
866
+ if (action === "attach") {
867
+ rejectUnknownOptions(parsed, []);
868
+ if (parsed.operands.length !== 2)
869
+ throw usageError("agent-sessions attach requires exactly one AgentSession ID.");
870
+ assertCanonicalUuid(requireOperand(parsed.operands, 1, "AgentSession ID"), "AgentSession ID");
871
+ return;
872
+ }
873
+ throw usageError(`Unknown agent-sessions action ${action}.`);
874
+ }
875
+ export async function executeCommand(context) {
876
+ const { parsed, config, client } = context;
877
+ switch (parsed.command) {
878
+ case "config": {
879
+ rejectUnknownOptions(parsed, []);
880
+ if (parsed.operands.length !== 1 || parsed.operands[0] !== "get") {
881
+ throw unsupportedError("configuration mutation", "config_writes_not_implemented");
882
+ }
883
+ const data = publicConfig(config);
884
+ return Object.freeze({ command: "config.get", data, human: renderScalarRecord(data) });
885
+ }
886
+ case "capabilities": {
887
+ rejectUnknownOptions(parsed, ["scope", "resource-id"]);
888
+ requireCredential(context);
889
+ const scope = stringOption(parsed, "scope") ?? "account";
890
+ if (scope !== "account" && scope !== "machine" && scope !== "agent_session") {
891
+ throw usageError("Option --scope must be account, machine, or agent_session.");
892
+ }
893
+ const resourceId = stringOption(parsed, "resource-id");
894
+ if (scope !== "account" && resourceId === undefined) {
895
+ throw usageError("Option --resource-id is required for a resource-scoped capability query.");
896
+ }
897
+ const snapshot = await client.discoverCapabilities(scope, resourceId);
898
+ const data = capabilityRecord(snapshot);
899
+ return Object.freeze({
900
+ command: "capabilities",
901
+ data,
902
+ human: snapshot.capabilities.length === 0
903
+ ? "No capabilities were advertised for this context."
904
+ : snapshot.capabilities
905
+ .map((capability) => `${capability.id}\t${capability.availability}\t${capability.interaction}`)
906
+ .join("\n"),
907
+ });
908
+ }
909
+ case "machines":
910
+ return executeMachines(context);
911
+ case "records": {
912
+ requireCredential(context);
913
+ await requireCapability({
914
+ client,
915
+ scope: "account",
916
+ capabilityId: "records.list",
917
+ now: context.capabilityClock ?? context.now,
918
+ allowedInteractions: ["read_only"],
919
+ });
920
+ const records = await client.listRecords();
921
+ const data = Object.freeze({
922
+ items: records.map((record) => Object.freeze({
923
+ id: record.id,
924
+ machine_id: record.machineId,
925
+ kind: record.kind,
926
+ summary: record.summary,
927
+ detail: record.detail,
928
+ created_at: record.createdAt,
929
+ })),
930
+ });
931
+ return Object.freeze({
932
+ command: "records.list",
933
+ data,
934
+ human: records.length === 0
935
+ ? "No records found."
936
+ : records.map((record) => `${record.createdAt}\t${record.machineId}\t${record.kind}\t${record.summary}`).join("\n"),
937
+ });
938
+ }
939
+ case "authorizations": {
940
+ requireCredential(context);
941
+ const machineId = assertCanonicalUuid(requireOption(parsed, "machine", "Run `cuna machines list` to find a machine ID."), "machine ID");
942
+ await requireCapability({
943
+ client,
944
+ scope: "machine",
945
+ resourceId: machineId,
946
+ capabilityId: "authorizations.list",
947
+ now: context.capabilityClock ?? context.now,
948
+ allowedInteractions: ["read_only"],
949
+ });
950
+ const authorizations = await client.listAuthorizations(machineId);
951
+ const data = Object.freeze({
952
+ machine_id: machineId,
953
+ ...authorizations,
954
+ });
955
+ const lines = authorizations.secret_configuration.flatMap((configuration) => {
956
+ const binding = `secret:${configuration.secret_id}`;
957
+ const items = [
958
+ ...configuration.environment.map((item) => `environment\t${item.name}\t${JSON.stringify(item.value_template)}\t${binding}`),
959
+ ...configuration.egress_rules.map((item) => `egress:${item.action}\t${item.host_pattern}\tpath:${JSON.stringify(item.path_pattern) ?? "unspecified"}\t${item.name}\t${JSON.stringify(item.value_template)}\t${binding}`),
960
+ ...configuration.files.map((item) => `file\t${item.path}\t${JSON.stringify(item.value_template)}\t${binding}`),
961
+ ];
962
+ return items.length === 0 ? [`${binding}\tNo injection entries configured.`] : items;
963
+ });
964
+ return Object.freeze({
965
+ command: "authorizations.list",
966
+ data,
967
+ human: `Configuration revision ${authorizations.revision}\n${lines.length === 0
968
+ ? "No inline secret configuration is set for this machine."
969
+ : lines.join("\n")}`,
970
+ });
971
+ }
972
+ case "account": {
973
+ requireCredential(context);
974
+ const identity = await client.getIdentity();
975
+ const data = Object.freeze({ id: identity.id, email: identity.email });
976
+ return Object.freeze({ command: "account.show", data, human: `${identity.id}\t${identity.email}` });
977
+ }
978
+ case "workspace": {
979
+ requireCredential(context);
980
+ const identity = await client.getIdentity();
981
+ const data = Object.freeze({
982
+ assigned: identity.workspaceAssigned,
983
+ ...(identity.waitlistPosition === undefined
984
+ ? {}
985
+ : { waitlist_position: identity.waitlistPosition }),
986
+ });
987
+ return Object.freeze({
988
+ command: "workspace.show",
989
+ data,
990
+ human: identity.workspaceAssigned
991
+ ? "A Cuna workspace is assigned to this account."
992
+ : `No Cuna workspace is assigned. Waitlist position: ${identity.waitlistPosition ?? "unknown"}.`,
993
+ });
994
+ }
995
+ case "usage": {
996
+ requireCredential(context);
997
+ const identity = await client.getIdentity();
998
+ if (identity.workspaceUsage === undefined) {
999
+ // The refusal lives here, where the figure is read, and nowhere else.
1000
+ // A usage payload this build does not recognise stops this one command;
1001
+ // every other command still gets its identity.
1002
+ if (identity.workspaceUsageProblem !== undefined) {
1003
+ throw new CunaError({
1004
+ code: "cuna.remote.malformed_response",
1005
+ message: "Cuna reported spend in a shape this CLI does not accept.",
1006
+ exitCode: EXIT_CODES.remote,
1007
+ hint: "No figure is shown rather than a wrong one. Every other command is unaffected. " +
1008
+ "Run `cuna version --json` and report it with the details above at " +
1009
+ "https://github.com/Cuna-Labs/cuna-cli/issues.",
1010
+ details: { reason: "workspace_usage_off_contract", detail: identity.workspaceUsageProblem },
1011
+ });
1012
+ }
1013
+ throw unsupportedError("workspace usage", "workspace_usage_unavailable");
1014
+ }
1015
+ const usage = identity.workspaceUsage;
1016
+ const data = Object.freeze({
1017
+ estimated_spend_usd: usage.estimatedSpendUsd,
1018
+ estimated_spend_is_lower_bound: usage.estimatedSpendIsLowerBound,
1019
+ balance_status: usage.balanceStatus,
1020
+ balance_usd: usage.balanceUsd,
1021
+ ...(usage.balanceUnavailableReason === undefined
1022
+ ? {}
1023
+ : { balance_unavailable_reason: usage.balanceUnavailableReason }),
1024
+ note: usage.note,
1025
+ });
1026
+ // "At least" is the whole claim and the sentence carries it. A balance is
1027
+ // printed only when the producer says it read one: an unavailable balance
1028
+ // is not zero.
1029
+ return Object.freeze({
1030
+ command: "usage.show",
1031
+ data,
1032
+ human: [
1033
+ `At least $${usage.estimatedSpendUsd.toFixed(2)} spent.`,
1034
+ usage.balanceStatus === "available" && usage.balanceUsd !== null
1035
+ ? `Balance $${usage.balanceUsd.toFixed(2)}.`
1036
+ : "No balance available.",
1037
+ usage.note,
1038
+ ].join(" "),
1039
+ });
1040
+ }
1041
+ case "api-keys": {
1042
+ requireCredential(context);
1043
+ const action = requireOperand(parsed.operands, 0, "api-keys action");
1044
+ if (context.credentialMode !== "interactive") {
1045
+ throw new CunaError({
1046
+ code: "cuna.auth.interactive_required",
1047
+ message: "API-key management requires an interactive Cuna session.",
1048
+ exitCode: EXIT_CODES.auth,
1049
+ hint: "Unset the automation credential, run `cuna login`, then repeat this command.",
1050
+ });
1051
+ }
1052
+ await requireCapability({ client, scope: "account", capabilityId: "api_keys.manage", now: context.capabilityClock ?? context.now });
1053
+ if (action === "list") {
1054
+ const keys = await client.listApiKeys();
1055
+ const data = Object.freeze({
1056
+ items: keys.map((key) => Object.freeze({
1057
+ id: key.id,
1058
+ name: key.name,
1059
+ prefix: key.prefix,
1060
+ last_four: key.lastFour,
1061
+ created_at: key.createdAt,
1062
+ expires_at: key.expiresAt,
1063
+ last_used_at: key.lastUsedAt,
1064
+ revoked_at: key.revokedAt,
1065
+ })),
1066
+ });
1067
+ return Object.freeze({
1068
+ command: "api-keys.list",
1069
+ data,
1070
+ human: keys.length === 0
1071
+ ? "No API keys found."
1072
+ : keys.map((key) => `${key.id}\t${key.name}\t${key.prefix}…${key.lastFour}\t${apiKeyStatusLabel(key, context.now)}`).join("\n"),
1073
+ });
1074
+ }
1075
+ if (action === "revoke") {
1076
+ const id = assertCanonicalUuid(requireOperand(parsed.operands, 1, "API key ID"), "API key ID");
1077
+ await client.revokeApiKey(id);
1078
+ const observed = (await client.listApiKeys()).find((key) => key.id === id);
1079
+ if (observed !== undefined && observed.revokedAt === null) {
1080
+ postconditionUnverified("API-key revocation", { api_key_id: id, observed_state: "active" });
1081
+ }
1082
+ return Object.freeze({
1083
+ command: "api-keys.revoke",
1084
+ data: Object.freeze({ id, revoked: true }),
1085
+ human: `Revoked API key ${id}.`,
1086
+ });
1087
+ }
1088
+ if (action === "create") {
1089
+ const input = apiKeyCreateInput(parsed, context.now);
1090
+ const prior = await client.listApiKeys();
1091
+ const priorIds = new Set(prior.map((key) => key.id));
1092
+ const operationKey = `cuna-api-key-create-${randomUUID()}`;
1093
+ let created;
1094
+ let createFailure;
1095
+ try {
1096
+ created = await client.createApiKey(input, operationKey);
1097
+ }
1098
+ catch (error) {
1099
+ createFailure = error;
1100
+ }
1101
+ if (created?.idempotencyReplayed === true) {
1102
+ createFailure = new Error("API-key creation replay did not return one-time secret material.");
1103
+ }
1104
+ if (createFailure !== undefined) {
1105
+ // Reuse the same operation authority once. This can recover the exact
1106
+ // committed ID after a timeout or malformed first response without
1107
+ // creating a sibling key. The replay secret, if any, is never used:
1108
+ // after an uncertain response the only safe outcome is reconciliation.
1109
+ let replayed;
1110
+ try {
1111
+ replayed = await client.createApiKey(input, operationKey);
1112
+ }
1113
+ catch { /* post-list remains authoritative */ }
1114
+ const observedAt = context.now + 120_000;
1115
+ const candidates = (await client.listApiKeys()).filter((key) => {
1116
+ const createdAt = Date.parse(key.createdAt);
1117
+ return !priorIds.has(key.id) && key.name === input.name && key.revokedAt === null &&
1118
+ key.expiresAt === (input.expiresAt ?? null) && Number.isFinite(createdAt) &&
1119
+ createdAt >= context.now - 5_000 && createdAt <= observedAt + 5_000;
1120
+ });
1121
+ if (candidates.length === 1) {
1122
+ await client.revokeApiKey(candidates[0].id);
1123
+ const remaining = (await client.listApiKeys()).filter((key) => key.id === candidates[0].id && key.revokedAt === null);
1124
+ if (remaining.length !== 0) {
1125
+ throw new CunaError({
1126
+ code: "cuna.api_keys.create_cleanup_unverified",
1127
+ message: "Cuna could not verify cleanup of an API key created during an uncertain response.",
1128
+ exitCode: EXIT_CODES.conflict,
1129
+ hint: "Revoke the listed API key ID in the Cuna dashboard before retrying.",
1130
+ details: { manual_cleanup_ids: [candidates[0].id], idempotency_key: operationKey },
1131
+ cause: createFailure,
1132
+ });
1133
+ }
1134
+ throw new CunaError({
1135
+ code: "cuna.api_keys.create_secret_unobserved",
1136
+ message: "Cuna created the API key, but its one-time secret response was not observed; the CLI revoked it safely.",
1137
+ exitCode: EXIT_CODES.network,
1138
+ hint: "Retry creation with a new name. The unobserved key was revoked and cannot authenticate.",
1139
+ retryable: true,
1140
+ details: { api_key_id: candidates[0].id, reconciled: true, revoked: true, cleanup_verified: true, idempotency_replayed: replayed?.idempotencyReplayed ?? null },
1141
+ cause: createFailure,
1142
+ });
1143
+ }
1144
+ if (candidates.length === 0) {
1145
+ throw new CunaError({
1146
+ code: "cuna.api_keys.create_failed_no_commit",
1147
+ message: "API-key creation failed and reconciliation found no newly created key.",
1148
+ exitCode: EXIT_CODES.network,
1149
+ hint: "No cleanup is required. Retry creation with the same name when connectivity is stable.",
1150
+ retryable: true,
1151
+ details: { reconciled: true, created: false, idempotency_key: operationKey },
1152
+ cause: createFailure,
1153
+ });
1154
+ }
1155
+ throw new CunaError({
1156
+ code: "cuna.api_keys.create_reconciliation_ambiguous",
1157
+ message: "API-key creation failed and reconciliation found multiple possible new keys.",
1158
+ exitCode: EXIT_CODES.conflict,
1159
+ hint: "Review and revoke the listed API key IDs in the Cuna dashboard before retrying.",
1160
+ details: { reconciled: false, manual_cleanup_ids: candidates.map((key) => key.id), idempotency_key: operationKey },
1161
+ cause: createFailure,
1162
+ });
1163
+ }
1164
+ if (created === undefined || created.idempotencyReplayed)
1165
+ throw new Error("API-key creation reconciliation invariant failed.");
1166
+ const data = Object.freeze({
1167
+ id: created.id,
1168
+ name: created.name,
1169
+ prefix: created.prefix,
1170
+ last_four: created.lastFour,
1171
+ created_at: created.createdAt,
1172
+ expires_at: created.expiresAt,
1173
+ key: created.key,
1174
+ });
1175
+ return Object.freeze({
1176
+ command: "api-keys.create",
1177
+ data,
1178
+ human: `Created API key ${created.name}. Copy it now; Cuna will not show it again.\n${created.key}`,
1179
+ });
1180
+ }
1181
+ throw usageError(`Unknown api-keys action ${action}.`);
1182
+ }
1183
+ case "agent-sessions":
1184
+ return executeAgentSessions(context);
1185
+ case "executions":
1186
+ return executeExecutions(context);
1187
+ case "agent": {
1188
+ requireCredential(context);
1189
+ rejectUnknownOptions(parsed, ["agent-session", "yes"]);
1190
+ if (parsed.operands.length !== 1 || parsed.operands[0] !== "logout") {
1191
+ throw usageError("agent requires the logout action.");
1192
+ }
1193
+ requireConfirmation(parsed, "agent.logout");
1194
+ const id = assertCanonicalUuid(requireOption(parsed, "agent-session", "Run `cuna agent-sessions list --machine <id>` to find an AgentSession ID."), "AgentSession ID");
1195
+ const session = await client.getAgentSession(id);
1196
+ if (session.processEpoch === undefined ||
1197
+ session.authMode !== "interactive_login" ||
1198
+ (session.agent !== "claude-code" && session.agent !== "codex")) {
1199
+ throw new CunaError({
1200
+ code: "cuna.agent.auth_logout_unavailable",
1201
+ message: "Provider sign-out is unavailable for this AgentSession.",
1202
+ exitCode: EXIT_CODES.policy,
1203
+ hint: "Select a running Claude Code or Codex AgentSession using interactive sign-in.",
1204
+ });
1205
+ }
1206
+ await requireCapability({
1207
+ client,
1208
+ scope: "agent_session",
1209
+ resourceId: id,
1210
+ capabilityId: "agent_sessions.auth_logout",
1211
+ now: context.capabilityClock ?? context.now,
1212
+ });
1213
+ const receipt = await client.logoutAgentSessionAuth(id, session.processEpoch);
1214
+ if (receipt.agentSessionId !== session.id ||
1215
+ receipt.processEpoch !== session.processEpoch ||
1216
+ receipt.authMode !== session.authMode ||
1217
+ receipt.agent !== session.agent) {
1218
+ throw new CunaError({
1219
+ code: "cuna.remote.malformed_response",
1220
+ message: "Cuna returned a provider sign-out receipt for another AgentSession authority.",
1221
+ exitCode: EXIT_CODES.remote,
1222
+ hint: OFF_CONTRACT_RESPONSE_HINT,
1223
+ details: {
1224
+ operation: "POST /v1/agent-sessions/{id}/agent-auth/logout",
1225
+ // The first field that disagreed, not all four: a list of every
1226
+ // compared field says nothing about which one was wrong.
1227
+ field: receipt.agentSessionId !== session.id
1228
+ ? "agent_session_id"
1229
+ : receipt.processEpoch !== session.processEpoch
1230
+ ? "process_epoch"
1231
+ : receipt.authMode !== session.authMode
1232
+ ? "auth_mode"
1233
+ : "agent",
1234
+ predicate: "matches_requested_resource",
1235
+ },
1236
+ });
1237
+ }
1238
+ const observedAuth = await client.getAgentSessionAuth(id);
1239
+ const observationIsAboutThisSession = observedAuth.agentSessionId === id &&
1240
+ observedAuth.agent === session.agent &&
1241
+ observedAuth.processEpoch === session.processEpoch;
1242
+ // Absence of evidence is not a contradiction. Some providers abstain from
1243
+ // reporting a login state at all — the Edge does exactly that for Codex,
1244
+ // because a successful `codex login status` proves credentials exist and
1245
+ // not that the account would be accepted. That abstention arrives as
1246
+ // `state: "unavailable"`, and treating it as a failed postcondition
1247
+ // turned a sign-out the server had CONFIRMED into an error the person
1248
+ // could do nothing about. `postconditionUnverified` is reserved, by its
1249
+ // own contract above, for an observation that CONTRADICTS the write.
1250
+ const providerAbstains = observedAuth.state === "unavailable";
1251
+ if (!observationIsAboutThisSession ||
1252
+ (!providerAbstains && observedAuth.state !== "login_required")) {
1253
+ postconditionUnverified("AgentSession provider logout", {
1254
+ agent_session_id: id,
1255
+ observed_state: observedAuth.state,
1256
+ });
1257
+ }
1258
+ return Object.freeze({
1259
+ command: "agent.logout",
1260
+ data: Object.freeze({
1261
+ observation_id: receipt.observationId,
1262
+ agent_session_id: receipt.agentSessionId,
1263
+ process_epoch: receipt.processEpoch,
1264
+ auth_mode: receipt.authMode,
1265
+ agent: receipt.agent,
1266
+ agent_version: receipt.agentVersion,
1267
+ adapter_version: receipt.adapterVersion,
1268
+ observed_at: receipt.observedAt,
1269
+ outcome: receipt.outcome,
1270
+ // Say which of the two happened. The sign-out is confirmed either
1271
+ // way; only one of them was independently observed afterwards.
1272
+ post_state: providerAbstains ? "unobserved" : "login_required",
1273
+ }),
1274
+ human: providerAbstains
1275
+ ? `Signed out ${session.agent} in AgentSession ${session.id}. ${agentDisplayName(session.agent)} does not report a login state, so this was not independently re-checked.`
1276
+ : `Signed out ${session.agent} in AgentSession ${session.id}.`,
1277
+ });
1278
+ }
1279
+ case "signup":
1280
+ case "login":
1281
+ case "logout":
1282
+ case "whoami":
1283
+ case "access":
1284
+ throw unsupportedError("browser authentication", "browser_auth_dispatch_unavailable");
1285
+ case "claude":
1286
+ case "codex":
1287
+ case "opencode":
1288
+ case "connect":
1289
+ // Public process dispatch is owned by runCli, which composes exact attach
1290
+ // and the automatic journey before this generic command dispatcher. A
1291
+ // direct executeCommand call has no TTY, sync lifecycle or credential
1292
+ // composition authority, so it must fail closed without claiming that
1293
+ // the shipped runtime itself is absent.
1294
+ throw unsupportedError("terminal workspace", "run_cli_composition_required");
1295
+ case "shell":
1296
+ throw unsupportedError("terminal workspace", "terminal_runtime_unavailable");
1297
+ case "sync":
1298
+ throw unsupportedError("workspace synchronization", "workspace_sync_runtime_unavailable");
1299
+ case "companion":
1300
+ throw unsupportedError("local companion", "local_companion_unavailable");
1301
+ case "doctor": {
1302
+ rejectUnknownOptions(parsed, ["check-browser-login"]);
1303
+ // `doctor` reads no credential, so an unusable one must not stop it —
1304
+ // that is the whole reason the refusal moved out of `resolveConfig`. It
1305
+ // reports the state instead, because a diagnostic that survives a broken
1306
+ // environment and then says nothing about it is no better than dying.
1307
+ const features = context.runtimeFeatures ?? INITIAL_RUNTIME_GATES;
1308
+ const data = Object.freeze({
1309
+ platform: process.platform,
1310
+ node: process.version,
1311
+ environment_credential: environmentCredentialState(config),
1312
+ environment_credential_variable: config.apiKeyVariable ?? null,
1313
+ runtime_features: features,
1314
+ });
1315
+ // `doctor` is the command the help text sends a stuck user to first, and
1316
+ // it answered with a JSON dump on a TTY. Every field the record carries is
1317
+ // still here; each feature now states its implementation AND the reason
1318
+ // code that names the prerequisite, on the line that reports it.
1319
+ return Object.freeze({
1320
+ command: "doctor",
1321
+ data,
1322
+ human: [
1323
+ `platform\t${data.platform}`,
1324
+ `node\t${data.node}`,
1325
+ `environment_credential\t${data.environment_credential}`,
1326
+ `environment_credential_variable\t${data.environment_credential_variable ?? "null"}`,
1327
+ "runtime_features",
1328
+ ...features.map((gate) => ` ${gate.feature}\t${gate.implementation}\t${gate.reason}`),
1329
+ ].join("\n"),
1330
+ });
1331
+ }
1332
+ case "self-test": {
1333
+ rejectUnknownOptions(parsed, ["offline"]);
1334
+ if (parsed.operands.length !== 0)
1335
+ throw usageError("self-test accepts no operands.");
1336
+ if (!booleanOption(parsed, "offline")) {
1337
+ throw usageError("self-test requires --offline in this release.", "Run `cuna self-test --offline --json`.");
1338
+ }
1339
+ const runtimeSupport = evaluateRuntimeSupport({
1340
+ nodeVersion: process.versions.node,
1341
+ platform: process.platform,
1342
+ architecture: process.arch,
1343
+ });
1344
+ const buildDigest = await packageBuildDigest();
1345
+ const virtualTerminal = await verifyVirtualTerminalInterop();
1346
+ // `canonical_api_origin` used to live in `checks` as
1347
+ // `config.baseUrl === DEFAULT_BASE_URL || config.developmentProfile`.
1348
+ // That condition cannot be false: `normalizeBaseUrl` (config/config.ts)
1349
+ // returns `DEFAULT_BASE_URL` or throws unless a development profile is
1350
+ // active, so the check restated its own precondition. It is now reported
1351
+ // as what it always was — a configuration fact, not an integrity gate —
1352
+ // and `apiOriginIsCanonical` can and does read `false`.
1353
+ const checks = Object.freeze({
1354
+ node_runtime: runtimeSupport.nodeRuntime,
1355
+ supported_platform: runtimeSupport.platform,
1356
+ supported_architecture: runtimeSupport.architecture,
1357
+ package_identity: /^[0-9a-f]{64}$/u.test(buildDigest),
1358
+ virtual_terminal: virtualTerminal,
1359
+ network_requests: 0,
1360
+ });
1361
+ const ok = Object.values(checks).every((value) => value === true || value === 0);
1362
+ const data = Object.freeze({
1363
+ ok,
1364
+ mode: "offline",
1365
+ // `ok` answers one question: is the installed artifact intact and
1366
+ // admissible on this host? It said nothing about the six runtime
1367
+ // feature gates `doctor` reports, every one of which is currently
1368
+ // `unsupported` — so "Offline self-test passed." read as a verdict on
1369
+ // the product while covering only the installation.
1370
+ scope: "installation_integrity",
1371
+ notChecked: Object.freeze([
1372
+ "runtime_features",
1373
+ "server_contract",
1374
+ "credential_state",
1375
+ ]),
1376
+ version: CLI_VERSION,
1377
+ buildDigest,
1378
+ platform: process.platform,
1379
+ architecture: process.arch,
1380
+ apiOrigin: config.baseUrl,
1381
+ apiOriginSource: config.baseUrlSource,
1382
+ apiOriginIsCanonical: config.baseUrl === DEFAULT_BASE_URL,
1383
+ updateChannel: ARTIFACT_CHANNEL,
1384
+ artifactChannel: ARTIFACT_CHANNEL,
1385
+ protocolRange: PROTOCOL_RANGE,
1386
+ checks,
1387
+ });
1388
+ if (!ok) {
1389
+ throw new CunaError({
1390
+ code: "cuna.self_test.failed",
1391
+ hint: `The installed CLI does not match its own build record. Reinstall the local Cuna .tgz package, and report it at ${SUPPORT_URL} if it recurs.`,
1392
+ message: "The installed Cuna CLI failed an offline integrity check.",
1393
+ exitCode: EXIT_CODES.internal,
1394
+ details: {
1395
+ failed_checks: Object.entries(checks)
1396
+ .filter(([, value]) => value !== true && value !== 0)
1397
+ .map(([name]) => name)
1398
+ .join(","),
1399
+ },
1400
+ });
1401
+ }
1402
+ return Object.freeze({
1403
+ command: "self-test",
1404
+ data,
1405
+ human: "Offline self-test passed: installation integrity only. It does not check runtime feature availability — run `cuna doctor` for that.",
1406
+ });
1407
+ }
1408
+ default:
1409
+ throw usageError(`Unknown command ${parsed.command ?? "<none>"}.`, "Run `cuna --help`.");
1410
+ }
1411
+ }
1412
+ async function verifyVirtualTerminalInterop() {
1413
+ let viewport;
1414
+ try {
1415
+ const [{ ViewportRegistry }, { XtermViewportAdapter }] = await Promise.all([
1416
+ import("../terminal/viewport.js"),
1417
+ import("../terminal/xterm-vte.js"),
1418
+ ]);
1419
+ const registry = new ViewportRegistry();
1420
+ viewport = new XtermViewportAdapter({
1421
+ tabId: "offline-self-test",
1422
+ binding: {
1423
+ userId: "offline",
1424
+ machineId: "offline",
1425
+ agentSessionId: "offline",
1426
+ processEpoch: "offline",
1427
+ fencingGeneration: 1,
1428
+ },
1429
+ columns: 20,
1430
+ rows: 2,
1431
+ scrollback: 0,
1432
+ registry,
1433
+ });
1434
+ const snapshot = await viewport.write(new TextEncoder().encode("cuna"), 1n, 1n);
1435
+ return snapshot.cells[0] === "cuna";
1436
+ }
1437
+ catch {
1438
+ return false;
1439
+ }
1440
+ finally {
1441
+ viewport?.dispose();
1442
+ }
1443
+ }
1444
+ /**
1445
+ * Where this installation keeps its note about an in-place update it dispatched
1446
+ * and never saw settle.
1447
+ *
1448
+ * The adapter is optional on `CommandContext`, and its absence is a refusal
1449
+ * rather than a silent fall-through: dispatching a mutation this command could
1450
+ * not record would leave the next invocation free to repeat it.
1451
+ */
1452
+ function liveUpdateNotesFor(context, platform) {
1453
+ // Two conditions, one refusal. The adapter may be absent entirely, or it may
1454
+ // predate the exclusive-create primitive the reservation is built on — and a
1455
+ // store that can only read-then-write is not a reservation at all.
1456
+ if (platform === undefined || typeof platform.createExclusiveConfig !== "function") {
1457
+ throw new CunaError({
1458
+ code: "cuna.machine.live_supervisor_update_unrecordable",
1459
+ message: "Cuna cannot record an in-place supervisor update on this host.",
1460
+ exitCode: EXIT_CODES.internal,
1461
+ hint: "Nothing was sent. This command reserves a local record before it dispatches, so that a second invocation cannot repeat an update whose outcome is unknown, and it will not start one it cannot reserve.",
1462
+ details: { reason: platform === undefined ? "no_platform_adapter" : "no_exclusive_reservation" },
1463
+ });
1464
+ }
1465
+ // Scoped by the API origin alone. The profile is deliberately absent: the
1466
+ // suppression protects a Machine, and selecting a different local profile
1467
+ // against the same API must not clear it.
1468
+ return liveSupervisorUpdateNotes(platform, { baseUrl: context.config.baseUrl });
1469
+ }
1470
+ function outstandingLiveUpdate(machineId, operationId, dispatchedAt) {
1471
+ return new CunaError({
1472
+ code: "cuna.machine.live_supervisor_update_outcome_unknown",
1473
+ message: `An in-place supervisor update for Machine ${machineId} dispatched at ${dispatchedAt} has no known outcome on this computer.`,
1474
+ exitCode: EXIT_CODES.conflict,
1475
+ // The authoritative read comes first now, because one exists. It sends
1476
+ // nothing to the Machine and is the only thing that can say what this
1477
+ // operation did. Starting a NEW update is still absent from this list, and
1478
+ // so is switching profiles: the record covers this Machine on this API for
1479
+ // every profile.
1480
+ hint: `Read it with \`cuna machines live-update-status ${machineId}\`, which sends nothing to the Machine. If it reports next_action repeat_same_operation, resolve it with \`cuna machines live-update-supervisor ${machineId} --resume\`, which re-sends this same operation ${operationId} and never rotates control a second time. Cuna will not start a different update to find out, and another profile on this computer will not either.`,
1481
+ retryable: false,
1482
+ details: {
1483
+ machine_id: machineId,
1484
+ operation_id: operationId,
1485
+ dispatched_at: dispatchedAt,
1486
+ outcome: "unknown",
1487
+ },
1488
+ });
1489
+ }
1490
+ /**
1491
+ * What the producer's own record says about an operation whose answer was lost.
1492
+ *
1493
+ * Called only from the refusal path, and deliberately best-effort: it is one
1494
+ * bounded read that dispatches no installer, and a read that itself fails must
1495
+ * never replace the refusal the caller is owed with a story about a second
1496
+ * request. `undefined` means "this CLI could not ask", which is not "nothing
1497
+ * happened".
1498
+ */
1499
+ async function reconcileLiveUpdateOperation(client, machineId, operationId) {
1500
+ const answer = await readLiveUpdateOperationAnswer(client, machineId, operationId);
1501
+ return answer.state === "read" ? answer.operation : undefined;
1502
+ }
1503
+ async function readLiveUpdateOperationAnswer(client, machineId, operationId) {
1504
+ try {
1505
+ return Object.freeze({
1506
+ state: "read",
1507
+ operation: await client.readMachineSupervisorInPlaceUpdate(machineId, operationId),
1508
+ });
1509
+ }
1510
+ catch (error) {
1511
+ const reason = error instanceof CunaError ? error.details?.["reason"] : undefined;
1512
+ if (reason === "resource_not_found")
1513
+ return Object.freeze({ state: "absent" });
1514
+ return Object.freeze({ state: "unavailable" });
1515
+ }
1516
+ }
1517
+ /** The per-AgentSession rows a record and a human line are both built from. */
1518
+ function liveUpdateSessionRecords(sessions) {
1519
+ return Object.freeze(sessions.map((session) => Object.freeze({
1520
+ agent_session_id: session.agentSessionId,
1521
+ process_epoch: session.processEpoch,
1522
+ ...(session.outcome === undefined ? {} : { outcome: session.outcome }),
1523
+ })));
1524
+ }
1525
+ /**
1526
+ * One operation, as a record and as prose.
1527
+ *
1528
+ * Two rules it holds and a renderer would otherwise be free to break: a
1529
+ * withheld per-session account is stated as withheld rather than expanded into
1530
+ * `unknown` outcomes, and `installed_at` is described by its evidence, because
1531
+ * `reconciled` says when Cuna LOOKED and not when the artifact arrived.
1532
+ */
1533
+ function liveUpdateOperationReport(operation, machineId) {
1534
+ const reading = readLiveSupervisorUpdateOperation(operation);
1535
+ const data = {
1536
+ machine_id: machineId,
1537
+ operation_id: operation.operationId,
1538
+ phase: operation.phase,
1539
+ control_rotated: operation.controlRotated,
1540
+ installer_outcome: operation.installerOutcome,
1541
+ ...(operation.controlGeneration === undefined
1542
+ ? {}
1543
+ : { control_generation: operation.controlGeneration }),
1544
+ ...(operation.artifactSha256 === undefined ? {} : { artifact_sha256: operation.artifactSha256 }),
1545
+ ...(operation.installedAt === undefined ? {} : { installed_at: operation.installedAt }),
1546
+ ...(operation.installationEvidence === undefined
1547
+ ? {}
1548
+ : { installation_evidence: operation.installationEvidence }),
1549
+ declared_sessions: liveUpdateSessionRecords(operation.declaredSessions),
1550
+ agent_sessions: liveUpdateSessionRecords(operation.sessions),
1551
+ session_account_withheld: reading.accountWithheld,
1552
+ ...(operation.failure === undefined
1553
+ ? {}
1554
+ : {
1555
+ failure: Object.freeze({
1556
+ status: operation.failure.status,
1557
+ code: operation.failure.code,
1558
+ title: operation.failure.title,
1559
+ detail: operation.failure.detail,
1560
+ retryable: operation.failure.retryable,
1561
+ action: operation.failure.action,
1562
+ }),
1563
+ }),
1564
+ next_action: operation.nextAction,
1565
+ claimed_at: operation.claimedAt,
1566
+ updated_at: operation.updatedAt,
1567
+ ...(operation.settledAt === undefined ? {} : { settled_at: operation.settledAt }),
1568
+ ...(operation.retiredAt === undefined ? {} : { retired_at: operation.retiredAt }),
1569
+ ...(operation.retirementOutcome === undefined
1570
+ ? {}
1571
+ : { retirement_outcome: operation.retirementOutcome }),
1572
+ // The fence, as a decided value rather than two nullable fields a caller
1573
+ // has to combine. `installer_can_still_act` is the one an automated caller
1574
+ // needs before touching this Machine, and it is false ONLY on the producer's
1575
+ // own evidence -- never inferred from the phase or from a running Machine.
1576
+ installer_reach: reading.installerReach,
1577
+ installer_can_still_act: reading.installerCanStillAct,
1578
+ // Stated in the record because a caller reading only `--json` gets no prose,
1579
+ // and because this is the sentence the producer is most emphatic about.
1580
+ machine_running_implies_installed: false,
1581
+ // The same rule one layer out: the fence's record of an admitted installer
1582
+ // that finished is not Cuna's observation of an installation, and only
1583
+ // `installer_outcome` is that.
1584
+ retirement_outcome_implies_installed: false,
1585
+ };
1586
+ const lines = [
1587
+ `Operation ${operation.operationId} on Machine ${machineId}.`,
1588
+ `Phase ${liveSupervisorUpdatePhaseLabel(operation.phase)}.`,
1589
+ `Installer ${liveSupervisorInstallerOutcomeLabel(operation.installerOutcome, reading.installerReach)}.`,
1590
+ operation.controlRotated
1591
+ ? "This Machine's supervisor control HAS rotated for this update. That is irreversible and stays true even if nothing was installed."
1592
+ : "This Machine's supervisor control has not rotated for this update.",
1593
+ ...(operation.artifactSha256 === undefined
1594
+ ? ["No supervisor artifact is bound to this operation yet."]
1595
+ : [`Target artifact ${operation.artifactSha256}.`]),
1596
+ ...(operation.controlGeneration === undefined
1597
+ ? []
1598
+ : [`Control generation ${operation.controlGeneration}.`]),
1599
+ ...(operation.installedAt === undefined || operation.installationEvidence === undefined
1600
+ ? []
1601
+ : [`Installation established ${operation.installedAt} by ${liveSupervisorInstallationEvidenceLabel(operation.installationEvidence)}.`]),
1602
+ reading.declared === 0
1603
+ ? "This operation has not bound the AgentSessions it is accountable for."
1604
+ : `It measured ${reading.declared} AgentSession${reading.declared === 1 ? "" : "s"}:`,
1605
+ ...operation.declaredSessions.map((session) => ` ${session.agentSessionId} process epoch ${session.processEpoch}`),
1606
+ ...(reading.accountWithheld
1607
+ ? ["Cuna has settled no per-AgentSession account for this operation. That is an answer it has not given, not a set of unknown outcomes."]
1608
+ : reading.accounted === 0
1609
+ ? []
1610
+ : [
1611
+ `Per-AgentSession account, ${reading.accounted} of ${reading.declared} measured:`,
1612
+ ...operation.sessions.map((session) => ` ${session.agentSessionId} ${liveSupervisorSessionOutcomeLabel(session.outcome)}`),
1613
+ ]),
1614
+ ...(operation.failure === undefined
1615
+ ? []
1616
+ : [`Recorded refusal ${operation.failure.code} (HTTP ${operation.failure.status}): ${operation.failure.title}. ${operation.failure.detail}`]),
1617
+ "A running Machine is not evidence that this update did or did not apply. This read is.",
1618
+ // The install fence, ordered after the refusal and before the next action:
1619
+ // it is what decides whether "nothing further will change it" also means
1620
+ // "nothing further will change this Machine because of it".
1621
+ ...(operation.retiredAt === undefined
1622
+ ? []
1623
+ : [`Installer retired on the Machine ${operation.retiredAt}.`]),
1624
+ ...liveSupervisorInstallerReachLines(reading, machineId),
1625
+ reading.mayRepeatSameOperation
1626
+ ? `Next action: repeat this same operation. Run \`cuna machines live-update-supervisor ${machineId} --resume\`. Starting a different update is refused while this one is open.`
1627
+ : "Next action: none. This operation is settled and nothing further will change it.",
1628
+ ];
1629
+ return Object.freeze({ data, lines: Object.freeze(lines) });
1630
+ }
1631
+ /**
1632
+ * `machines live-update-supervisor` — the running-Machine supervisor update.
1633
+ *
1634
+ * The three rules that shape this function, in the order they bind:
1635
+ *
1636
+ * 1. Preservation is conditional on the SERVER's preflight, never on anything
1637
+ * read here. The local state check below is a cheap refusal that names the
1638
+ * other command; it is not the authority and does not weaken one.
1639
+ * 2. A 200 is an account, not a success. Every AgentSession outcome is
1640
+ * reported, and anything other than `preserved` is said plainly.
1641
+ * 3. An outcome this process did not learn is `unknown`. It is never retried,
1642
+ * never reconciled with a second mutation, and never resolved by falling
1643
+ * back to the stopped-Machine replacement.
1644
+ */
1645
+ async function executeLiveUpdateSupervisor(context) {
1646
+ const { parsed, client, now } = context;
1647
+ const { intent } = preflightLiveUpdateSupervisor(parsed);
1648
+ const id = assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
1649
+ const notes = liveUpdateNotesFor(context, context.platform);
1650
+ if (intent === "forget-unknown")
1651
+ return clearLiveUpdateRecord(context, notes, id);
1652
+ const reading = await notes.read(id);
1653
+ if (reading.state === "unreadable") {
1654
+ // Fail closed and name the way out, for both sending intents. The record may
1655
+ // describe a dispatch that is still in flight, so this is not a corrupt file
1656
+ // to step over -- and Cuna cannot read an operation identity out of it, so
1657
+ // there is nothing to resume either.
1658
+ throw new CunaError({
1659
+ code: "cuna.machine.live_supervisor_update_record_unreadable",
1660
+ message: `This computer holds a supervisor update record for Machine ${id} that Cuna cannot read.`,
1661
+ exitCode: EXIT_CODES.conflict,
1662
+ hint: `Nothing was sent. That record may describe an update whose outcome is still unknown, and Cuna cannot read the operation identity out of it to ask. Clear it with \`cuna machines live-update-supervisor ${id} --forget-unknown\`, which works on an unreadable record, and read \`cuna machines list\` and \`cuna agent-sessions list --machine ${id}\` before starting another update.`,
1663
+ retryable: false,
1664
+ details: {
1665
+ machine_id: id,
1666
+ record_path: reading.path,
1667
+ reason: reading.reason,
1668
+ outcome: "unknown",
1669
+ },
1670
+ });
1671
+ }
1672
+ if (intent === "resume") {
1673
+ // A caller-known identity, for an update this computer never recorded. The
1674
+ // cross-client half of recovery: an owner who started one in the console, or
1675
+ // on another computer, holds its id and nothing else.
1676
+ const named = stringOption(parsed, "operation");
1677
+ if (named === undefined && reading.state === "none") {
1678
+ throw new CunaError({
1679
+ code: "cuna.machine.live_supervisor_update_nothing_to_resume",
1680
+ message: `This computer holds no in-place supervisor update record for Machine ${id}.`,
1681
+ exitCode: EXIT_CODES.conflict,
1682
+ hint: `Nothing was sent. --resume re-sends an operation identity, and there is none recorded for ${id} on ${context.config.baseUrl}. An update started from another computer or from the web console has an identity this CLI never held: name it with \`cuna machines live-update-supervisor ${id} --resume --operation UUID\`, or read it first with \`cuna machines live-update-status ${id} --operation UUID\`. To start a new update, run \`cuna machines live-update-supervisor ${id} --yes\`.`,
1683
+ retryable: false,
1684
+ details: { machine_id: id, outcome: "not_sent" },
1685
+ });
1686
+ }
1687
+ if (named !== undefined && reading.state === "outstanding" && reading.note.operationId !== named) {
1688
+ // A live reservation for a different identity. It is not this command's
1689
+ // to overwrite, and the exclusive create below would refuse anyway.
1690
+ throw new CunaError({
1691
+ code: "cuna.machine.live_supervisor_update_already_reserved",
1692
+ message: `This computer is already holding in-place supervisor update ${reading.note.operationId} for Machine ${id}.`,
1693
+ exitCode: EXIT_CODES.conflict,
1694
+ hint: `Nothing was sent. Resolve the update this computer recorded before taking on another: read it with \`cuna machines live-update-status ${id}\`. Two updates must never rotate one Machine's control at once.`,
1695
+ retryable: false,
1696
+ details: {
1697
+ machine_id: id,
1698
+ operation_id: reading.note.operationId,
1699
+ requested_operation_id: named,
1700
+ outcome: "not_sent",
1701
+ },
1702
+ });
1703
+ }
1704
+ const operationId = named ?? reading.note.operationId;
1705
+ /* Read the authoritative status BEFORE repeating anything. Two reasons, and
1706
+ the second is the one a reviewer drove out:
1707
+ - the producer's own `next_action` is what says a repeat resolves this,
1708
+ so offering one without asking would be the client deciding;
1709
+ - a repeat of an identity the producer never journalled is that
1710
+ identity's FIRST admission and runs a whole update. Calling that a
1711
+ "repeat" was true of the command and false of the effect. */
1712
+ const status = await readLiveUpdateOperationAnswer(client, id, operationId);
1713
+ if (status.state === "absent") {
1714
+ /* A not-found is two facts, and only the record's own account label tells
1715
+ them apart. The SAME account can see its own operations, so this really
1716
+ is "never admitted". A different account, or a record that names none,
1717
+ and a caller-supplied identity that no record backs, are all cases where
1718
+ the operation may exist and simply be out of view -- and saying it was
1719
+ never admitted there would be the false half of the same coin the
1720
+ reviewer caught on the other side. */
1721
+ const recordAccount = reading.state === "outstanding" ? reading.note.account : undefined;
1722
+ const signedIn = await liveUpdateAccountLabel(client);
1723
+ const neverAdmitted = named === undefined && recordAccount !== undefined && signedIn !== undefined &&
1724
+ recordAccount === signedIn;
1725
+ throw new CunaError({
1726
+ code: neverAdmitted
1727
+ ? "cuna.machine.live_supervisor_update_nothing_to_resume"
1728
+ : "cuna.machine.live_supervisor_update_operation_inaccessible",
1729
+ message: neverAdmitted
1730
+ ? `Cuna has no in-place supervisor update ${operationId} for Machine ${id} under this account, which is the account that recorded it.`
1731
+ : `The signed-in account cannot read in-place supervisor update ${operationId} on Machine ${id}.`,
1732
+ exitCode: EXIT_CODES.conflict,
1733
+ hint: neverAdmitted
1734
+ ? `Nothing was sent. There is nothing to resume: this identity was never admitted, so re-sending it would be a NEW update's first admission rather than finishing one -- and Cuna will not do that under a flag that says "resume". Clear the record with \`cuna machines live-update-supervisor ${id} --forget-unknown\`, then start one with \`--yes\`.`
1735
+ : `Nothing was sent, and this is NOT evidence that the operation never existed: Cuna answers an operation outside this account's view exactly as it answers one that never existed. ${recordAccount === undefined
1736
+ ? "This computer's record does not name the account that filed it."
1737
+ : `This computer's record was filed under account ${recordAccount}.`} Check the signed-in account with \`cuna whoami\`, sign in as the Machine's owner, then read it with \`cuna machines live-update-status ${id}${named === undefined ? "" : ` --operation ${operationId}`}\`.`,
1738
+ retryable: false,
1739
+ details: {
1740
+ machine_id: id,
1741
+ operation_id: operationId,
1742
+ outcome: neverAdmitted ? "not_sent" : "inaccessible",
1743
+ local_record_cleared: false,
1744
+ ...(neverAdmitted ? { operation_admitted: false } : {}),
1745
+ ...(recordAccount === undefined ? {} : { record_account: recordAccount }),
1746
+ ...(signedIn === undefined ? {} : { signed_in_account: signedIn }),
1747
+ },
1748
+ });
1749
+ }
1750
+ if (status.state === "read" && status.operation.nextAction === "none") {
1751
+ const report = liveUpdateOperationReport(status.operation, id);
1752
+ // Settled: repeating returns this same recorded answer, so the honest
1753
+ // action is to show it and release the record rather than re-send.
1754
+ if (reading.state === "outstanding" && reading.note.operationId === operationId) {
1755
+ await notes.settle(id, operationId);
1756
+ }
1757
+ return Object.freeze({
1758
+ command: "machines.live-update-status",
1759
+ data: Object.freeze({
1760
+ ...report.data,
1761
+ operation_id_source: named === undefined ? "local_record" : "named",
1762
+ dispatched_installer: false,
1763
+ server_state_changed: false,
1764
+ resumed: false,
1765
+ }),
1766
+ human: [
1767
+ "Nothing was re-sent: this update is settled, and repeating it would return this same recorded answer.",
1768
+ ...report.lines,
1769
+ ].join("\n"),
1770
+ });
1771
+ }
1772
+ /* A caller-known identity needs the same reservation every dispatch takes,
1773
+ so a concurrent invocation cannot send it at the same moment. An identity
1774
+ this computer already records IS the reservation, and re-reserving it
1775
+ would lose to itself. */
1776
+ let recordedAccount = reading.state === "outstanding" ? reading.note.account : undefined;
1777
+ if (reading.state === "none") {
1778
+ const account = await liveUpdateAccountLabel(client);
1779
+ const reserved = await notes.reserve(id, operationId, new Date(now).toISOString(), account);
1780
+ if (reserved === undefined) {
1781
+ throw new CunaError({
1782
+ code: "cuna.machine.live_supervisor_update_already_reserved",
1783
+ message: `Another Cuna process on this computer is already updating the supervisor of Machine ${id}.`,
1784
+ exitCode: EXIT_CODES.conflict,
1785
+ hint: "Nothing was sent. Let that invocation finish and read its answer; two in-place updates of one Machine must not run together.",
1786
+ retryable: false,
1787
+ details: { machine_id: id, operation_id: operationId, outcome: "not_sent" },
1788
+ });
1789
+ }
1790
+ recordedAccount = account;
1791
+ }
1792
+ // No fresh identity: the whole point of this path is that it does not
1793
+ // change. The producer fences concurrent advances of one identity itself,
1794
+ // and refuses outright to issue a second enrollment under it.
1795
+ return dispatchLiveUpdate(context, notes, id, {
1796
+ operationId,
1797
+ dispatchedAt: reading.state === "outstanding" ? reading.note.dispatchedAt : new Date(now).toISOString(),
1798
+ origin: "recorded_identity",
1799
+ ...(recordedAccount === undefined ? {} : { recordedAccount }),
1800
+ });
1801
+ }
1802
+ if (reading.state === "outstanding") {
1803
+ throw outstandingLiveUpdate(id, reading.note.operationId, reading.note.dispatchedAt);
1804
+ }
1805
+ // The same `machines:update` authority the stopped-boundary replacement takes.
1806
+ // The deliberately unsupported AgentSession-create capability is NOT
1807
+ // substituted for it: this operation is not an OpenCode remediation.
1808
+ await requireCapability({
1809
+ client, scope: "machine", resourceId: id, capabilityId: "machines.lifecycle",
1810
+ now: context.capabilityClock ?? now,
1811
+ });
1812
+ const current = await client.getMachine(id);
1813
+ if (current.state !== "running") {
1814
+ throw new CunaError({
1815
+ code: "cuna.machine.live_supervisor_update_requires_running",
1816
+ message: "The in-place supervisor update replaces the supervisor while the Machine keeps running.",
1817
+ exitCode: EXIT_CODES.conflict,
1818
+ hint: `Cuna will not start ${current.name} to satisfy this command. For a stopped Machine use \`cuna machines update-supervisor ${id} --yes\`, which is the separate stopped-boundary action.`,
1819
+ details: { machine_id: id, observed_state: current.state },
1820
+ });
1821
+ }
1822
+ // Chosen HERE, before anything is sent, because a caller that learns its
1823
+ // operation identity from the response cannot use it when the response is the
1824
+ // thing that was lost.
1825
+ const operationId = randomUUID();
1826
+ const dispatchedAt = new Date(now).toISOString();
1827
+ // A label, recorded only because the two reads above already established that
1828
+ // the API is answering, and never consulted to admit anything. It exists for
1829
+ // one sentence: when a later read of this record is refused as not-found, the
1830
+ // owner is told the record was filed under a different account rather than
1831
+ // that the operation never existed.
1832
+ const account = await liveUpdateAccountLabel(client);
1833
+ // The reservation is the gate, not the read above. Between that read and this
1834
+ // line another process on this computer may have taken the slot, and only an
1835
+ // exclusive create can say which of them did. `undefined` means it lost.
1836
+ const reserved = await notes.reserve(id, operationId, dispatchedAt, account);
1837
+ if (reserved === undefined) {
1838
+ const rival = await notes.read(id);
1839
+ throw rival.state === "outstanding"
1840
+ ? outstandingLiveUpdate(id, rival.note.operationId, rival.note.dispatchedAt)
1841
+ : new CunaError({
1842
+ code: "cuna.machine.live_supervisor_update_already_reserved",
1843
+ message: `Another Cuna process on this computer is already updating the supervisor of Machine ${id}.`,
1844
+ exitCode: EXIT_CODES.conflict,
1845
+ hint: "Nothing was sent. Let that invocation finish and read its answer; two in-place updates of one Machine must not run together.",
1846
+ retryable: false,
1847
+ details: { machine_id: id, outcome: "not_sent" },
1848
+ });
1849
+ }
1850
+ return dispatchLiveUpdate(context, notes, id, {
1851
+ operationId,
1852
+ dispatchedAt,
1853
+ origin: "new_identity",
1854
+ ...(account === undefined ? {} : { recordedAccount: account }),
1855
+ });
1856
+ }
1857
+ /**
1858
+ * The account this dispatch is filed under, when asking costs nothing.
1859
+ *
1860
+ * Best effort on purpose. A label that cannot be fetched is omitted, never
1861
+ * waited for and never a reason to refuse: the record must stay writable in
1862
+ * exactly the state it exists for, and this runs only after two successful
1863
+ * reads have already established that the API is answering.
1864
+ */
1865
+ async function liveUpdateAccountLabel(client) {
1866
+ try {
1867
+ return (await client.getIdentity()).id;
1868
+ }
1869
+ catch {
1870
+ return undefined;
1871
+ }
1872
+ }
1873
+ /**
1874
+ * Send one in-place update under an identity that is already recorded.
1875
+ *
1876
+ * Shared by the two intents that send, because the only difference between them
1877
+ * is where the identity came from -- and that difference is carried explicitly
1878
+ * in `origin` rather than re-derived, because it decides whether a not-found
1879
+ * answer means "nothing was admitted" or "this account cannot see it".
1880
+ */
1881
+ async function dispatchLiveUpdate(context, notes, id, dispatch) {
1882
+ const { client } = context;
1883
+ const { operationId, origin } = dispatch;
1884
+ const repeat = origin === "recorded_identity";
1885
+ let result;
1886
+ try {
1887
+ result = await client.updateMachineSupervisorInPlace(id, operationId);
1888
+ }
1889
+ catch (error) {
1890
+ let disposition = classifyLiveSupervisorUpdateFailure(error, origin);
1891
+ // Dropping the note is bookkeeping; the outcome is the answer. A local
1892
+ // filesystem fault must never replace the producer's own refusal with a
1893
+ // story about this computer's state directory, and a note that survives
1894
+ // fails in the safe direction.
1895
+ //
1896
+ // Bound to THIS identity: `settle` removes the record only when the record
1897
+ // names the operation this invocation sent. A record another process wrote
1898
+ // is not ours to remove.
1899
+ const drop = async () => {
1900
+ try {
1901
+ await notes.settle(id, operationId);
1902
+ }
1903
+ catch { /* the outcome below is the answer */ }
1904
+ };
1905
+ if (disposition === "not_admitted_machine_busy") {
1906
+ /* The producer's claim ordering says nothing was journalled under this
1907
+ identity, and the producer itself can confirm it. One bounded read that
1908
+ dispatches no installer and changes no state turns a reading of the
1909
+ ordering into an answer before a record is released. Only `absent` is
1910
+ that answer; a row, or a read that could not ask, keeps the record. */
1911
+ const answer = await readLiveUpdateOperationAnswer(client, id, operationId);
1912
+ if (answer.state !== "absent")
1913
+ disposition = "operation_conflict";
1914
+ }
1915
+ if (!liveSupervisorUpdateRecordSurvives(disposition, origin)) {
1916
+ await drop();
1917
+ // Nothing was admitted under an identity this process had just minted, so
1918
+ // the producer's own words are already the best available explanation.
1919
+ if (disposition === "not_admitted")
1920
+ throw error;
1921
+ if (disposition === "not_admitted_machine_busy") {
1922
+ throw new CunaError({
1923
+ code: "cuna.machine.live_supervisor_update_machine_busy",
1924
+ message: `Another in-place supervisor update is already running on Machine ${id}.`,
1925
+ exitCode: EXIT_CODES.conflict,
1926
+ hint: `Nothing was sent to this Machine and Cuna recorded no update for you: it refused before admitting one, and confirmed by reading that it has no operation ${operationId}. Two updates must never rotate one Machine's control at once. Read the one that is running with \`cuna machines live-update-status ${id} --operation UUID\` if you know its identity, and start yours again once it settles.`,
1927
+ retryable: true,
1928
+ details: { machine_id: id, operation_id: operationId, outcome: "not_admitted", local_record_cleared: true },
1929
+ cause: error,
1930
+ });
1931
+ }
1932
+ throw new CunaError({
1933
+ code: disposition === "applied_with_ended_sessions"
1934
+ ? "cuna.machine.live_supervisor_update_ended_sessions"
1935
+ : "cuna.machine.live_supervisor_update_control_rotated",
1936
+ message: disposition === "applied_with_ended_sessions"
1937
+ ? `The in-place supervisor update on Machine ${id} was installed and ended AgentSessions.`
1938
+ : `The in-place supervisor update on Machine ${id} rotated its supervisor control and installed nothing.`,
1939
+ exitCode: EXIT_CODES.conflict,
1940
+ hint: `${error instanceof CunaError && error.hint !== undefined ? error.hint : "The update cannot be undone."} This operation is settled, so repeating this identity returns this same answer. Read \`cuna agent-sessions list --machine ${id}\`.`,
1941
+ retryable: false,
1942
+ details: { machine_id: id, operation_id: operationId, outcome: disposition },
1943
+ cause: error,
1944
+ });
1945
+ }
1946
+ // The record stays. Before reporting an unknown effect, ask the one thing
1947
+ // that can answer: a read that dispatches no installer and changes no state.
1948
+ // "Reconcile uncertain mutations before retrying" is the rule; this is the
1949
+ // reconciliation, and it is bounded to a single request.
1950
+ const reconciled = await reconcileLiveUpdateOperation(client, id, operationId);
1951
+ if (reconciled !== undefined) {
1952
+ const report = liveUpdateOperationReport(reconciled, id);
1953
+ const settled = reconciled.phase === "settled";
1954
+ if (settled)
1955
+ await drop();
1956
+ throw new CunaError({
1957
+ code: settled
1958
+ ? "cuna.machine.live_supervisor_update_settled_refusal"
1959
+ : "cuna.machine.live_supervisor_update_outcome_unknown",
1960
+ message: settled
1961
+ ? `In-place supervisor update ${operationId} on Machine ${id} is settled, and its recorded answer is a refusal.`
1962
+ : `In-place supervisor update ${operationId} on Machine ${id} has not settled.`,
1963
+ exitCode: EXIT_CODES.conflict,
1964
+ hint: `${report.lines.join(" ")} ${settled
1965
+ ? "This computer's record was cleared because the operation is terminal."
1966
+ : "This computer keeps the record, so the identity survives."}`,
1967
+ retryable: false,
1968
+ details: {
1969
+ operation_outcome: settled ? "settled" : "open",
1970
+ local_record_cleared: settled,
1971
+ ...report.data,
1972
+ },
1973
+ cause: error,
1974
+ });
1975
+ }
1976
+ if (disposition === "operation_inaccessible") {
1977
+ throw new CunaError({
1978
+ code: "cuna.machine.live_supervisor_update_operation_inaccessible",
1979
+ message: `The signed-in account cannot read in-place supervisor update ${operationId} on Machine ${id}.`,
1980
+ exitCode: EXIT_CODES.conflict,
1981
+ hint: `Nothing on this Machine was changed by this attempt, and this is NOT evidence that the operation never existed: Cuna answers an unowned Machine and an operation outside this account's view with the same not-found. ${dispatch.recordedAccount === undefined
1982
+ ? "This computer's record does not name the account that filed it."
1983
+ : `This computer's record was filed under account ${dispatch.recordedAccount}.`} Check the signed-in account with \`cuna whoami\`, sign in as the owner, then read it with \`cuna machines live-update-status ${id}\`. The record is kept, so the identity survives.`,
1984
+ retryable: false,
1985
+ details: {
1986
+ machine_id: id,
1987
+ operation_id: operationId,
1988
+ outcome: "inaccessible",
1989
+ local_record_cleared: false,
1990
+ ...(dispatch.recordedAccount === undefined ? {} : { record_account: dispatch.recordedAccount }),
1991
+ },
1992
+ cause: error,
1993
+ });
1994
+ }
1995
+ // The read did not answer either. The record stays exactly where it is.
1996
+ throw outstandingLiveUpdate(id, operationId, dispatch.dispatchedAt);
1997
+ }
1998
+ const summary = summarizeLiveSupervisorUpdate(result);
1999
+ // A 200 the producer only emits with every session decided. If a deployment
2000
+ // ever answers 200 with an `unknown` in it, that is still an outcome this
2001
+ // computer does not know, so the note survives and the result says so.
2002
+ if (!summary.anyUnknown)
2003
+ await notes.settle(id, operationId);
2004
+ const sessions = result.sessions.map((session) => Object.freeze({
2005
+ agent_session_id: session.agentSessionId,
2006
+ process_epoch: session.processEpoch,
2007
+ outcome: session.outcome,
2008
+ }));
2009
+ const lines = result.sessions.map((session) => ` ${session.agentSessionId} ${liveSupervisorSessionOutcomeLabel(session.outcome)}`);
2010
+ const subject = repeat
2011
+ ? `Cuna resolved the in-place supervisor update it had already sent for ${result.machine.name}`
2012
+ : `Cuna installed a new terminal supervisor on ${result.machine.name} without stopping it`;
2013
+ const headline = summary.total === 0
2014
+ ? `${subject}. No AgentSession existed on it, so nothing was preserved or lost.`
2015
+ : summary.allPreserved
2016
+ ? `${subject}, and all ${summary.total} AgentSessions were preserved.`
2017
+ : `${subject}. ${summary.preserved} of ${summary.total} AgentSessions were preserved; this update is not complete.`;
2018
+ return Object.freeze({
2019
+ command: "machines.live-update-supervisor",
2020
+ data: Object.freeze({
2021
+ machine: machineRecord(result.machine),
2022
+ operation_id: result.operationId,
2023
+ // What this invocation did with the identity, which a caller reading only
2024
+ // `--json` cannot otherwise tell apart -- and which is the difference
2025
+ // between spending a control generation and not spending one.
2026
+ dispatch: repeat ? "repeat_same_operation" : "new_operation",
2027
+ control_generation: result.controlGeneration,
2028
+ artifact_sha256: result.artifactSha256,
2029
+ installed_at: result.installedAt,
2030
+ agent_sessions: Object.freeze(sessions),
2031
+ summary: Object.freeze({
2032
+ total: summary.total,
2033
+ preserved: summary.preserved,
2034
+ exited: summary.exited,
2035
+ ended: summary.ended,
2036
+ unknown: summary.unknown,
2037
+ all_preserved: summary.allPreserved,
2038
+ }),
2039
+ // An installed supervisor is software, not an authorization. Stated in the
2040
+ // record because a caller reading only `--json` gets no prose.
2041
+ grants_observation_or_control: false,
2042
+ establishes_provider_login: false,
2043
+ }),
2044
+ human: [
2045
+ headline,
2046
+ ...(repeat
2047
+ ? [`This re-sent operation ${result.operationId}, the one this computer had already recorded. It did not start a second update and did not rotate this Machine's control again.`]
2048
+ : [`Operation ${result.operationId}.`]),
2049
+ `Control generation ${result.controlGeneration}, artifact ${result.artifactSha256}, installed ${result.installedAt}.`,
2050
+ ...(lines.length === 0 ? [] : ["AgentSession custody:", ...lines]),
2051
+ "This installed software only. It grants no observation or control of any AgentSession and signs no provider in.",
2052
+ ...(summary.anyUnknown
2053
+ ? [`Cuna could not re-read ${summary.unknown} of ${summary.total} AgentSessions, so this computer's record of this update stays open. Read it with \`cuna machines live-update-status ${id}\`.`]
2054
+ : []),
2055
+ ].join("\n"),
2056
+ });
2057
+ }
2058
+ /**
2059
+ * `--forget-unknown`, and why it now asks before it forgets.
2060
+ *
2061
+ * It used to be a person saying "I have looked and I am done with this record",
2062
+ * and that was honest while the record held nothing but a timestamp: there was
2063
+ * no identity to lose and no read that could resolve it.
2064
+ *
2065
+ * The record now holds the operation identity, and that changes what discarding
2066
+ * it costs. An unsettled operation still holds the Machine on the producer's
2067
+ * side -- every DIFFERENT identity is refused until it settles, and the only
2068
+ * thing that settles it is a repeat of the identity in this file. Clearing it
2069
+ * while it is open does not free the Machine; it strands it, with no way to
2070
+ * resume and no way to start afresh. So this path reads the operation first and
2071
+ * clears only what is terminal.
2072
+ *
2073
+ * That read is also what prevents clearing an ACTIVELY EXECUTING reservation,
2074
+ * without inventing an age threshold or a liveness file that could go stale: an
2075
+ * attempt in flight is by construction not settled, and a settled operation has
2076
+ * no attempt whose effect is still open.
2077
+ *
2078
+ * Two things it still clears without asking anyone: bytes that carry no
2079
+ * identity to ask about, and nothing at all.
2080
+ */
2081
+ async function clearLiveUpdateRecord(context, notes, id) {
2082
+ const { client } = context;
2083
+ const reading = await notes.read(id);
2084
+ const unchanged = "Nothing was sent to the Machine by this command, and clearing a record is not a cancellation.";
2085
+ if (reading.state === "none") {
2086
+ return Object.freeze({
2087
+ command: "machines.live-update-supervisor",
2088
+ data: Object.freeze({
2089
+ machine_id: id,
2090
+ local_record_cleared: false,
2091
+ record_readable: true,
2092
+ server_state_changed: false,
2093
+ scope: "api_origin_and_machine",
2094
+ }),
2095
+ human: `No local record of an in-place supervisor update exists for Machine ${id} on ${context.config.baseUrl}. Nothing was sent and nothing changed.`,
2096
+ });
2097
+ }
2098
+ if (reading.state === "unreadable") {
2099
+ const removed = await notes.discard(id);
2100
+ return Object.freeze({
2101
+ command: "machines.live-update-supervisor",
2102
+ data: Object.freeze({
2103
+ machine_id: id,
2104
+ local_record_cleared: removed,
2105
+ record_readable: false,
2106
+ unreadable_reason: reading.reason,
2107
+ server_state_changed: false,
2108
+ scope: "api_origin_and_machine",
2109
+ }),
2110
+ human: `Cleared an unreadable local record for Machine ${id} (${reading.reason}). It carried no operation identity, so there is nothing Cuna could have read about it and nothing an update could still have been resumed with. ${unchanged}`,
2111
+ });
2112
+ }
2113
+ const { operationId, dispatchedAt } = reading.note;
2114
+ const answer = await readLiveUpdateOperationAnswer(client, id, operationId);
2115
+ if (answer.state === "absent") {
2116
+ /* The producer has no operation with this identity. That is two different
2117
+ facts depending on WHO asked, and the record's own account label is what
2118
+ tells them apart -- evidence, never authority, exactly as it has always
2119
+ been used here.
2120
+ Same account: this owner can see their own operations, so there is
2121
+ genuinely none under this identity. Nothing was admitted, nothing can be
2122
+ resumed, and refusing to clear it would strand the Machine for no reason.
2123
+ Different or unrecorded account: the producer answers an operation
2124
+ outside the caller's view exactly as it answers one that never existed,
2125
+ so this says nothing about whether the owner's operation exists. The
2126
+ record stays. */
2127
+ const signedIn = await liveUpdateAccountLabel(client);
2128
+ const sameAccount = reading.note.account !== undefined && signedIn !== undefined && reading.note.account === signedIn;
2129
+ if (!sameAccount) {
2130
+ throw new CunaError({
2131
+ code: "cuna.machine.live_supervisor_update_record_unresolved",
2132
+ message: `The signed-in account cannot see in-place supervisor update ${operationId} on Machine ${id}, so Cuna kept this computer's record of it.`,
2133
+ exitCode: EXIT_CODES.conflict,
2134
+ hint: `The record was NOT cleared, and that is the safe direction: a not-found answer here does not mean the operation is gone, because Cuna answers an unowned Machine and an operation outside this account's view the same way. ${reading.note.account === undefined
2135
+ ? "This record does not name the account that filed it, so Cuna cannot tell the two apart."
2136
+ : `This record was filed under account ${reading.note.account}.`} Check the signed-in account with \`cuna whoami\`, sign in as the owner, then read it with \`cuna machines live-update-status ${id}\`.`,
2137
+ retryable: false,
2138
+ details: {
2139
+ machine_id: id,
2140
+ operation_id: operationId,
2141
+ dispatched_at: dispatchedAt,
2142
+ local_record_cleared: false,
2143
+ ...(reading.note.account === undefined ? {} : { record_account: reading.note.account }),
2144
+ ...(signedIn === undefined ? {} : { signed_in_account: signedIn }),
2145
+ },
2146
+ });
2147
+ }
2148
+ const removedAbsent = await notes.settle(id, operationId);
2149
+ return Object.freeze({
2150
+ command: "machines.live-update-supervisor",
2151
+ data: Object.freeze({
2152
+ machine_id: id,
2153
+ operation_id: operationId,
2154
+ local_record_cleared: removedAbsent === "settled",
2155
+ record_readable: true,
2156
+ dispatched_at: dispatchedAt,
2157
+ account: reading.note.account,
2158
+ operation_admitted: false,
2159
+ server_state_changed: false,
2160
+ scope: "api_origin_and_machine",
2161
+ }),
2162
+ human: [
2163
+ `Cuna has no in-place supervisor update ${operationId} for Machine ${id} under this account, which is the account this record was filed by, so the record was cleared.`,
2164
+ "That identity was never admitted: nothing was installed, no supervisor control was issued, and nothing on this Machine was changed by it. Starting a new update is now possible again.",
2165
+ unchanged,
2166
+ ].join("\n"),
2167
+ });
2168
+ }
2169
+ if (answer.state === "unavailable") {
2170
+ throw new CunaError({
2171
+ code: "cuna.machine.live_supervisor_update_record_unresolved",
2172
+ message: `Cuna could not read in-place supervisor update ${operationId} on Machine ${id}, so it kept this computer's record of it.`,
2173
+ exitCode: EXIT_CODES.conflict,
2174
+ hint: `The record was NOT cleared, and that is the safe direction: while that operation is open the producer refuses every different update identity for this Machine, so clearing this would leave nothing able to resume it and nothing able to replace it. Read it again with \`cuna machines live-update-status ${id}\`.`,
2175
+ retryable: true,
2176
+ details: {
2177
+ machine_id: id,
2178
+ operation_id: operationId,
2179
+ dispatched_at: dispatchedAt,
2180
+ local_record_cleared: false,
2181
+ ...(reading.note.account === undefined ? {} : { record_account: reading.note.account }),
2182
+ },
2183
+ });
2184
+ }
2185
+ const operation = answer.operation;
2186
+ const report = liveUpdateOperationReport(operation, id);
2187
+ if (operation.phase !== "settled") {
2188
+ throw new CunaError({
2189
+ code: "cuna.machine.live_supervisor_update_not_settled",
2190
+ message: `In-place supervisor update ${operationId} on Machine ${id} has not settled, so this computer's record of it was kept.`,
2191
+ exitCode: EXIT_CODES.conflict,
2192
+ hint: `${report.lines.join(" ")} The record was NOT cleared: it holds the only identity that can resolve this operation, and the producer refuses every different one until it does.`,
2193
+ retryable: false,
2194
+ details: { local_record_cleared: false, ...report.data },
2195
+ });
2196
+ }
2197
+ const removed = await notes.settle(id, operationId);
2198
+ return Object.freeze({
2199
+ command: "machines.live-update-supervisor",
2200
+ data: Object.freeze({
2201
+ local_record_cleared: removed === "settled",
2202
+ record_readable: true,
2203
+ dispatched_at: dispatchedAt,
2204
+ // Present only if the note carried the label. It is shown, never used.
2205
+ ...(reading.note.account === undefined ? {} : { account: reading.note.account }),
2206
+ server_state_changed: false,
2207
+ // The exclusion is keyed by API origin and Machine, so this is what was
2208
+ // cleared -- not "this profile's" record.
2209
+ scope: "api_origin_and_machine",
2210
+ ...report.data,
2211
+ }),
2212
+ human: [
2213
+ `Cuna read in-place supervisor update ${operationId} on Machine ${id} and it is settled, so this computer's record of it was cleared. That record covered every profile on this computer.`,
2214
+ ...report.lines,
2215
+ unchanged,
2216
+ ].join("\n"),
2217
+ });
2218
+ }
2219
+ /**
2220
+ * `machines live-update-status` -- the authoritative read, and the only thing
2221
+ * that can say what one in-place update did.
2222
+ *
2223
+ * It sends no mutation. The producer states that this route dispatches no
2224
+ * installer, sends nothing to the Machine and writes nothing, which is exactly
2225
+ * why it is safe to reach for after an interruption and why a second POST with
2226
+ * a new identity is not.
2227
+ */
2228
+ async function executeLiveUpdateStatus(context) {
2229
+ const { parsed, client } = context;
2230
+ preflightLiveUpdateStatus(parsed);
2231
+ const id = assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
2232
+ const notes = liveUpdateNotesFor(context, context.platform);
2233
+ const named = stringOption(parsed, "operation");
2234
+ const reading = named === undefined ? await notes.read(id) : undefined;
2235
+ if (reading !== undefined && reading.state !== "outstanding") {
2236
+ throw new CunaError({
2237
+ code: "cuna.machine.live_supervisor_update_operation_unnamed",
2238
+ message: `Cuna does not know which in-place supervisor update to read for Machine ${id}.`,
2239
+ exitCode: EXIT_CODES.usage,
2240
+ hint: reading.state === "unreadable"
2241
+ ? `This computer holds a record for this Machine that Cuna cannot read (${reading.reason}), so it carries no operation identity. Name one with --operation UUID, or clear the record with \`cuna machines live-update-supervisor ${id} --forget-unknown\`.`
2242
+ : `This computer holds no record of an update for this Machine on ${context.config.baseUrl}. Name the operation with --operation UUID: an update started from another computer or from the web console has an identity this CLI never held.`,
2243
+ retryable: false,
2244
+ details: {
2245
+ machine_id: id,
2246
+ ...(reading.state === "unreadable" ? { record_path: reading.path, reason: reading.reason } : {}),
2247
+ },
2248
+ });
2249
+ }
2250
+ const operationId = named ?? reading.note.operationId;
2251
+ const operation = await client.readMachineSupervisorInPlaceUpdate(id, operationId);
2252
+ const report = liveUpdateOperationReport(operation, id);
2253
+ return Object.freeze({
2254
+ command: "machines.live-update-status",
2255
+ data: Object.freeze({
2256
+ ...report.data,
2257
+ operation_id_source: named === undefined ? "local_record" : "named",
2258
+ // Said in the record as well as in the prose: this command is a read, and
2259
+ // a caller consuming only `--json` must not have to infer that.
2260
+ dispatched_installer: false,
2261
+ server_state_changed: false,
2262
+ }),
2263
+ human: report.lines.join("\n"),
2264
+ });
2265
+ }
2266
+ async function executeMachines(context) {
2267
+ const { parsed, client, now } = context;
2268
+ requireCredential(context);
2269
+ const action = parsed.operands[0] ?? "overview";
2270
+ if (action === "overview") {
2271
+ rejectUnknownOptions(parsed, []);
2272
+ const machines = await listAllMachines(client);
2273
+ const overview = await Promise.all(machines.map(async (machine) => {
2274
+ try {
2275
+ const sessions = (await listAllMachineAgentSessions(client, machine.id))
2276
+ .filter(isAgentSessionIntendedActive)
2277
+ .slice()
2278
+ .sort((left, right) => left.agent.localeCompare(right.agent) || left.name.localeCompare(right.name) || left.id.localeCompare(right.id));
2279
+ return Object.freeze({ machine, sessions: Object.freeze(sessions) });
2280
+ }
2281
+ catch (error) {
2282
+ return Object.freeze({
2283
+ machine,
2284
+ sessions: Object.freeze([]),
2285
+ sessionsError: safeSessionsErrorReason(error),
2286
+ });
2287
+ }
2288
+ }));
2289
+ overview.sort((left, right) => left.machine.name.localeCompare(right.machine.name) || left.machine.id.localeCompare(right.machine.id));
2290
+ return Object.freeze({
2291
+ command: "machines.overview",
2292
+ data: Object.freeze({
2293
+ items: Object.freeze(overview.map(({ machine, sessions, sessionsError }) => Object.freeze({
2294
+ ...machineRecord(machine),
2295
+ session_counts: machineSessionCounts(machine, sessions, now),
2296
+ agent_sessions: Object.freeze(sessions.map((session) => agentSessionRecord(session, machine, now))),
2297
+ ...(sessionsError === undefined ? {} : { agent_sessions_error: sessionsError }),
2298
+ }))),
2299
+ }),
2300
+ human: renderMachineOverview(overview, now),
2301
+ });
2302
+ }
2303
+ if (action === "list") {
2304
+ rejectUnknownOptions(parsed, []);
2305
+ if (parsed.operands.length !== 1)
2306
+ throw usageError("machines list accepts no operands.");
2307
+ const page = await client.listMachines();
2308
+ const items = page.items.map(machineRecord);
2309
+ return Object.freeze({
2310
+ command: "machines.list",
2311
+ data: Object.freeze({ items, ...(page.nextCursor === undefined ? {} : { next_cursor: page.nextCursor }) }),
2312
+ // The provider verdict is what decides whether the ID on this row can host
2313
+ // a session at all. Without it a machine declaring `opencode` and a machine
2314
+ // declaring nothing printed byte-identically, and the person picked an ID
2315
+ // the very next command refuses. `machines list` takes no `--cursor`, so a
2316
+ // truncated page says so without promising an option that does not exist.
2317
+ human: [
2318
+ ...(items.length === 0
2319
+ ? ["No machines found."]
2320
+ : page.items.map((machine) => {
2321
+ const provider = machineProviderAvailability(machine);
2322
+ return `${machine.id}\t${machine.name}\t${machine.state}\t${provider.displayName} ${providerVerdict(provider)}`;
2323
+ })),
2324
+ ...(page.nextCursor === undefined
2325
+ ? []
2326
+ : ["-- truncated; more machines exist beyond this page"]),
2327
+ ].join("\n"),
2328
+ });
2329
+ }
2330
+ if (action === "create") {
2331
+ rejectUnknownOptions(parsed, ["name", "agent", "vcpus", "memory-mib", "background", "yes", "idempotency-key"]);
2332
+ if (parsed.operands.length !== 1)
2333
+ throw usageError("machines create accepts no operands.");
2334
+ requireConfirmation(parsed, "machines.create");
2335
+ const key = idempotencyKey(parsed);
2336
+ const rawName = stringOption(parsed, "name");
2337
+ if (rawName === undefined)
2338
+ throw usageError("Option --name is required.");
2339
+ const name = assertSafeDisplayText(rawName, "machine name");
2340
+ if (name.length < 1 || name.length > 80) {
2341
+ throw usageError("Option --name must contain 1 through 80 characters.");
2342
+ }
2343
+ const agent = agentOption(parsed, false);
2344
+ const vcpus = integerOption(parsed, "vcpus", 1, 8);
2345
+ const memoryMiB = integerOption(parsed, "memory-mib", 512, 16_384);
2346
+ await requireCapability({ client, scope: "account", capabilityId: "machines.create", now: context.capabilityClock ?? now });
2347
+ const input = {
2348
+ name,
2349
+ ...(agent === undefined ? {} : { agent }),
2350
+ ...(vcpus === undefined ? {} : { vcpus }),
2351
+ ...(memoryMiB === undefined ? {} : { memoryMiB }),
2352
+ ...(booleanOption(parsed, "background") ? { background: true } : {}),
2353
+ };
2354
+ const machine = await client.createMachine(input, key);
2355
+ const observed = await client.getMachine(machine.id);
2356
+ if (observed.id !== machine.id || observed.name !== name ||
2357
+ (agent !== undefined && observed.agent !== agent) ||
2358
+ (vcpus !== undefined && observed.vcpus !== vcpus) ||
2359
+ (memoryMiB !== undefined && observed.memoryMiB !== memoryMiB)) {
2360
+ postconditionUnverified("machine creation", {
2361
+ machine_id: machine.id,
2362
+ observed_id: observed.id,
2363
+ observed_name: observed.name,
2364
+ });
2365
+ }
2366
+ return Object.freeze({
2367
+ command: "machines.create",
2368
+ data: machineRecord(observed),
2369
+ human: `Created machine ${observed.name} (${observed.id}) in state ${observed.state}.`,
2370
+ });
2371
+ }
2372
+ if (action === "update-supervisor") {
2373
+ rejectUnknownOptions(parsed, ["yes"]);
2374
+ if (parsed.operands.length !== 2)
2375
+ throw usageError("machines update-supervisor requires exactly one machine ID.");
2376
+ requireConfirmation(parsed, "machines.update-supervisor");
2377
+ const id = assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
2378
+ // A normal AgentSession create treats this capability refusal as its stop
2379
+ // condition. This is the one explicit remediation action: it is admitted
2380
+ // only when the server itself reported the OpenCode-specific prerequisite.
2381
+ let updateRequired = false;
2382
+ let stoppedRuntimeUnverified;
2383
+ try {
2384
+ await requireCapability({
2385
+ client,
2386
+ scope: "machine",
2387
+ resourceId: id,
2388
+ capabilityId: "agent_sessions.create",
2389
+ now: context.capabilityClock ?? now,
2390
+ });
2391
+ }
2392
+ catch (error) {
2393
+ if (isOpenCodeSupervisorRepairCapabilityRejection(error)) {
2394
+ updateRequired = true;
2395
+ }
2396
+ else if (isOpenCodeRuntimeUnverifiedCapabilityRejection(error)) {
2397
+ // A stopped Machine cannot run the OpenCode binary probe. That is the
2398
+ // required precondition for this explicit repair, not evidence that
2399
+ // the supervisor update itself is forbidden. Re-read the exact
2400
+ // Machine below; the endpoint remains the authority for whether a
2401
+ // compatible supervisor already exists.
2402
+ stoppedRuntimeUnverified = error;
2403
+ }
2404
+ else {
2405
+ throw error;
2406
+ }
2407
+ }
2408
+ // The replacement route has the same `machines:update` authority as a
2409
+ // start. Do not substitute the deliberately unsupported create capability
2410
+ // for the authorization that permits the remediation.
2411
+ await requireCapability({
2412
+ client,
2413
+ scope: "machine",
2414
+ resourceId: id,
2415
+ capabilityId: "machines.lifecycle",
2416
+ now: context.capabilityClock ?? now,
2417
+ });
2418
+ const current = await client.getMachine(id);
2419
+ if (current.state !== "stopped") {
2420
+ throw new CunaError({
2421
+ code: "cuna.machine.supervisor_update_requires_stopped",
2422
+ message: "The Machine must be stopped before its terminal supervisor can be updated.",
2423
+ exitCode: EXIT_CODES.conflict,
2424
+ hint: `Cuna will not stop ${current.name} or terminate any AgentSessions. End only the sessions you intend to end, then run \`cuna machines stop ${id} --yes\`.`,
2425
+ details: { machine_id: id, observed_state: current.state },
2426
+ });
2427
+ }
2428
+ if (stoppedRuntimeUnverified !== undefined) {
2429
+ if (current.agent !== "opencode")
2430
+ throw stoppedRuntimeUnverified;
2431
+ updateRequired = true;
2432
+ }
2433
+ if (!updateRequired) {
2434
+ throw new CunaError({
2435
+ code: "cuna.machine.supervisor_update_not_required",
2436
+ message: "Cuna does not report that this Machine needs an OpenCode terminal-supervisor update.",
2437
+ exitCode: EXIT_CODES.unsupported,
2438
+ hint: "Create an OpenCode AgentSession normally. This action is available only after Cuna reports the exact supervisor prerequisite.",
2439
+ details: { machine_id: id },
2440
+ });
2441
+ }
2442
+ const updated = await client.replaceMachineSupervisor(id);
2443
+ if (updated.id !== id) {
2444
+ postconditionUnverified("terminal supervisor update", { machine_id: id, observed_id: updated.id });
2445
+ }
2446
+ return Object.freeze({
2447
+ command: "machines.update-supervisor",
2448
+ data: machineRecord(updated),
2449
+ human: `Cuna confirmed a new terminal supervisor for ${updated.name}; the Machine is ${updated.state}.`,
2450
+ });
2451
+ }
2452
+ if (action === "live-update-supervisor") {
2453
+ return executeLiveUpdateSupervisor(context);
2454
+ }
2455
+ if (action === "live-update-status") {
2456
+ return executeLiveUpdateStatus(context);
2457
+ }
2458
+ if (action === "start" || action === "pause" || action === "resume" || action === "stop") {
2459
+ rejectUnknownOptions(parsed, ["yes"]);
2460
+ if (parsed.operands.length !== 2)
2461
+ throw usageError(`machines ${action} requires exactly one machine ID.`);
2462
+ requireConfirmation(parsed, `machines.${action}`);
2463
+ const id = assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
2464
+ // The public capability registry deliberately groups the four reversible
2465
+ // lifecycle transitions under one semantic authority. The operation path
2466
+ // still binds the exact action; discovery must not invent per-action IDs
2467
+ // that the producer never advertises.
2468
+ await requireCapability({ client, scope: "machine", resourceId: id, capabilityId: "machines.lifecycle", now: context.capabilityClock ?? now });
2469
+ const expectedState = action === "pause" ? "paused" : action === "stop" ? "stopped" : "running";
2470
+ // Read the state first so the result can say whether anything moved. The
2471
+ // transition is still requested either way: this side's idea of the state
2472
+ // and the provider's can diverge, so a start against a Machine we already
2473
+ // call running may be a real repair.
2474
+ const before = await client.getMachine(id);
2475
+ const alreadyInState = before.id === id && before.state === expectedState;
2476
+ const machine = await client.transitionMachine(id, action);
2477
+ if (machine.id !== id) {
2478
+ // An identity contradiction: the producer answered about a different
2479
+ // machine. No amount of waiting converges that.
2480
+ postconditionUnverified(`machine ${action}`, { machine_id: id, observed_id: machine.id });
2481
+ }
2482
+ // A lifecycle transition is asynchronous on the producer, so the state the
2483
+ // very next read returns is usually the state BEFORE the transition. Read
2484
+ // back until it converges or until the CLI's own budget elapses.
2485
+ const observed = await convergeOnRemoteState(context, {
2486
+ operation: `machine ${action}`,
2487
+ settleWith: "cuna machines list",
2488
+ probe: async () => {
2489
+ const machineNow = await client.getMachine(id);
2490
+ return Object.freeze({
2491
+ settled: machineNow.id === id && machineNow.state === expectedState,
2492
+ observation: machineNow,
2493
+ details: Object.freeze({
2494
+ machine_id: id,
2495
+ expected_state: expectedState,
2496
+ observed_state: machineNow.state,
2497
+ }),
2498
+ });
2499
+ },
2500
+ });
2501
+ return Object.freeze({
2502
+ command: `machines.${action}`,
2503
+ data: Object.freeze({ ...machineRecord(observed), state_changed: !alreadyInState }),
2504
+ human: alreadyInState
2505
+ ? `Machine ${observed.name} was already ${observed.state}; nothing changed.`
2506
+ : `Machine ${observed.name} is ${observed.state}.`,
2507
+ });
2508
+ }
2509
+ if (action === "delete") {
2510
+ rejectUnknownOptions(parsed, ["yes"]);
2511
+ if (parsed.operands.length !== 2)
2512
+ throw usageError("machines delete requires exactly one machine ID.");
2513
+ requireConfirmation(parsed, "machines.delete");
2514
+ const id = assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
2515
+ await requireCapability({ client, scope: "machine", resourceId: id, capabilityId: "machines.delete", now: context.capabilityClock ?? now });
2516
+ await client.deleteMachine(id);
2517
+ // MEASURED 2026-08-19: the immediate read that used to stand here saw
2518
+ // `present` and the command reported `cuna.remote.postcondition_unverified`,
2519
+ // `retryable: false`, for a machine that `cuna machines list` showed gone six
2520
+ // seconds later. The producer accepts a durable delete before the resource
2521
+ // disappears from reads; the CLI has to let it.
2522
+ await convergeOnRemoteState(context, {
2523
+ operation: "machine deletion",
2524
+ settleWith: "cuna machines list",
2525
+ probe: async () => {
2526
+ try {
2527
+ const observed = await client.getMachine(id);
2528
+ return Object.freeze({
2529
+ settled: observed.state === "deleted",
2530
+ observation: undefined,
2531
+ details: Object.freeze({ machine_id: id, observed_state: observed.state }),
2532
+ });
2533
+ }
2534
+ catch (error) {
2535
+ if (error instanceof CunaError && error.code === "cuna.remote.not_found") {
2536
+ return Object.freeze({
2537
+ settled: true,
2538
+ observation: undefined,
2539
+ details: Object.freeze({ machine_id: id, observed_state: "absent" }),
2540
+ });
2541
+ }
2542
+ throw error;
2543
+ }
2544
+ },
2545
+ });
2546
+ return Object.freeze({ command: "machines.delete", data: { id, acknowledged: true }, human: `Delete acknowledged for ${id}.` });
2547
+ }
2548
+ throw usageError(`Unknown machines action ${action}.`);
2549
+ }
2550
+ function preflightExecutions(parsed) {
2551
+ const action = parsed.operands[0];
2552
+ rejectUnknownOptions(parsed, action === "list" ? ["machine", "execution-workspace-id", "after"] :
2553
+ action === "cancel" ? ["machine", "yes"] : ["machine"]);
2554
+ assertMachineId(requireOption(parsed, "machine", "Run `cuna machines list` to find a Machine ID."));
2555
+ if (action === "list") {
2556
+ if (parsed.operands.length !== 1)
2557
+ throw usageError("executions list accepts no operands.");
2558
+ for (const name of ["execution-workspace-id", "after"]) {
2559
+ const value = stringOption(parsed, name);
2560
+ if (value !== undefined)
2561
+ assertCanonicalUuid(value, name);
2562
+ }
2563
+ return;
2564
+ }
2565
+ if ((action !== "get" && action !== "cancel") || parsed.operands.length !== 2) {
2566
+ throw usageError("executions requires list, get EXECUTION_ID, or cancel EXECUTION_ID.");
2567
+ }
2568
+ assertCanonicalUuid(requireOperand(parsed.operands, 1, "execution ID"), "execution ID");
2569
+ if (action === "cancel")
2570
+ requireConfirmation(parsed, "executions.cancel");
2571
+ }
2572
+ function executionRecord(item) {
2573
+ return Object.freeze({ operation_id: item.operationId, machine_id: item.machineId,
2574
+ execution_workspace_id: item.executionWorkspaceId, leader_state: item.leaderState, ownership_state: item.ownershipState,
2575
+ cancel_requested: item.cancelRequested, exit_code: item.exitCode, duration_ms: item.durationMs,
2576
+ reason: item.reason, created_at: item.createdAt, observed_at: item.observedAt });
2577
+ }
2578
+ function executionLine(item) {
2579
+ return `${item.operationId}\tleader=${item.leaderState}\townership=${item.ownershipState}` +
2580
+ `\tworkspace=${item.executionWorkspaceId ?? "legacy"}` +
2581
+ (item.exitCode === null ? "" : `\texit=${item.exitCode}`) +
2582
+ (item.cancelRequested ? "\tcancellation requested" : "") +
2583
+ (item.reason === null ? "" : `\t${item.reason}`);
2584
+ }
2585
+ async function executeExecutions(context) {
2586
+ preflightExecutions(context.parsed);
2587
+ requireCredential(context);
2588
+ const { parsed, client } = context;
2589
+ const machineId = requireOption(parsed, "machine");
2590
+ const action = parsed.operands[0];
2591
+ // Recovery reads and cancellation remain usable without a live supervisor.
2592
+ // The server authorizes exact ownership; machines.exec availability describes
2593
+ // new admission and must not hide an outstanding operation during an outage.
2594
+ if (action === "list") {
2595
+ const executionWorkspaceId = stringOption(parsed, "execution-workspace-id");
2596
+ const after = stringOption(parsed, "after");
2597
+ const page = await client.listManagedExecutions(machineId, {
2598
+ ...(executionWorkspaceId === undefined ? {} : { executionWorkspaceId }), ...(after === undefined ? {} : { after }),
2599
+ });
2600
+ return Object.freeze({ command: "executions.list", data: { machine_id: page.machineId,
2601
+ items: page.items.map(executionRecord), next_cursor: page.nextCursor },
2602
+ human: `Remote executions on Machine ${machineId}\n` +
2603
+ (page.items.length === 0 ? "No executions in this page." : page.items.map(executionLine).join("\n")) +
2604
+ (page.nextCursor === null ? "" : `\nNext: cuna executions list --machine ${machineId}` +
2605
+ (executionWorkspaceId === undefined ? "" : ` --execution-workspace-id ${executionWorkspaceId}`) + ` --after ${page.nextCursor}`),
2606
+ });
2607
+ }
2608
+ const operationId = requireOperand(parsed.operands, 1, "execution ID");
2609
+ const item = action === "cancel" ? await client.cancelManagedExecution(machineId, operationId) :
2610
+ await client.getManagedExecution(machineId, operationId);
2611
+ const followup = `cuna executions get ${operationId} --machine ${machineId}`;
2612
+ return Object.freeze({ command: `executions.${action}`, data: executionRecord(item),
2613
+ human: (action === "cancel" ? "Cancellation accepted.\n" : "") + executionLine(item) +
2614
+ (item.ownershipState === "cleared" ? "\nProcess ownership is cleared." :
2615
+ `\nProcess cleanup is not confirmed. Inspect: ${followup}`),
2616
+ });
2617
+ }
2618
+ async function executeAgentSessions(context) {
2619
+ const { parsed, client, now } = context;
2620
+ requireCredential(context);
2621
+ const action = requireOperand(parsed.operands, 0, "agent-sessions action");
2622
+ if (action === "list") {
2623
+ rejectUnknownOptions(parsed, ["machine", "limit", "cursor"]);
2624
+ if (parsed.operands.length !== 1)
2625
+ throw usageError("agent-sessions list accepts no operands.");
2626
+ const machineId = assertMachineId(requireOption(parsed, "machine", "Run `cuna machines list` to find a machine ID."));
2627
+ const limit = integerOption(parsed, "limit", 1, 100);
2628
+ const cursor = stringOption(parsed, "cursor");
2629
+ const page = await client.listAgentSessions(machineId, {
2630
+ ...(limit === undefined ? {} : { limit }),
2631
+ ...(cursor === undefined ? {} : { cursor }),
2632
+ });
2633
+ const items = page.items.map((session) => agentSessionRecord(session));
2634
+ return Object.freeze({
2635
+ command: "agent-sessions.list",
2636
+ data: { items, ...(page.nextCursor === undefined ? {} : { next_cursor: page.nextCursor }) },
2637
+ human: [
2638
+ ...(items.length === 0
2639
+ ? ["No AgentSessions found."]
2640
+ : page.items.map((item) => `${item.id}\t${item.name}\t${item.agent}\t${agentSessionStateLabel(item)}\t${item.cwd}`)),
2641
+ ...truncationFooter(page.nextCursor),
2642
+ ].join("\n"),
2643
+ });
2644
+ }
2645
+ if (action === "get") {
2646
+ rejectUnknownOptions(parsed, []);
2647
+ if (parsed.operands.length !== 2)
2648
+ throw usageError("agent-sessions get requires exactly one AgentSession ID.");
2649
+ const id = assertCanonicalUuid(requireOperand(parsed.operands, 1, "AgentSession ID"), "AgentSession ID");
2650
+ const session = await client.getAgentSession(id);
2651
+ return Object.freeze({ command: "agent-sessions.get", data: agentSessionRecord(session), human: `${session.id}\t${session.name}\t${session.agent}\t${agentSessionStateLabel(session)}\t${session.cwd}` });
2652
+ }
2653
+ if (action === "create") {
2654
+ rejectUnknownOptions(parsed, [
2655
+ "machine", "workspace-binding-id", "workspace-generation", "name", "agent", "cwd",
2656
+ "auth-mode", "credential-binding", "yes", "idempotency-key",
2657
+ ]);
2658
+ if (parsed.operands.length !== 1)
2659
+ throw usageError("agent-sessions create accepts no operands.");
2660
+ requireConfirmation(parsed, "agent-sessions.create");
2661
+ const machineId = assertMachineId(requireOption(parsed, "machine", "Run `cuna machines list` to find a machine ID."));
2662
+ // Nothing in the product publishes a binding id, so both options are
2663
+ // resolved from `.cuna/workspace.json`, which the journey commands write.
2664
+ // An explicit flag still wins, so a caller who knows better decides.
2665
+ const givenBindingId = stringOption(parsed, "workspace-binding-id");
2666
+ const givenGeneration = integerOption(parsed, "workspace-generation", 1, Number.MAX_SAFE_INTEGER);
2667
+ // Read the folder only when something is missing: a fully specified
2668
+ // invocation costs no extra round trip.
2669
+ const localBinding = givenBindingId !== undefined && givenGeneration !== undefined
2670
+ ? undefined
2671
+ : await resolveLocalWorkspaceBinding(context);
2672
+ const workspaceBindingId = assertCanonicalUuid(givenBindingId ?? localBinding?.bindingId ?? "", "workspace binding ID");
2673
+ const workspaceGeneration = givenGeneration ?? localBinding?.generation;
2674
+ if (workspaceGeneration === undefined) {
2675
+ throw usageError("Option --workspace-generation is required.");
2676
+ }
2677
+ const agent = agentOption(parsed, true);
2678
+ if (agent === undefined)
2679
+ throw usageError("Option --agent is required.");
2680
+ const cwd = assertSafeDisplayText(stringOption(parsed, "cwd") ?? "/workspace", "workspace path");
2681
+ if (!cwd.startsWith("/workspace") || cwd.split("/").includes("..") || cwd.length > 1024) {
2682
+ throw usageError("Option --cwd must be a safe absolute path inside /workspace.");
2683
+ }
2684
+ const rawName = stringOption(parsed, "name");
2685
+ const name = rawName === undefined ? undefined : assertSafeDisplayText(rawName, "AgentSession name");
2686
+ if (name !== undefined && (name.length < 1 || name.length > 80)) {
2687
+ throw usageError("Option --name must contain 1 through 80 characters.");
2688
+ }
2689
+ const credentialBinding = stringOption(parsed, "credential-binding");
2690
+ const rawAuthMode = stringOption(parsed, "auth-mode");
2691
+ if (agent === "opencode" &&
2692
+ (rawAuthMode === "credential_binding" || credentialBinding !== undefined)) {
2693
+ throw usageError("OpenCode supports interactive_login only; credential bindings are not accepted.", "Use OpenCode's interactive provider flow and omit --credential-binding.");
2694
+ }
2695
+ const requestedAuthMode = normalizedAgentSessionAuthMode(rawAuthMode, credentialBinding);
2696
+ const authMode = agent === "opencode" ? "interactive_login" : requestedAuthMode;
2697
+ const machine = await client.getMachine(machineId);
2698
+ if (!machineSupportsProvider(machine, agent)) {
2699
+ const installed = machineProviderAvailability(machine);
2700
+ throw new CunaError({
2701
+ code: "cuna.agent.provider_not_installed",
2702
+ message: `${providerDisplayName(agent)} is unavailable on machine ${machine.name}. Declared installed provider: ${installed.displayName}.`,
2703
+ exitCode: EXIT_CODES.unsupported,
2704
+ hint: `Choose a machine whose installed provider is ${providerDisplayName(agent)}, or create one with \`cuna machines create --agent ${agent} ...\`.`,
2705
+ details: {
2706
+ machine_id: machine.id,
2707
+ requested_provider: agent,
2708
+ installed_provider: installed.declaredId ?? "unknown",
2709
+ },
2710
+ });
2711
+ }
2712
+ try {
2713
+ await requireCapability({ client, scope: "machine", resourceId: machineId, capabilityId: "agent_sessions.create", now: context.capabilityClock ?? now });
2714
+ }
2715
+ catch (error) {
2716
+ if (agent === "opencode" && isOpenCodeSupervisorUpgradeCapabilityRejection(error)) {
2717
+ throw openCodeSupervisorUpgradeRequired({
2718
+ ...(error.details === undefined ? {} : { details: error.details }),
2719
+ machineId,
2720
+ cause: error,
2721
+ });
2722
+ }
2723
+ if (agent === "opencode" && isOpenCodeRuntimeUnverifiedCapabilityRejection(error)) {
2724
+ throw openCodeRuntimeUnverified({
2725
+ ...(error.details === undefined ? {} : { details: error.details }),
2726
+ machineId,
2727
+ cause: error,
2728
+ });
2729
+ }
2730
+ throw error;
2731
+ }
2732
+ const session = await client.createAgentSession(machineId, {
2733
+ ...(name === undefined ? {} : { name }),
2734
+ agent,
2735
+ cwd,
2736
+ workspaceBindingId,
2737
+ workspaceGeneration,
2738
+ ...(authMode === undefined ? {} : { authMode }),
2739
+ ...(credentialBinding === undefined
2740
+ ? {}
2741
+ : { credentialBindingId: assertCanonicalUuid(credentialBinding, "credential binding ID") }),
2742
+ }, idempotencyKey(parsed));
2743
+ const observed = await client.getAgentSession(session.id);
2744
+ if (observed.id !== session.id || observed.machineId !== machineId || observed.agent !== agent ||
2745
+ observed.cwd !== cwd || observed.workspaceBindingId !== workspaceBindingId ||
2746
+ observed.workspaceGeneration !== workspaceGeneration ||
2747
+ (name !== undefined && observed.name !== name) ||
2748
+ (authMode !== undefined && observed.authMode !== authMode)) {
2749
+ postconditionUnverified("AgentSession creation", {
2750
+ agent_session_id: session.id,
2751
+ machine_id: machineId,
2752
+ observed_agent: observed.agent,
2753
+ });
2754
+ }
2755
+ return Object.freeze({ command: "agent-sessions.create", data: agentSessionRecord(observed), human: `Created ${observed.agent} AgentSession ${observed.id}.` });
2756
+ }
2757
+ if (action === "terminate") {
2758
+ rejectUnknownOptions(parsed, ["yes"]);
2759
+ if (parsed.operands.length !== 2)
2760
+ throw usageError("agent-sessions terminate requires exactly one AgentSession ID.");
2761
+ requireConfirmation(parsed, "agent-sessions.terminate");
2762
+ const id = assertCanonicalUuid(requireOperand(parsed.operands, 1, "AgentSession ID"), "AgentSession ID");
2763
+ await requireCapability({ client, scope: "agent_session", resourceId: id, capabilityId: "agent_sessions.terminate", now: context.capabilityClock ?? now });
2764
+ await client.terminateAgentSession(id);
2765
+ const observed = await convergeOnRemoteState(context, {
2766
+ operation: "AgentSession termination",
2767
+ settleWith: `cuna agent-sessions get ${id}`,
2768
+ probe: async () => {
2769
+ const session = await client.getAgentSession(id);
2770
+ return Object.freeze({
2771
+ settled: session.id === id && agentSessionTerminationConfirmed(session),
2772
+ observation: session,
2773
+ details: Object.freeze({
2774
+ agent_session_id: id,
2775
+ observed_desired_state: session.desiredState,
2776
+ observed_request_state: session.requestState,
2777
+ observed_process_state: session.processState,
2778
+ }),
2779
+ });
2780
+ },
2781
+ });
2782
+ return Object.freeze({ command: "agent-sessions.terminate", data: agentSessionRecord(observed), human: `AgentSession ${observed.id} is ${observed.requestState}/${observed.processState}.` });
2783
+ }
2784
+ if (action === "rename") {
2785
+ rejectUnknownOptions(parsed, ["name", "yes"]);
2786
+ if (parsed.operands.length !== 2)
2787
+ throw usageError("agent-sessions rename requires exactly one AgentSession ID.");
2788
+ requireConfirmation(parsed, "agent-sessions.rename");
2789
+ const id = assertCanonicalUuid(requireOperand(parsed.operands, 1, "AgentSession ID"), "AgentSession ID");
2790
+ const rawName = stringOption(parsed, "name");
2791
+ const name = rawName === undefined ? undefined : assertSafeDisplayText(rawName, "AgentSession name");
2792
+ if (name === undefined || name.length < 1 || name.length > 80) {
2793
+ throw usageError("Option --name must contain 1 through 80 characters.");
2794
+ }
2795
+ await requireCapability({ client, scope: "agent_session", resourceId: id, capabilityId: "agent_sessions.rename", now: context.capabilityClock ?? now });
2796
+ await client.renameAgentSession(id, name);
2797
+ const observed = await client.getAgentSession(id);
2798
+ if (observed.id !== id || observed.name !== name) {
2799
+ postconditionUnverified("AgentSession rename", { agent_session_id: id, expected_name: name, observed_name: observed.name });
2800
+ }
2801
+ return Object.freeze({
2802
+ command: "agent-sessions.rename",
2803
+ data: agentSessionRecord(observed),
2804
+ human: `Renamed AgentSession ${observed.id} to ${observed.name}.`,
2805
+ });
2806
+ }
2807
+ if (action === "attach") {
2808
+ // See the root command arm above: runCli intercepts this public path after
2809
+ // exact identity and TTY admission. The generic dispatcher cannot attach.
2810
+ throw unsupportedError("AgentSession attach", "run_cli_composition_required");
2811
+ }
2812
+ throw usageError(`Unknown agent-sessions action ${action}.`);
2813
+ }
2814
+ //# sourceMappingURL=commands.js.map