@provos/ironcurtain 0.11.0 → 0.13.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 (472) hide show
  1. package/README.md +23 -19
  2. package/dist/cli-help.d.ts +12 -0
  3. package/dist/cli-help.js +21 -0
  4. package/dist/cli-help.js.map +1 -1
  5. package/dist/cli.js +14 -6
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config/agent-model-guard.d.ts +14 -0
  8. package/dist/config/agent-model-guard.js +24 -0
  9. package/dist/config/agent-model-guard.js.map +1 -0
  10. package/dist/config/config-command.d.ts +33 -1
  11. package/dist/config/config-command.js +723 -11
  12. package/dist/config/config-command.js.map +1 -1
  13. package/dist/config/first-start.js +2 -8
  14. package/dist/config/first-start.js.map +1 -1
  15. package/dist/config/model-provider.d.ts +9 -0
  16. package/dist/config/model-provider.js +13 -0
  17. package/dist/config/model-provider.js.map +1 -1
  18. package/dist/config/openrouter-catalog.d.ts +46 -0
  19. package/dist/config/openrouter-catalog.js +111 -0
  20. package/dist/config/openrouter-catalog.js.map +1 -0
  21. package/dist/config/openrouter-models-fallback.d.ts +15 -0
  22. package/dist/config/openrouter-models-fallback.js +49 -0
  23. package/dist/config/openrouter-models-fallback.js.map +1 -0
  24. package/dist/config/paths.d.ts +48 -6
  25. package/dist/config/paths.js +63 -8
  26. package/dist/config/paths.js.map +1 -1
  27. package/dist/config/types.d.ts +8 -0
  28. package/dist/config/user-config.d.ts +179 -6
  29. package/dist/config/user-config.js +282 -5
  30. package/dist/config/user-config.js.map +1 -1
  31. package/dist/daemon/daemon-command.js +14 -5
  32. package/dist/daemon/daemon-command.js.map +1 -1
  33. package/dist/daemon/ironcurtain-daemon.d.ts +18 -0
  34. package/dist/daemon/ironcurtain-daemon.js +28 -1
  35. package/dist/daemon/ironcurtain-daemon.js.map +1 -1
  36. package/dist/daemon-client/daemon-client.d.ts +123 -0
  37. package/dist/daemon-client/daemon-client.js +324 -0
  38. package/dist/daemon-client/daemon-client.js.map +1 -0
  39. package/dist/docker/adapters/claude-code.js +109 -1
  40. package/dist/docker/adapters/claude-code.js.map +1 -1
  41. package/dist/docker/adapters/codex.d.ts +18 -0
  42. package/dist/docker/adapters/codex.js +376 -0
  43. package/dist/docker/adapters/codex.js.map +1 -0
  44. package/dist/docker/adapters/goose.js +38 -2
  45. package/dist/docker/adapters/goose.js.map +1 -1
  46. package/dist/docker/agent-adapter.d.ts +34 -6
  47. package/dist/docker/agent-adapter.js +2 -0
  48. package/dist/docker/agent-adapter.js.map +1 -1
  49. package/dist/docker/agent-registry.js +5 -0
  50. package/dist/docker/agent-registry.js.map +1 -1
  51. package/dist/docker/apple-container-manager.d.ts +49 -0
  52. package/dist/docker/apple-container-manager.js +486 -0
  53. package/dist/docker/apple-container-manager.js.map +1 -0
  54. package/dist/docker/ca.js +9 -2
  55. package/dist/docker/ca.js.map +1 -1
  56. package/dist/docker/code-mode-proxy.d.ts +11 -0
  57. package/dist/docker/code-mode-proxy.js +3 -1
  58. package/dist/docker/code-mode-proxy.js.map +1 -1
  59. package/dist/docker/container-lifecycle.d.ts +2 -2
  60. package/dist/docker/container-lifecycle.js +9 -0
  61. package/dist/docker/container-lifecycle.js.map +1 -1
  62. package/dist/docker/container-runtime.d.ts +29 -0
  63. package/dist/docker/container-runtime.js +76 -0
  64. package/dist/docker/container-runtime.js.map +1 -0
  65. package/dist/docker/docker-agent-session.d.ts +31 -0
  66. package/dist/docker/docker-agent-session.js +97 -7
  67. package/dist/docker/docker-agent-session.js.map +1 -1
  68. package/dist/docker/docker-infrastructure.d.ts +232 -15
  69. package/dist/docker/docker-infrastructure.js +762 -118
  70. package/dist/docker/docker-infrastructure.js.map +1 -1
  71. package/dist/docker/docker-manager.d.ts +25 -5
  72. package/dist/docker/docker-manager.js +388 -21
  73. package/dist/docker/docker-manager.js.map +1 -1
  74. package/dist/docker/docker-probe.d.ts +45 -0
  75. package/dist/docker/docker-probe.js +86 -0
  76. package/dist/docker/docker-probe.js.map +1 -0
  77. package/dist/docker/docker-progress-sink.d.ts +1 -1
  78. package/dist/docker/docker-resource-lifecycle.d.ts +71 -0
  79. package/dist/docker/docker-resource-lifecycle.js +454 -0
  80. package/dist/docker/docker-resource-lifecycle.js.map +1 -0
  81. package/dist/docker/gc-command.d.ts +2 -0
  82. package/dist/docker/gc-command.js +37 -0
  83. package/dist/docker/gc-command.js.map +1 -0
  84. package/dist/docker/mitm-proxy.d.ts +58 -5
  85. package/dist/docker/mitm-proxy.js +338 -26
  86. package/dist/docker/mitm-proxy.js.map +1 -1
  87. package/dist/docker/network-topology.d.ts +88 -0
  88. package/dist/docker/network-topology.js +167 -0
  89. package/dist/docker/network-topology.js.map +1 -0
  90. package/dist/docker/oauth-credentials.d.ts +13 -0
  91. package/dist/docker/oauth-credentials.js +125 -0
  92. package/dist/docker/oauth-credentials.js.map +1 -1
  93. package/dist/docker/openrouter.d.ts +107 -0
  94. package/dist/docker/openrouter.js +236 -0
  95. package/dist/docker/openrouter.js.map +1 -0
  96. package/dist/docker/orientation.js +1 -1
  97. package/dist/docker/orientation.js.map +1 -1
  98. package/dist/docker/package-types.d.ts +1 -1
  99. package/dist/docker/platform.d.ts +3 -1
  100. package/dist/docker/platform.js +3 -1
  101. package/dist/docker/platform.js.map +1 -1
  102. package/dist/docker/provider-config.d.ts +35 -1
  103. package/dist/docker/provider-config.js +93 -3
  104. package/dist/docker/provider-config.js.map +1 -1
  105. package/dist/docker/provision-lock.d.ts +46 -0
  106. package/dist/docker/provision-lock.js +152 -0
  107. package/dist/docker/provision-lock.js.map +1 -0
  108. package/dist/docker/pty-session.d.ts +22 -0
  109. package/dist/docker/pty-session.js +238 -84
  110. package/dist/docker/pty-session.js.map +1 -1
  111. package/dist/docker/pty-types.d.ts +15 -0
  112. package/dist/docker/pty-types.js +8 -0
  113. package/dist/docker/pty-types.js.map +1 -1
  114. package/dist/docker/registry-proxy.d.ts +46 -0
  115. package/dist/docker/registry-proxy.js +255 -1
  116. package/dist/docker/registry-proxy.js.map +1 -1
  117. package/dist/docker/resource-limits.d.ts +14 -5
  118. package/dist/docker/resource-limits.js +11 -8
  119. package/dist/docker/resource-limits.js.map +1 -1
  120. package/dist/docker/sse-extractor.js +55 -5
  121. package/dist/docker/sse-extractor.js.map +1 -1
  122. package/dist/docker/stream-delay.d.ts +86 -0
  123. package/dist/docker/stream-delay.js +218 -0
  124. package/dist/docker/stream-delay.js.map +1 -0
  125. package/dist/docker/token-stream-types.d.ts +15 -0
  126. package/dist/docker/trajectory-capture.d.ts +79 -0
  127. package/dist/docker/trajectory-capture.js +521 -0
  128. package/dist/docker/trajectory-capture.js.map +1 -0
  129. package/dist/docker/trajectory-reassembler.d.ts +286 -0
  130. package/dist/docker/trajectory-reassembler.js +1010 -0
  131. package/dist/docker/trajectory-reassembler.js.map +1 -0
  132. package/dist/docker/trajectory-tap.d.ts +100 -0
  133. package/dist/docker/trajectory-tap.js +410 -0
  134. package/dist/docker/trajectory-tap.js.map +1 -0
  135. package/dist/docker/trajectory-types.d.ts +187 -0
  136. package/dist/docker/trajectory-types.js +43 -0
  137. package/dist/docker/trajectory-types.js.map +1 -0
  138. package/dist/docker/types.d.ts +116 -12
  139. package/dist/doctor/checks.d.ts +39 -6
  140. package/dist/doctor/checks.js +129 -25
  141. package/dist/doctor/checks.js.map +1 -1
  142. package/dist/doctor/doctor-command.js +39 -9
  143. package/dist/doctor/doctor-command.js.map +1 -1
  144. package/dist/doctor/mcp-liveness.d.ts +1 -1
  145. package/dist/doctor/mcp-liveness.js +12 -4
  146. package/dist/doctor/mcp-liveness.js.map +1 -1
  147. package/dist/escalation/escalation-watcher.js +6 -0
  148. package/dist/escalation/escalation-watcher.js.map +1 -1
  149. package/dist/{mux → escalation}/trusted-input.d.ts +7 -4
  150. package/dist/{mux → escalation}/trusted-input.js +8 -5
  151. package/dist/escalation/trusted-input.js.map +1 -0
  152. package/dist/hash.js.map +1 -1
  153. package/dist/index.js +65 -16
  154. package/dist/index.js.map +1 -1
  155. package/dist/mux/mux-app.d.ts +9 -0
  156. package/dist/mux/mux-app.js +18 -16
  157. package/dist/mux/mux-app.js.map +1 -1
  158. package/dist/mux/mux-command.js +26 -8
  159. package/dist/mux/mux-command.js.map +1 -1
  160. package/dist/mux/mux-input-handler.d.ts +26 -2
  161. package/dist/mux/mux-input-handler.js +93 -11
  162. package/dist/mux/mux-input-handler.js.map +1 -1
  163. package/dist/mux/mux-renderer.d.ts +2 -1
  164. package/dist/mux/mux-renderer.js +67 -0
  165. package/dist/mux/mux-renderer.js.map +1 -1
  166. package/dist/mux/provider-profile-snapshot.d.ts +44 -0
  167. package/dist/mux/provider-profile-snapshot.js +56 -0
  168. package/dist/mux/provider-profile-snapshot.js.map +1 -0
  169. package/dist/mux/types.d.ts +3 -2
  170. package/dist/mux/types.js +6 -2
  171. package/dist/mux/types.js.map +1 -1
  172. package/dist/observe/observe-command.js +112 -125
  173. package/dist/observe/observe-command.js.map +1 -1
  174. package/dist/persona/broad-policy-validator.d.ts +95 -0
  175. package/dist/persona/broad-policy-validator.js +171 -0
  176. package/dist/persona/broad-policy-validator.js.map +1 -0
  177. package/dist/persona/compile-persona-policy.d.ts +54 -2
  178. package/dist/persona/compile-persona-policy.js +29 -4
  179. package/dist/persona/compile-persona-policy.js.map +1 -1
  180. package/dist/persona/event-bus-progress-reporter.d.ts +57 -0
  181. package/dist/persona/event-bus-progress-reporter.js +66 -0
  182. package/dist/persona/event-bus-progress-reporter.js.map +1 -0
  183. package/dist/persona/persona-command.js +46 -76
  184. package/dist/persona/persona-command.js.map +1 -1
  185. package/dist/persona/persona-compile-orchestrator.d.ts +101 -0
  186. package/dist/persona/persona-compile-orchestrator.js +504 -0
  187. package/dist/persona/persona-compile-orchestrator.js.map +1 -0
  188. package/dist/persona/persona-service.d.ts +137 -0
  189. package/dist/persona/persona-service.js +263 -0
  190. package/dist/persona/persona-service.js.map +1 -0
  191. package/dist/persona/policy-mutation-audit.d.ts +109 -0
  192. package/dist/persona/policy-mutation-audit.js +179 -0
  193. package/dist/persona/policy-mutation-audit.js.map +1 -0
  194. package/dist/persona/rule-delta.d.ts +30 -0
  195. package/dist/persona/rule-delta.js +82 -0
  196. package/dist/persona/rule-delta.js.map +1 -0
  197. package/dist/persona/types.d.ts +12 -0
  198. package/dist/pipeline/annotate.js +10 -2
  199. package/dist/pipeline/annotate.js.map +1 -1
  200. package/dist/pipeline/compile.js +14 -2
  201. package/dist/pipeline/compile.js.map +1 -1
  202. package/dist/pipeline/constitution-compiler.js.map +1 -1
  203. package/dist/pipeline/generate-with-repair.js.map +1 -1
  204. package/dist/pipeline/pipeline-runner.d.ts +63 -4
  205. package/dist/pipeline/pipeline-runner.js +159 -50
  206. package/dist/pipeline/pipeline-runner.js.map +1 -1
  207. package/dist/pipeline/pipeline-shared.d.ts +32 -3
  208. package/dist/pipeline/pipeline-shared.js +76 -8
  209. package/dist/pipeline/pipeline-shared.js.map +1 -1
  210. package/dist/pipeline/refresh-lists.js +3 -5
  211. package/dist/pipeline/refresh-lists.js.map +1 -1
  212. package/dist/pipeline/scenario-schema-validator.js.map +1 -1
  213. package/dist/{mux → pty}/pty-bridge.d.ts +41 -3
  214. package/dist/{mux → pty}/pty-bridge.js +71 -18
  215. package/dist/pty/pty-bridge.js.map +1 -0
  216. package/dist/pty/resolve-ironcurtain-bin.d.ts +17 -0
  217. package/dist/pty/resolve-ironcurtain-bin.js +22 -0
  218. package/dist/pty/resolve-ironcurtain-bin.js.map +1 -0
  219. package/dist/sandbox/index.d.ts +19 -4
  220. package/dist/sandbox/index.js +31 -18
  221. package/dist/sandbox/index.js.map +1 -1
  222. package/dist/servers/fetch-server.js +2 -0
  223. package/dist/servers/fetch-server.js.map +1 -1
  224. package/dist/session/index.d.ts +1 -1
  225. package/dist/session/index.js +20 -2
  226. package/dist/session/index.js.map +1 -1
  227. package/dist/session/preflight.d.ts +20 -35
  228. package/dist/session/preflight.js +92 -109
  229. package/dist/session/preflight.js.map +1 -1
  230. package/dist/session/resource-budget-tracker.js +8 -0
  231. package/dist/session/resource-budget-tracker.js.map +1 -1
  232. package/dist/session/session-manager.d.ts +7 -0
  233. package/dist/session/session-manager.js.map +1 -1
  234. package/dist/session/types.d.ts +23 -0
  235. package/dist/signal/bot-command.js +6 -1
  236. package/dist/signal/bot-command.js.map +1 -1
  237. package/dist/signal/setup-signal.js +1 -1
  238. package/dist/signal/setup-signal.js.map +1 -1
  239. package/dist/signal/signal-bot-daemon.d.ts +1 -0
  240. package/dist/signal/signal-bot-daemon.js +9 -2
  241. package/dist/signal/signal-bot-daemon.js.map +1 -1
  242. package/dist/signal/signal-container.d.ts +3 -3
  243. package/dist/signal/signal-container.js +2 -2
  244. package/dist/signal/signal-container.js.map +1 -1
  245. package/dist/trusted-process/call-circuit-breaker.d.ts +6 -0
  246. package/dist/trusted-process/call-circuit-breaker.js +22 -3
  247. package/dist/trusted-process/call-circuit-breaker.js.map +1 -1
  248. package/dist/trusted-process/container-command.d.ts +35 -0
  249. package/dist/trusted-process/container-command.js +49 -0
  250. package/dist/trusted-process/container-command.js.map +1 -0
  251. package/dist/trusted-process/mcp-error-utils.js +4 -1
  252. package/dist/trusted-process/mcp-error-utils.js.map +1 -1
  253. package/dist/trusted-process/mcp-proxy-server.js +37 -3
  254. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  255. package/dist/trusted-process/policy-engine.js +13 -2
  256. package/dist/trusted-process/policy-engine.js.map +1 -1
  257. package/dist/trusted-process/tcp-server-transport.d.ts +19 -1
  258. package/dist/trusted-process/tcp-server-transport.js +56 -10
  259. package/dist/trusted-process/tcp-server-transport.js.map +1 -1
  260. package/dist/trusted-process/tool-call-coordinator.d.ts +22 -8
  261. package/dist/trusted-process/tool-call-coordinator.js +149 -65
  262. package/dist/trusted-process/tool-call-coordinator.js.map +1 -1
  263. package/dist/trusted-process/tool-call-pipeline.d.ts +8 -0
  264. package/dist/trusted-process/tool-call-pipeline.js +68 -34
  265. package/dist/trusted-process/tool-call-pipeline.js.map +1 -1
  266. package/dist/util/atomic-write.d.ts +31 -0
  267. package/dist/util/atomic-write.js +43 -0
  268. package/dist/util/atomic-write.js.map +1 -0
  269. package/dist/utils/preflight-checks.js +1 -3
  270. package/dist/utils/preflight-checks.js.map +1 -1
  271. package/dist/utils/stdio-guard.d.ts +1 -0
  272. package/dist/utils/stdio-guard.js +27 -0
  273. package/dist/utils/stdio-guard.js.map +1 -0
  274. package/dist/web-ui/__tests__/config-dispatch.test.d.ts +13 -0
  275. package/dist/web-ui/__tests__/config-dispatch.test.js +383 -0
  276. package/dist/web-ui/__tests__/config-dispatch.test.js.map +1 -0
  277. package/dist/web-ui/__tests__/json-rpc-dispatch.test.js +6 -0
  278. package/dist/web-ui/__tests__/json-rpc-dispatch.test.js.map +1 -1
  279. package/dist/web-ui/conn-registry.d.ts +14 -0
  280. package/dist/web-ui/conn-registry.js +25 -0
  281. package/dist/web-ui/conn-registry.js.map +1 -0
  282. package/dist/web-ui/dispatch/config-dispatch.d.ts +15 -0
  283. package/dist/web-ui/dispatch/config-dispatch.js +277 -0
  284. package/dist/web-ui/dispatch/config-dispatch.js.map +1 -0
  285. package/dist/web-ui/dispatch/escalation-dispatch.js +11 -2
  286. package/dist/web-ui/dispatch/escalation-dispatch.js.map +1 -1
  287. package/dist/web-ui/dispatch/persona-dispatch.d.ts +46 -2
  288. package/dist/web-ui/dispatch/persona-dispatch.js +209 -56
  289. package/dist/web-ui/dispatch/persona-dispatch.js.map +1 -1
  290. package/dist/web-ui/dispatch/pty-dispatch.d.ts +15 -0
  291. package/dist/web-ui/dispatch/pty-dispatch.js +66 -0
  292. package/dist/web-ui/dispatch/pty-dispatch.js.map +1 -0
  293. package/dist/web-ui/dispatch/session-dispatch.js +89 -6
  294. package/dist/web-ui/dispatch/session-dispatch.js.map +1 -1
  295. package/dist/web-ui/dispatch/types.d.ts +35 -0
  296. package/dist/web-ui/dispatch/types.js +42 -1
  297. package/dist/web-ui/dispatch/types.js.map +1 -1
  298. package/dist/web-ui/dispatch/workflow-dispatch.js +116 -5
  299. package/dist/web-ui/dispatch/workflow-dispatch.js.map +1 -1
  300. package/dist/web-ui/json-rpc-dispatch.js +4 -1
  301. package/dist/web-ui/json-rpc-dispatch.js.map +1 -1
  302. package/dist/web-ui/pty-session-manager.d.ts +234 -0
  303. package/dist/web-ui/pty-session-manager.js +599 -0
  304. package/dist/web-ui/pty-session-manager.js.map +1 -0
  305. package/dist/web-ui/web-event-bus.d.ts +34 -1
  306. package/dist/web-ui/web-event-bus.js.map +1 -1
  307. package/dist/web-ui/web-session-transport.js.map +1 -1
  308. package/dist/web-ui/web-ui-server.d.ts +22 -0
  309. package/dist/web-ui/web-ui-server.js +41 -0
  310. package/dist/web-ui/web-ui-server.js.map +1 -1
  311. package/dist/web-ui/web-ui-types.d.ts +212 -5
  312. package/dist/web-ui/web-ui-types.js.map +1 -1
  313. package/dist/web-ui-static/assets/index-CCVI4nEK.css +1 -0
  314. package/dist/web-ui-static/assets/index-Dm012gvY.js +114 -0
  315. package/dist/web-ui-static/index.html +2 -2
  316. package/dist/workflow/cli-shared.d.ts +62 -0
  317. package/dist/workflow/cli-shared.js +117 -0
  318. package/dist/workflow/cli-shared.js.map +1 -0
  319. package/dist/workflow/cli-support.js +0 -2
  320. package/dist/workflow/cli-support.js.map +1 -1
  321. package/dist/workflow/container-snapshots.d.ts +29 -0
  322. package/dist/workflow/container-snapshots.js +137 -0
  323. package/dist/workflow/container-snapshots.js.map +1 -0
  324. package/dist/workflow/daemon-gate-commands.d.ts +21 -0
  325. package/dist/workflow/daemon-gate-commands.js +595 -0
  326. package/dist/workflow/daemon-gate-commands.js.map +1 -0
  327. package/dist/workflow/discovery.d.ts +29 -0
  328. package/dist/workflow/discovery.js +67 -7
  329. package/dist/workflow/discovery.js.map +1 -1
  330. package/dist/workflow/lane-template.d.ts +89 -0
  331. package/dist/workflow/lane-template.js +141 -0
  332. package/dist/workflow/lane-template.js.map +1 -0
  333. package/dist/workflow/lint.d.ts +7 -1
  334. package/dist/workflow/lint.js +186 -3
  335. package/dist/workflow/lint.js.map +1 -1
  336. package/dist/workflow/machine-builder.d.ts +90 -2
  337. package/dist/workflow/machine-builder.js +222 -29
  338. package/dist/workflow/machine-builder.js.map +1 -1
  339. package/dist/workflow/message-log.d.ts +15 -1
  340. package/dist/workflow/message-log.js.map +1 -1
  341. package/dist/workflow/orchestrator.d.ts +505 -1
  342. package/dist/workflow/orchestrator.js +1725 -69
  343. package/dist/workflow/orchestrator.js.map +1 -1
  344. package/dist/workflow/prompt-builder.js +2 -1
  345. package/dist/workflow/prompt-builder.js.map +1 -1
  346. package/dist/workflow/run-state-command.js +1 -1
  347. package/dist/workflow/run-state-command.js.map +1 -1
  348. package/dist/workflow/shutdown-signals.d.ts +7 -0
  349. package/dist/workflow/shutdown-signals.js +40 -0
  350. package/dist/workflow/shutdown-signals.js.map +1 -0
  351. package/dist/workflow/status-parser.js +19 -4
  352. package/dist/workflow/status-parser.js.map +1 -1
  353. package/dist/workflow/types.d.ts +123 -23
  354. package/dist/workflow/types.js +2 -0
  355. package/dist/workflow/types.js.map +1 -1
  356. package/dist/workflow/validate.d.ts +23 -1
  357. package/dist/workflow/validate.js +311 -22
  358. package/dist/workflow/validate.js.map +1 -1
  359. package/dist/workflow/workflow-command.d.ts +0 -17
  360. package/dist/workflow/workflow-command.js +118 -94
  361. package/dist/workflow/workflow-command.js.map +1 -1
  362. package/dist/workflow/workflow-discovery.d.ts +2 -0
  363. package/dist/workflow/workflow-discovery.js +22 -1
  364. package/dist/workflow/workflow-discovery.js.map +1 -1
  365. package/dist/workflow/workflow-manager.d.ts +34 -0
  366. package/dist/workflow/workflow-manager.js +52 -1
  367. package/dist/workflow/workflow-manager.js.map +1 -1
  368. package/dist/workflow/workflows/design-and-code/README.md +50 -0
  369. package/dist/workflow/workflows/deterministic-eval-smoke/scripts/format_report.js +29 -0
  370. package/dist/workflow/workflows/deterministic-eval-smoke/scripts/package.json +5 -0
  371. package/dist/workflow/workflows/deterministic-eval-smoke/scripts/requirements.txt +1 -0
  372. package/dist/workflow/workflows/deterministic-eval-smoke/scripts/run_eval.py +41 -0
  373. package/dist/workflow/workflows/deterministic-eval-smoke/workflow.yaml +57 -0
  374. package/dist/workflow/workflows/deterministic-verdict-smoke/scripts/classify.py +18 -0
  375. package/dist/workflow/workflows/deterministic-verdict-smoke/workflow.yaml +51 -0
  376. package/dist/workflow/workflows/evolve/README.md +66 -0
  377. package/dist/workflow/workflows/evolve/scripts/LICENSE +201 -0
  378. package/dist/workflow/workflows/evolve/scripts/README.md +314 -0
  379. package/dist/workflow/workflows/evolve/scripts/evolve-brief +16 -0
  380. package/dist/workflow/workflows/evolve/scripts/evolve-cognition +16 -0
  381. package/dist/workflow/workflows/evolve/scripts/evolve-db +16 -0
  382. package/dist/workflow/workflows/evolve/scripts/evolve-eval +16 -0
  383. package/dist/workflow/workflows/evolve/scripts/evolve-files +16 -0
  384. package/dist/workflow/workflows/evolve/scripts/evolve-summary +16 -0
  385. package/dist/workflow/workflows/evolve/scripts/evolve_core/__init__.py +14 -0
  386. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/__init__.py +17 -0
  387. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/base.py +21 -0
  388. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/factory.py +90 -0
  389. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/greedy.py +17 -0
  390. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/island.py +316 -0
  391. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/random.py +18 -0
  392. package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/ucb1.py +55 -0
  393. package/dist/workflow/workflows/evolve/scripts/evolve_core/best_snapshot.py +62 -0
  394. package/dist/workflow/workflows/evolve/scripts/evolve_core/cli.py +752 -0
  395. package/dist/workflow/workflows/evolve/scripts/evolve_core/cognition.py +127 -0
  396. package/dist/workflow/workflows/evolve/scripts/evolve_core/database.py +218 -0
  397. package/dist/workflow/workflows/evolve/scripts/evolve_core/diff.py +57 -0
  398. package/dist/workflow/workflows/evolve/scripts/evolve_core/embedding.py +84 -0
  399. package/dist/workflow/workflows/evolve/scripts/evolve_core/file_lock.py +45 -0
  400. package/dist/workflow/workflows/evolve/scripts/evolve_core/run_state.py +343 -0
  401. package/dist/workflow/workflows/evolve/scripts/evolve_core/sampling_config.py +173 -0
  402. package/dist/workflow/workflows/evolve/scripts/evolve_core/structures.py +93 -0
  403. package/dist/workflow/workflows/evolve/scripts/evolve_core/vector_index.py +191 -0
  404. package/dist/workflow/workflows/evolve/scripts/evolve_result.py +1503 -0
  405. package/dist/workflow/workflows/evolve/scripts/requirements.txt +2 -0
  406. package/dist/workflow/workflows/evolve/workflow.yaml +646 -0
  407. package/dist/workflow/workflows/test-email-summary/workflow.yaml +1 -0
  408. package/dist/workflow/workflows/vuln-discovery/README.md +54 -0
  409. package/dist/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +6 -1
  410. package/dist/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +1 -0
  411. package/dist/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +4 -0
  412. package/dist/workflow/workflows/vuln-discovery/workflow.yaml +13 -8
  413. package/docker/Dockerfile.base +22 -0
  414. package/docker/Dockerfile.base.arm64 +18 -0
  415. package/docker/Dockerfile.claude-code +3 -1
  416. package/docker/Dockerfile.codex +17 -0
  417. package/docker/Dockerfile.goose +3 -1
  418. package/docker/entrypoint-claude-code.sh +28 -45
  419. package/docker/entrypoint-codex.sh +42 -0
  420. package/docker/entrypoint-goose.sh +5 -24
  421. package/docker/entrypoint-uid-remap.sh +93 -0
  422. package/package.json +39 -30
  423. package/src/workflow/workflows/design-and-code/README.md +50 -0
  424. package/src/workflow/workflows/deterministic-eval-smoke/scripts/format_report.js +29 -0
  425. package/src/workflow/workflows/deterministic-eval-smoke/scripts/package.json +5 -0
  426. package/src/workflow/workflows/deterministic-eval-smoke/scripts/requirements.txt +1 -0
  427. package/src/workflow/workflows/deterministic-eval-smoke/scripts/run_eval.py +41 -0
  428. package/src/workflow/workflows/deterministic-eval-smoke/workflow.yaml +57 -0
  429. package/src/workflow/workflows/deterministic-verdict-smoke/scripts/classify.py +18 -0
  430. package/src/workflow/workflows/deterministic-verdict-smoke/workflow.yaml +51 -0
  431. package/src/workflow/workflows/evolve/README.md +66 -0
  432. package/src/workflow/workflows/evolve/scripts/LICENSE +201 -0
  433. package/src/workflow/workflows/evolve/scripts/README.md +314 -0
  434. package/src/workflow/workflows/evolve/scripts/evolve-brief +16 -0
  435. package/src/workflow/workflows/evolve/scripts/evolve-cognition +16 -0
  436. package/src/workflow/workflows/evolve/scripts/evolve-db +16 -0
  437. package/src/workflow/workflows/evolve/scripts/evolve-eval +16 -0
  438. package/src/workflow/workflows/evolve/scripts/evolve-files +16 -0
  439. package/src/workflow/workflows/evolve/scripts/evolve-summary +16 -0
  440. package/src/workflow/workflows/evolve/scripts/evolve_core/__init__.py +14 -0
  441. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/__init__.py +17 -0
  442. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/base.py +21 -0
  443. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/factory.py +90 -0
  444. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/greedy.py +17 -0
  445. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/island.py +316 -0
  446. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/random.py +18 -0
  447. package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/ucb1.py +55 -0
  448. package/src/workflow/workflows/evolve/scripts/evolve_core/best_snapshot.py +62 -0
  449. package/src/workflow/workflows/evolve/scripts/evolve_core/cli.py +752 -0
  450. package/src/workflow/workflows/evolve/scripts/evolve_core/cognition.py +127 -0
  451. package/src/workflow/workflows/evolve/scripts/evolve_core/database.py +218 -0
  452. package/src/workflow/workflows/evolve/scripts/evolve_core/diff.py +57 -0
  453. package/src/workflow/workflows/evolve/scripts/evolve_core/embedding.py +84 -0
  454. package/src/workflow/workflows/evolve/scripts/evolve_core/file_lock.py +45 -0
  455. package/src/workflow/workflows/evolve/scripts/evolve_core/run_state.py +343 -0
  456. package/src/workflow/workflows/evolve/scripts/evolve_core/sampling_config.py +173 -0
  457. package/src/workflow/workflows/evolve/scripts/evolve_core/structures.py +93 -0
  458. package/src/workflow/workflows/evolve/scripts/evolve_core/vector_index.py +191 -0
  459. package/src/workflow/workflows/evolve/scripts/evolve_result.py +1503 -0
  460. package/src/workflow/workflows/evolve/scripts/requirements.txt +2 -0
  461. package/src/workflow/workflows/evolve/workflow.yaml +646 -0
  462. package/src/workflow/workflows/test-email-summary/workflow.yaml +1 -0
  463. package/src/workflow/workflows/vuln-discovery/README.md +54 -0
  464. package/src/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +6 -1
  465. package/src/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +1 -0
  466. package/src/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +4 -0
  467. package/src/workflow/workflows/vuln-discovery/workflow.yaml +13 -8
  468. package/dist/mux/pty-bridge.js.map +0 -1
  469. package/dist/mux/trusted-input.js.map +0 -1
  470. package/dist/web-ui-static/assets/index-BFO5z1js.js +0 -76
  471. package/dist/web-ui-static/assets/index-CCUwrkIL.css +0 -1
  472. package/packages/web-ui/README.md +0 -113
@@ -9,8 +9,9 @@ import * as p from '@clack/prompts';
9
9
  import { readFileSync } from 'node:fs';
10
10
  import { resolve, dirname } from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
- import { loadUserConfig, saveUserConfig, validateModelId, ESCALATION_TIMEOUT_MIN, ESCALATION_TIMEOUT_MAX, WEB_SEARCH_PROVIDERS, WEB_SEARCH_PROVIDER_LABELS, WEB_SEARCH_PROVIDER_URLS, GOOSE_PROVIDERS, DOCKER_AGENTS, SESSION_MODES, } from './user-config.js';
12
+ import { loadUserConfig, saveUserConfig, validateModelId, ESCALATION_TIMEOUT_MIN, ESCALATION_TIMEOUT_MAX, WEB_SEARCH_PROVIDERS, WEB_SEARCH_PROVIDER_LABELS, WEB_SEARCH_PROVIDER_URLS, GOOSE_PROVIDERS, DOCKER_AGENTS, SESSION_MODES, CONTAINER_RUNTIMES, NATIVE_PROFILE_NAME, DEFAULT_GLM_SLUG, maskApiKey, cloneProviderPreference, } from './user-config.js';
13
13
  import { getUserConfigPath } from './paths.js';
14
+ import { listOpenrouterModels, catalogEnforces, } from './openrouter-catalog.js';
14
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
16
  /** Known model options for selection prompts. */
16
17
  const KNOWN_MODELS = [
@@ -57,17 +58,97 @@ export function formatCost(n) {
57
58
  return 'disabled';
58
59
  return `$${n.toFixed(2)}`;
59
60
  }
60
- export function maskApiKey(key) {
61
- if (!key)
62
- return 'none';
63
- if (key.length <= 6)
64
- return '***';
65
- return key.slice(0, 3) + '...' + key.slice(-3);
66
- }
67
61
  function formatModelShort(id) {
68
62
  const known = KNOWN_MODELS.find((m) => m.value === id);
69
63
  return known ? known.label : id;
70
64
  }
65
+ /**
66
+ * The current editor view of the `profiles` record, in input shape (openrouter
67
+ * profiles carry the fields the editor mutates). A pending `profiles` record,
68
+ * when present, is already the whole record (read-modify-write) and replaces the
69
+ * resolved view; otherwise the resolved profiles are converted to input shape.
70
+ * The implicit `native` profile is intentionally excluded — it is never
71
+ * persisted and is always shown separately as read-only.
72
+ */
73
+ function currentProfiles(resolved, pending) {
74
+ if (pending.modelProviders?.profiles) {
75
+ return { ...pending.modelProviders.profiles };
76
+ }
77
+ const view = {};
78
+ for (const [name, profile] of Object.entries(resolved.modelProviders.profiles)) {
79
+ if (name === NATIVE_PROFILE_NAME)
80
+ continue;
81
+ view[name] = resolvedProfileToInput(profile);
82
+ }
83
+ return view;
84
+ }
85
+ /** Converts a resolved profile back to its input shape (drops resolution-only defaults where empty). */
86
+ function resolvedProfileToInput(profile) {
87
+ if (profile.type === 'native')
88
+ return { type: 'native' };
89
+ const input = { type: 'openrouter' };
90
+ if (profile.apiKey)
91
+ input.apiKey = profile.apiKey;
92
+ // A default-tracking profile OMITS `modelMap` so an edit round-trip re-persists
93
+ // the omission rather than pinning today's materialized DEFAULT_MODEL_MAP.
94
+ if (!profile.usesDefaultMap)
95
+ input.modelMap = profile.modelMap.map((r) => ({ match: r.match, model: r.model }));
96
+ const perAgent = {};
97
+ for (const agent of DOCKER_AGENTS) {
98
+ const slug = profile.perAgent[agent];
99
+ if (slug)
100
+ perAgent[agent] = slug;
101
+ }
102
+ if (Object.keys(perAgent).length > 0)
103
+ input.perAgent = perAgent;
104
+ if (profile.providerPreference) {
105
+ input.providerPreference = cloneProviderPreference(profile.providerPreference);
106
+ }
107
+ input.sessionAffinity = profile.sessionAffinity;
108
+ return input;
109
+ }
110
+ /** The current default profile name (pending override wins over the resolved value). */
111
+ function currentDefault(resolved, pending) {
112
+ return pending.modelProviders?.default ?? resolved.modelProviders.default;
113
+ }
114
+ /**
115
+ * F10 — re-point a dangling `default` to `native`. Returns `NATIVE_PROFILE_NAME`
116
+ * when the current default no longer names a profile in `remainingNames` (and is
117
+ * not itself `native`), otherwise returns the current default unchanged. Pure so
118
+ * the delete-repoints-default invariant is unit-testable without the interactive
119
+ * flow. Persisting a `default` that names a missing profile would make the next
120
+ * `loadUserConfig` a HARD error (the Zod `.refine`), so callers must apply this
121
+ * in the same pending write that deletes a profile.
122
+ */
123
+ export function repointDefaultAfterDelete(currentDefaultName, remainingNames) {
124
+ if (currentDefaultName === NATIVE_PROFILE_NAME)
125
+ return currentDefaultName;
126
+ return remainingNames.includes(currentDefaultName) ? currentDefaultName : NATIVE_PROFILE_NAME;
127
+ }
128
+ /** Compact one-line summary of an openrouter profile for list/hint rendering. */
129
+ function summarizeOpenrouterProfile(profile) {
130
+ const parts = [];
131
+ const map = profile.modelMap;
132
+ if (map && map.length === 0) {
133
+ parts.push('per-agent only');
134
+ }
135
+ else if (map && map.length > 0) {
136
+ parts.push(map.length === 1 ? `-> ${map[0].model}` : `${map.length} map rules`);
137
+ }
138
+ else {
139
+ parts.push('default map');
140
+ }
141
+ parts.push(`key: ${maskApiKey(profile.apiKey)}`);
142
+ return parts.join(', ');
143
+ }
144
+ /** Compact rendering of a modelMap for diffs: `*sonnet*->z-ai/glm-5.2; *opus*->…`. */
145
+ function formatModelMap(map) {
146
+ if (!map)
147
+ return 'default';
148
+ if (map.length === 0)
149
+ return 'per-agent only (empty map)';
150
+ return map.map((r) => `${r.match}->${r.model}`).join('; ');
151
+ }
71
152
  export function computeDiff(resolved, pending) {
72
153
  const diffs = [];
73
154
  const topLevelKeys = [
@@ -79,6 +160,7 @@ export function computeDiff(resolved, pending) {
79
160
  'gooseModel',
80
161
  'preferredDockerAgent',
81
162
  'preferredMode',
163
+ 'containerRuntime',
82
164
  ];
83
165
  for (const key of topLevelKeys) {
84
166
  if (key in pending && pending[key] !== undefined && pending[key] !== resolved[key]) {
@@ -92,6 +174,7 @@ export function computeDiff(resolved, pending) {
92
174
  'auditRedaction',
93
175
  'memory',
94
176
  'dockerResources',
177
+ 'snapshot',
95
178
  ];
96
179
  for (const section of nestedSections) {
97
180
  const pendingSection = pending[section];
@@ -140,8 +223,77 @@ export function computeDiff(resolved, pending) {
140
223
  }
141
224
  }
142
225
  }
226
+ diffModelProviders(resolved, pending, diffs);
143
227
  return diffs;
144
228
  }
229
+ /**
230
+ * Dedicated `modelProviders` diff branch (not the generic nestedSections loop,
231
+ * which compares by reference and would spuriously diff freshly-materialized
232
+ * profile objects — m14). Deep-compares each profile field via JSON.stringify so
233
+ * a no-op read-modify-write yields an EMPTY diff; masks every `apiKey`; renders
234
+ * modelMap rows compactly.
235
+ */
236
+ function diffModelProviders(resolved, pending, diffs) {
237
+ const pendingMp = pending.modelProviders;
238
+ if (!pendingMp)
239
+ return;
240
+ // default selector
241
+ const resolvedDefault = resolved.modelProviders.default;
242
+ if (pendingMp.default !== undefined && pendingMp.default !== resolvedDefault) {
243
+ diffs.push(['modelProviders.default', { from: resolvedDefault, to: pendingMp.default }]);
244
+ }
245
+ if (!pendingMp.profiles)
246
+ return;
247
+ const resolvedProfiles = resolved.modelProviders.profiles;
248
+ const pendingProfiles = pendingMp.profiles;
249
+ // Added / changed profiles.
250
+ for (const [name, pendingProfile] of Object.entries(pendingProfiles)) {
251
+ if (name === NATIVE_PROFILE_NAME)
252
+ continue;
253
+ const before = name in resolvedProfiles ? resolvedProfileToInput(resolvedProfiles[name]) : undefined;
254
+ diffOneProfile(name, before, pendingProfile, diffs);
255
+ }
256
+ // Removed profiles (present in resolved, absent in the whole-record pending write).
257
+ for (const name of Object.keys(resolvedProfiles)) {
258
+ if (name === NATIVE_PROFILE_NAME)
259
+ continue;
260
+ if (!(name in pendingProfiles)) {
261
+ diffs.push([`modelProviders.profiles.${name}`, { from: 'configured', to: 'removed' }]);
262
+ }
263
+ }
264
+ }
265
+ /** Field-by-field diff of one profile, masking apiKey and rendering modelMap compactly. */
266
+ function diffOneProfile(name, before, after, diffs) {
267
+ const prefix = `modelProviders.profiles.${name}`;
268
+ if (!before) {
269
+ diffs.push([prefix, { from: 'none', to: `added (${after.type})` }]);
270
+ return;
271
+ }
272
+ if (before.type !== after.type) {
273
+ diffs.push([`${prefix}.type`, { from: before.type, to: after.type }]);
274
+ return;
275
+ }
276
+ if (before.type === 'native' || after.type === 'native')
277
+ return;
278
+ if (before.apiKey !== after.apiKey) {
279
+ diffs.push([`${prefix}.apiKey`, { from: maskApiKey(before.apiKey), to: maskApiKey(after.apiKey) }]);
280
+ }
281
+ if (JSON.stringify(before.modelMap) !== JSON.stringify(after.modelMap)) {
282
+ diffs.push([`${prefix}.modelMap`, { from: formatModelMap(before.modelMap), to: formatModelMap(after.modelMap) }]);
283
+ }
284
+ if (JSON.stringify(before.perAgent) !== JSON.stringify(after.perAgent)) {
285
+ diffs.push([`${prefix}.perAgent`, { from: before.perAgent ?? {}, to: after.perAgent ?? {} }]);
286
+ }
287
+ if (JSON.stringify(before.providerPreference) !== JSON.stringify(after.providerPreference)) {
288
+ diffs.push([
289
+ `${prefix}.providerPreference`,
290
+ { from: before.providerPreference ?? 'default', to: after.providerPreference ?? 'default' },
291
+ ]);
292
+ }
293
+ if (before.sessionAffinity !== after.sessionAffinity) {
294
+ diffs.push([`${prefix}.sessionAffinity`, { from: before.sessionAffinity, to: after.sessionAffinity }]);
295
+ }
296
+ }
145
297
  function formatDiffValue(key, value) {
146
298
  if (value === null)
147
299
  return 'disabled';
@@ -155,7 +307,11 @@ function formatDiffValue(key, value) {
155
307
  return formatSeconds(value);
156
308
  if (key.includes('Cost'))
157
309
  return formatCost(value);
158
- return String(value);
310
+ if (typeof value === 'string')
311
+ return value;
312
+ if (typeof value === 'number')
313
+ return String(value);
314
+ return JSON.stringify(value);
159
315
  }
160
316
  // ─── Model prompt ────────────────────────────────────────────
161
317
  async function promptModelId(message, current) {
@@ -365,6 +521,76 @@ async function handleMemory(resolved, pending) {
365
521
  }
366
522
  }
367
523
  }
524
+ async function handleSnapshot(resolved, pending) {
525
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- interactive loop exited via return
526
+ while (true) {
527
+ const currentEnabled = pending.snapshot?.enabled ?? resolved.snapshot.enabled;
528
+ const currentMaxAge = pending.snapshot?.maxAgeDays ?? resolved.snapshot.maxAgeDays;
529
+ const currentSweep = pending.snapshot?.sweepIntervalHours ?? resolved.snapshot.sweepIntervalHours;
530
+ const field = await p.select({
531
+ message: 'Container Snapshots',
532
+ options: [
533
+ {
534
+ value: 'enabled',
535
+ label: 'Enabled (kill switch — snapshot capture + GC for all workflows)',
536
+ hint: currentEnabled ? 'on' : 'off',
537
+ },
538
+ {
539
+ value: 'maxAgeDays',
540
+ label: 'Max snapshot age before GC (days)',
541
+ hint: currentMaxAge === null ? 'unbounded' : String(currentMaxAge),
542
+ },
543
+ { value: 'sweepIntervalHours', label: 'GC sweep interval (hours)', hint: String(currentSweep) },
544
+ { value: 'back', label: 'Back' },
545
+ ],
546
+ });
547
+ if (isCancelled(field) || field === 'back')
548
+ return;
549
+ if (field === 'enabled') {
550
+ const enabled = await p.confirm({
551
+ message: 'Enable workflow container snapshots globally? ' +
552
+ '(off disables snapshot capture on stop and GC for all workflows)',
553
+ initialValue: currentEnabled,
554
+ });
555
+ if (isCancelled(enabled))
556
+ continue;
557
+ if (enabled !== currentEnabled) {
558
+ pending.snapshot = { ...pending.snapshot, enabled: enabled };
559
+ }
560
+ }
561
+ else if (field === 'maxAgeDays') {
562
+ const next = await promptNullableNumber({
563
+ message: 'Max snapshot age before automatic GC',
564
+ current: currentMaxAge,
565
+ validate: (n) => (n > 0 ? undefined : 'Must be a positive number of days'),
566
+ format: (n) => (n === null ? 'unbounded' : `${n} day(s)`),
567
+ });
568
+ if (next !== undefined && next !== currentMaxAge) {
569
+ pending.snapshot = { ...pending.snapshot, maxAgeDays: next };
570
+ }
571
+ }
572
+ else if (field === 'sweepIntervalHours') {
573
+ const input = await p.text({
574
+ message: 'GC sweep interval (hours):',
575
+ placeholder: String(currentSweep),
576
+ validate: (val) => {
577
+ if (!val || val.trim() === '')
578
+ return 'Must be a positive number';
579
+ const n = Number(val);
580
+ if (!Number.isFinite(n) || n <= 0)
581
+ return 'Must be positive';
582
+ return undefined;
583
+ },
584
+ });
585
+ if (isCancelled(input))
586
+ continue;
587
+ const newVal = Number(input);
588
+ if (newVal !== currentSweep) {
589
+ pending.snapshot = { ...pending.snapshot, sweepIntervalHours: newVal };
590
+ }
591
+ }
592
+ }
593
+ }
368
594
  async function handleResourceLimits(resolved, pending) {
369
595
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- interactive loop exited via return
370
596
  while (true) {
@@ -576,6 +802,437 @@ async function handleWebSearch(resolved, pending) {
576
802
  };
577
803
  }
578
804
  }
805
+ // ─── Model Providers ─────────────────────────────────────────
806
+ /** Returns a shallow copy of `obj` without the given key (avoids `delete obj[dynamicKey]`). */
807
+ function omitKey(obj, key) {
808
+ const copy = { ...obj };
809
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- key is intentionally dynamic here
810
+ delete copy[key];
811
+ return copy;
812
+ }
813
+ /**
814
+ * Writes the whole `profiles` record + `default` into pending (read-modify-write,
815
+ * required by the shallow `deepMergeConfig`). Callers always pass the complete
816
+ * view so no unmentioned profile is dropped unintentionally.
817
+ */
818
+ function commitModelProviders(pending, profiles, defaultName) {
819
+ pending.modelProviders = { default: defaultName, profiles };
820
+ }
821
+ async function handleModelProviders(resolved, pending) {
822
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- interactive loop exited via return
823
+ while (true) {
824
+ const profiles = currentProfiles(resolved, pending);
825
+ const defaultName = currentDefault(resolved, pending);
826
+ const options = [
827
+ {
828
+ value: `profile:${NATIVE_PROFILE_NAME}`,
829
+ label: NATIVE_PROFILE_NAME,
830
+ hint: defaultName === NATIVE_PROFILE_NAME ? 'native routing (default, not editable)' : 'native routing',
831
+ },
832
+ ];
833
+ for (const [name, profile] of Object.entries(profiles)) {
834
+ const summary = profile.type === 'openrouter' ? summarizeOpenrouterProfile(profile) : 'native';
835
+ options.push({
836
+ value: `profile:${name}`,
837
+ label: name,
838
+ hint: `${profile.type}, ${summary}${name === defaultName ? ' (default)' : ''}`,
839
+ });
840
+ }
841
+ options.push({ value: 'add', label: 'Add profile...' });
842
+ options.push({ value: 'default', label: 'Set default', hint: defaultName });
843
+ options.push({ value: 'back', label: 'Back' });
844
+ const action = await p.select({ message: 'Model Providers', options });
845
+ if (isCancelled(action) || action === 'back')
846
+ return;
847
+ if (action === 'add') {
848
+ await addProfile(resolved, pending);
849
+ }
850
+ else if (action === 'default') {
851
+ await setDefaultProfile(resolved, pending);
852
+ }
853
+ else if (typeof action === 'string' && action.startsWith('profile:')) {
854
+ const name = action.slice('profile:'.length);
855
+ if (name === NATIVE_PROFILE_NAME) {
856
+ p.note('The native profile uses today’s canonical routing and cannot be edited or deleted.', 'native');
857
+ continue;
858
+ }
859
+ await editProfile(resolved, pending, name);
860
+ }
861
+ }
862
+ }
863
+ async function addProfile(resolved, pending) {
864
+ const profiles = currentProfiles(resolved, pending);
865
+ const name = await p.text({
866
+ message: 'Profile name:',
867
+ placeholder: 'e.g. glm-5.2',
868
+ validate: (val) => {
869
+ if (!val || val.trim() === '')
870
+ return 'Name is required';
871
+ if (val === NATIVE_PROFILE_NAME)
872
+ return `"${NATIVE_PROFILE_NAME}" is a reserved profile name.`;
873
+ if (val in profiles)
874
+ return `Profile "${val}" already exists.`;
875
+ return undefined;
876
+ },
877
+ });
878
+ if (isCancelled(name))
879
+ return;
880
+ // v1 supports only the 'openrouter' type; native is implicit and never user-defined.
881
+ p.note('OpenRouter routes Docker agents through openrouter.ai with a bound model map + key.\n' +
882
+ 'Paste an sk-or-v1-... key; defaults map *sonnet*/*opus*/*haiku* -> ' +
883
+ `${DEFAULT_GLM_SLUG} with a soft z-ai cache pin.`, 'openrouter');
884
+ const apiKey = await p.text({
885
+ message: 'OpenRouter API key (sk-or-v1-...):',
886
+ placeholder: 'set via OPENROUTER_API_KEY env, or leave blank',
887
+ validate: () => undefined,
888
+ });
889
+ if (isCancelled(apiKey))
890
+ return;
891
+ const profile = { type: 'openrouter' };
892
+ if (apiKey)
893
+ profile.apiKey = apiKey;
894
+ profiles[name] = profile;
895
+ commitModelProviders(pending, profiles, currentDefault(resolved, pending));
896
+ }
897
+ async function setDefaultProfile(resolved, pending) {
898
+ const profiles = currentProfiles(resolved, pending);
899
+ const currentName = currentDefault(resolved, pending);
900
+ const options = [
901
+ {
902
+ value: NATIVE_PROFILE_NAME,
903
+ label: NATIVE_PROFILE_NAME,
904
+ hint: currentName === NATIVE_PROFILE_NAME ? '(current)' : undefined,
905
+ },
906
+ ...Object.keys(profiles).map((name) => ({
907
+ value: name,
908
+ label: name,
909
+ hint: name === currentName ? '(current)' : undefined,
910
+ })),
911
+ ];
912
+ const selected = await p.select({
913
+ message: 'Default profile (applies when no per-session choice is made):',
914
+ options,
915
+ });
916
+ if (isCancelled(selected))
917
+ return;
918
+ const name = selected;
919
+ if (name !== currentName) {
920
+ commitModelProviders(pending, profiles, name);
921
+ }
922
+ }
923
+ async function editProfile(resolved, pending, name) {
924
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- interactive loop exited via return
925
+ while (true) {
926
+ const profiles = currentProfiles(resolved, pending);
927
+ if (!(name in profiles))
928
+ return;
929
+ const profile = profiles[name];
930
+ if (profile.type !== 'openrouter')
931
+ return;
932
+ const field = await p.select({
933
+ message: `Profile: ${name}`,
934
+ options: [
935
+ { value: 'apiKey', label: 'API key', hint: maskApiKey(profile.apiKey) },
936
+ { value: 'modelMap', label: 'Model map (glob -> slug)', hint: formatModelMap(profile.modelMap) },
937
+ { value: 'perAgent', label: 'Per-agent model overrides', hint: perAgentSummary(profile) },
938
+ {
939
+ value: 'providerPreference',
940
+ label: 'Provider preference (cache pinning)',
941
+ hint: providerPreferenceSummary(profile.providerPreference),
942
+ },
943
+ {
944
+ value: 'sessionAffinity',
945
+ label: 'Session affinity (GLM cache)',
946
+ hint: (profile.sessionAffinity ?? true) ? 'on' : 'off',
947
+ },
948
+ { value: 'delete', label: 'Delete profile' },
949
+ { value: 'back', label: 'Back' },
950
+ ],
951
+ });
952
+ if (isCancelled(field) || field === 'back')
953
+ return;
954
+ if (field === 'delete') {
955
+ const confirmed = await p.confirm({ message: `Delete profile "${name}"?`, initialValue: false });
956
+ if (isCancelled(confirmed) || !confirmed)
957
+ continue;
958
+ const remaining = omitKey(profiles, name);
959
+ // F10: never persist a `default` that names the just-deleted profile.
960
+ const nextDefault = repointDefaultAfterDelete(currentDefault(resolved, pending), Object.keys(remaining));
961
+ commitModelProviders(pending, remaining, nextDefault);
962
+ return;
963
+ }
964
+ const updated = await editProfileField(field, profile);
965
+ if (updated) {
966
+ profiles[name] = updated;
967
+ commitModelProviders(pending, profiles, currentDefault(resolved, pending));
968
+ }
969
+ }
970
+ }
971
+ /** Edits one field of an openrouter profile, returning the updated profile or undefined (no change). */
972
+ async function editProfileField(field, profile) {
973
+ if (field === 'apiKey') {
974
+ const apiKey = await p.text({
975
+ message: 'OpenRouter API key (sk-or-v1-...):',
976
+ placeholder: profile.apiKey ? '(keep current)' : 'leave blank to use OPENROUTER_API_KEY env',
977
+ validate: () => undefined,
978
+ });
979
+ if (isCancelled(apiKey))
980
+ return undefined;
981
+ const next = { ...profile };
982
+ if (apiKey)
983
+ next.apiKey = apiKey;
984
+ else
985
+ delete next.apiKey;
986
+ return next;
987
+ }
988
+ if (field === 'modelMap')
989
+ return editModelMap(profile);
990
+ if (field === 'perAgent')
991
+ return editPerAgent(profile);
992
+ if (field === 'providerPreference')
993
+ return editProviderPreference(profile);
994
+ if (field === 'sessionAffinity') {
995
+ const enabled = await p.confirm({
996
+ message: 'Inject a stable session_id for GLM cache affinity?',
997
+ initialValue: profile.sessionAffinity ?? true,
998
+ });
999
+ if (isCancelled(enabled))
1000
+ return undefined;
1001
+ return { ...profile, sessionAffinity: enabled };
1002
+ }
1003
+ return undefined;
1004
+ }
1005
+ /**
1006
+ * Builds the option list for a model-slug autocomplete from the resolved catalog.
1007
+ *
1008
+ * - Every catalog slug becomes a `{ value, label }` option.
1009
+ * - `allowNone` prepends a `(none)` sentinel (value `''`) FIRST — the only way a
1010
+ * selection-only prompt can clear a per-agent override.
1011
+ * - A non-empty `current` absent from the catalog is appended as a grandfather
1012
+ * option, so editing a legacy/delisted slug never traps the user.
1013
+ *
1014
+ * There is intentionally NO refresh sentinel: `ironcurtain config` is short-lived
1015
+ * and fetches the catalog once, so re-running it IS the refresh path.
1016
+ */
1017
+ export function buildSlugOptions(catalog, current, opts) {
1018
+ const options = catalog.models.map((slug) => ({ value: slug, label: slug }));
1019
+ if (opts.allowNone) {
1020
+ options.unshift({ value: '', label: '(none — use model map)' });
1021
+ }
1022
+ if (current !== '' && !catalog.models.includes(current)) {
1023
+ options.push({ value: current, label: `${current} (current, unverified)` });
1024
+ }
1025
+ return options;
1026
+ }
1027
+ /** Autocomplete (hard-block) for an authoritative catalog; free text for the bundled floor. */
1028
+ export function slugPromptMode(source) {
1029
+ return catalogEnforces(source) ? 'autocomplete' : 'freetext';
1030
+ }
1031
+ /**
1032
+ * Prompts for one OpenRouter model slug. Fetches the catalog lazily (module-cached)
1033
+ * so apiKey / provider-preference-only edits never pay the network cost.
1034
+ *
1035
+ * Authoritative catalog (`live`/`cache`): a selection-only `autocomplete` whose
1036
+ * options ARE the allowlist — a structural hard-block — with `validate` as
1037
+ * belt-and-suspenders. For an UNSET field the `(none)` sentinel is first, so a
1038
+ * no-op Enter keeps it unset — clack auto-preselects the first option and collapses
1039
+ * a falsy `initialValue` to undefined; a non-empty `current` is passed as
1040
+ * `initialValue` so editing an existing slug starts on it.
1041
+ *
1042
+ * Bundled floor (offline): the list is known-incomplete, so free-text entry with a
1043
+ * one-line warning — the value is accepted as-is.
1044
+ *
1045
+ * Returns the chosen slug (`''` clears when `allowNone`), or `undefined` on cancel.
1046
+ */
1047
+ async function promptSlug(message, opts) {
1048
+ const { current, allowNone } = opts;
1049
+ const catalog = await listOpenrouterModels();
1050
+ if (slugPromptMode(catalog.source) === 'autocomplete') {
1051
+ // Allowlist = catalog slugs, plus `current` (grandfather a legacy/delisted slug)
1052
+ // and '' ONLY when the field is clearable (per-agent). A required field
1053
+ // (map-row model, allowNone:false) never admits an empty submit.
1054
+ const allowlist = new Set([...catalog.models, ...(current ? [current] : []), ...(allowNone ? [''] : [])]);
1055
+ const picked = await p.autocomplete({
1056
+ message,
1057
+ options: buildSlugOptions(catalog, current, { allowNone }),
1058
+ initialValue: current || '',
1059
+ initialUserInput: current || undefined,
1060
+ maxItems: 8,
1061
+ validate: (v) => (typeof v === 'string' && !allowlist.has(v) ? `Unknown model: ${v}` : undefined),
1062
+ });
1063
+ if (isCancelled(picked))
1064
+ return undefined;
1065
+ return picked;
1066
+ }
1067
+ p.note("Model catalog unavailable — slug can't be verified; entered value accepted as-is.");
1068
+ const typed = await p.text({
1069
+ message,
1070
+ placeholder: current || DEFAULT_GLM_SLUG,
1071
+ validate: allowNone ? () => undefined : (val) => (!val || !val.trim() ? 'Target slug is required' : undefined),
1072
+ });
1073
+ if (isCancelled(typed))
1074
+ return undefined;
1075
+ // Trim: a free-typed slug is persisted as-is, and " " would pass the schema's
1076
+ // min(1) yet break routing (and defeat "blank to clear" for per-agent). The web
1077
+ // path already trims (editableToDto / validateSlugs); keep the CLI consistent.
1078
+ return typed.trim();
1079
+ }
1080
+ async function editModelMap(profile) {
1081
+ const rows = [...(profile.modelMap ?? [])];
1082
+ p.note('Ordered glob -> slug rules; first match wins (matched against the requested model id).\n' +
1083
+ 'An EMPTY map means "per-agent-only mode": no glob mapping, rely on per-agent overrides.\n' +
1084
+ 'Leaving the map unset uses the built-in defaults (*sonnet*/*opus*/*haiku* -> ' +
1085
+ `${DEFAULT_GLM_SLUG}).`, 'Model map');
1086
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- interactive loop exited via return
1087
+ while (true) {
1088
+ const options = rows.map((row, i) => ({
1089
+ value: `row:${i}`,
1090
+ label: `${row.match} -> ${row.model}`,
1091
+ hint: 'select to remove',
1092
+ }));
1093
+ options.push({ value: 'add', label: 'Add rule...' });
1094
+ options.push({ value: 'done', label: 'Done', hint: rows.length === 0 ? 'empty = per-agent-only mode' : undefined });
1095
+ const action = await p.select({ message: `Model map (${rows.length} rule(s))`, options });
1096
+ if (isCancelled(action) || action === 'done')
1097
+ break;
1098
+ if (action === 'add') {
1099
+ const match = await p.text({
1100
+ message: 'Glob to match (e.g. *sonnet*):',
1101
+ validate: (val) => (!val ? 'Match glob is required' : undefined),
1102
+ });
1103
+ if (isCancelled(match))
1104
+ continue;
1105
+ const model = await promptSlug('Target OpenRouter slug (e.g. z-ai/glm-5.2):', {
1106
+ current: '',
1107
+ allowNone: false,
1108
+ });
1109
+ if (model === undefined)
1110
+ continue;
1111
+ rows.push({ match: match, model });
1112
+ }
1113
+ else if (typeof action === 'string' && action.startsWith('row:')) {
1114
+ const index = Number(action.slice('row:'.length));
1115
+ rows.splice(index, 1);
1116
+ }
1117
+ }
1118
+ return { ...profile, modelMap: rows };
1119
+ }
1120
+ async function editPerAgent(profile) {
1121
+ let perAgent = { ...(profile.perAgent ?? {}) };
1122
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- interactive loop exited via return
1123
+ while (true) {
1124
+ const options = DOCKER_AGENTS.map((agent) => ({
1125
+ value: agent,
1126
+ label: DOCKER_AGENT_LABELS[agent],
1127
+ hint: perAgent[agent] ?? 'none (use model map)',
1128
+ }));
1129
+ const selected = await p.select({
1130
+ message: 'Per-agent model overrides (wins over the model map)',
1131
+ options: [...options, { value: 'back', label: 'Back', hint: '' }],
1132
+ });
1133
+ if (isCancelled(selected) || selected === 'back')
1134
+ break;
1135
+ const agent = selected;
1136
+ const slug = await promptSlug(`${DOCKER_AGENT_LABELS[agent]} model slug (blank to clear):`, {
1137
+ current: perAgent[agent] ?? '',
1138
+ allowNone: true,
1139
+ });
1140
+ if (slug === undefined)
1141
+ continue;
1142
+ if (slug)
1143
+ perAgent[agent] = slug;
1144
+ else
1145
+ perAgent = omitKey(perAgent, agent);
1146
+ }
1147
+ const next = { ...profile };
1148
+ if (Object.keys(perAgent).length > 0)
1149
+ next.perAgent = perAgent;
1150
+ else
1151
+ delete next.perAgent;
1152
+ return next;
1153
+ }
1154
+ async function editProviderPreference(profile) {
1155
+ const pref = { ...(profile.providerPreference ?? {}) };
1156
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- interactive loop exited via return
1157
+ while (true) {
1158
+ const field = await p.select({
1159
+ message: 'Provider preference (leave unset for the default soft z-ai pin on z-ai/* slugs)',
1160
+ options: [
1161
+ { value: 'order', label: 'order (soft pin)', hint: pref.order?.join(', ') ?? 'none' },
1162
+ { value: 'only', label: 'only (strict pin)', hint: pref.only?.join(', ') ?? 'none' },
1163
+ {
1164
+ value: 'allowFallbacks',
1165
+ label: 'allowFallbacks',
1166
+ hint: pref.allowFallbacks === undefined ? 'default (true)' : pref.allowFallbacks ? 'true' : 'false',
1167
+ },
1168
+ { value: 'clear', label: 'Clear (revert to default pin)' },
1169
+ { value: 'back', label: 'Back' },
1170
+ ],
1171
+ });
1172
+ if (isCancelled(field) || field === 'back')
1173
+ break;
1174
+ if (field === 'clear') {
1175
+ return { ...profile, providerPreference: undefined };
1176
+ }
1177
+ if (field === 'order' || field === 'only') {
1178
+ const input = await p.text({
1179
+ message: `${field} — comma-separated provider slugs (e.g. z-ai), blank to clear:`,
1180
+ placeholder: pref[field]?.join(', ') ?? 'z-ai',
1181
+ validate: () => undefined,
1182
+ });
1183
+ if (isCancelled(input))
1184
+ continue;
1185
+ const list = input
1186
+ .split(',')
1187
+ .map((s) => s.trim())
1188
+ .filter((s) => s.length > 0);
1189
+ const next = list.length > 0 ? list : undefined;
1190
+ if (field === 'order')
1191
+ pref.order = next;
1192
+ else
1193
+ pref.only = next;
1194
+ }
1195
+ else if (field === 'allowFallbacks') {
1196
+ const enabled = await p.confirm({
1197
+ message: 'Allow OpenRouter to fall back to other providers?',
1198
+ initialValue: pref.allowFallbacks ?? true,
1199
+ });
1200
+ if (isCancelled(enabled))
1201
+ continue;
1202
+ pref.allowFallbacks = enabled;
1203
+ }
1204
+ }
1205
+ const cleaned = {};
1206
+ if (pref.order !== undefined)
1207
+ cleaned.order = pref.order;
1208
+ if (pref.only !== undefined)
1209
+ cleaned.only = pref.only;
1210
+ if (pref.allowFallbacks !== undefined)
1211
+ cleaned.allowFallbacks = pref.allowFallbacks;
1212
+ const hasAny = Object.keys(cleaned).length > 0;
1213
+ return { ...profile, providerPreference: hasAny ? cleaned : undefined };
1214
+ }
1215
+ function perAgentSummary(profile) {
1216
+ const perAgent = profile.perAgent;
1217
+ if (!perAgent)
1218
+ return 'none';
1219
+ const entries = Object.entries(perAgent).filter(([, v]) => v);
1220
+ if (entries.length === 0)
1221
+ return 'none';
1222
+ return entries.map(([agent, slug]) => `${agent}=${slug}`).join(', ');
1223
+ }
1224
+ function providerPreferenceSummary(pref) {
1225
+ if (!pref)
1226
+ return 'default (soft z-ai pin)';
1227
+ const parts = [];
1228
+ if (pref.only)
1229
+ parts.push(`only: ${pref.only.join(', ')}`);
1230
+ if (pref.order)
1231
+ parts.push(`order: ${pref.order.join(', ')}`);
1232
+ if (pref.allowFallbacks !== undefined)
1233
+ parts.push(`fallbacks: ${pref.allowFallbacks ? 'on' : 'off'}`);
1234
+ return parts.length > 0 ? parts.join(', ') : 'default';
1235
+ }
579
1236
  // ─── Server Credentials ──────────────────────────────────────
580
1237
  /** Loads server names from mcp-servers.json for the credential editor. */
581
1238
  function loadServerNames() {
@@ -688,18 +1345,25 @@ async function handleServerCredentials(resolved, pending) {
688
1345
  // ─── Session Mode ─────────────────────────────────────────────
689
1346
  /** Human-readable labels for session modes. */
690
1347
  const SESSION_MODE_LABELS = {
691
- docker: 'Docker (recommended)',
1348
+ container: 'Container (recommended)',
692
1349
  builtin: 'Builtin (V8 sandbox)',
693
1350
  };
694
1351
  /** Short labels used in hints (no parenthetical). */
695
1352
  const SESSION_MODE_SHORT_LABELS = {
696
- docker: 'Docker',
1353
+ container: 'Container',
697
1354
  builtin: 'Builtin',
698
1355
  };
1356
+ /** Human-readable labels for container runtime backends. */
1357
+ const CONTAINER_RUNTIME_LABELS = {
1358
+ auto: 'Auto (prefer Apple container when available)',
1359
+ docker: 'Docker',
1360
+ 'apple-container': 'Apple container (macOS 26+, Apple silicon)',
1361
+ };
699
1362
  async function handleSessionMode(resolved, pending) {
700
1363
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- interactive loop exited via return
701
1364
  while (true) {
702
1365
  const currentMode = pending.preferredMode ?? resolved.preferredMode;
1366
+ const currentRuntime = pending.containerRuntime ?? resolved.containerRuntime;
703
1367
  const field = await p.select({
704
1368
  message: 'Session Mode',
705
1369
  options: [
@@ -708,11 +1372,35 @@ async function handleSessionMode(resolved, pending) {
708
1372
  label: 'Preferred mode',
709
1373
  hint: SESSION_MODE_LABELS[currentMode],
710
1374
  },
1375
+ {
1376
+ value: 'containerRuntime',
1377
+ label: 'Container runtime',
1378
+ hint: CONTAINER_RUNTIME_LABELS[currentRuntime],
1379
+ },
711
1380
  { value: 'back', label: 'Back' },
712
1381
  ],
713
1382
  });
714
1383
  if (isCancelled(field) || field === 'back')
715
1384
  return;
1385
+ if (field === 'containerRuntime') {
1386
+ const runtimeOptions = CONTAINER_RUNTIMES.map((runtime) => ({
1387
+ value: runtime,
1388
+ label: CONTAINER_RUNTIME_LABELS[runtime],
1389
+ hint: runtime === currentRuntime ? '(current)' : undefined,
1390
+ }));
1391
+ const selected = await p.select({
1392
+ message: 'Select container runtime backend:',
1393
+ options: runtimeOptions,
1394
+ initialValue: currentRuntime,
1395
+ });
1396
+ if (isCancelled(selected))
1397
+ continue;
1398
+ const runtime = selected;
1399
+ if (runtime !== currentRuntime) {
1400
+ pending.containerRuntime = runtime;
1401
+ }
1402
+ continue;
1403
+ }
716
1404
  if (field === 'preferredMode') {
717
1405
  const modeOptions = SESSION_MODES.map((mode) => ({
718
1406
  value: mode,
@@ -744,6 +1432,7 @@ const GOOSE_PROVIDER_LABELS = {
744
1432
  const DOCKER_AGENT_LABELS = {
745
1433
  'claude-code': 'Claude Code',
746
1434
  goose: 'Goose',
1435
+ codex: 'Codex CLI',
747
1436
  };
748
1437
  async function handleDockerAgent(resolved, pending) {
749
1438
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- interactive loop exited via return
@@ -948,6 +1637,14 @@ function webSearchHint(resolved, pending) {
948
1637
  const provider = pending.webSearch?.provider ?? resolved.webSearch.provider;
949
1638
  return provider ? WEB_SEARCH_PROVIDER_LABELS[provider] : 'not configured';
950
1639
  }
1640
+ function modelProvidersHint(resolved, pending) {
1641
+ const profiles = currentProfiles(resolved, pending);
1642
+ const count = Object.keys(profiles).length;
1643
+ const defaultName = currentDefault(resolved, pending);
1644
+ if (count === 0)
1645
+ return `default: ${defaultName}`;
1646
+ return `${count} profile${count > 1 ? 's' : ''}, default: ${defaultName}`;
1647
+ }
951
1648
  function serverCredentialsHint(resolved, pending) {
952
1649
  const creds = { ...resolved.serverCredentials, ...pending.serverCredentials };
953
1650
  const configured = Object.entries(creds).filter(([, v]) => Object.keys(v).length > 0);
@@ -962,6 +1659,13 @@ function memoryHint(resolved, pending) {
962
1659
  return 'off (kill switch)';
963
1660
  return `on, auto-save: ${autoSave ? 'on' : 'off'}`;
964
1661
  }
1662
+ function snapshotHint(resolved, pending) {
1663
+ const enabled = pending.snapshot?.enabled ?? resolved.snapshot.enabled;
1664
+ const maxAge = pending.snapshot?.maxAgeDays ?? resolved.snapshot.maxAgeDays;
1665
+ if (!enabled)
1666
+ return 'off (kill switch)';
1667
+ return `on, max age: ${maxAge === null ? 'unbounded' : `${maxAge}d`}`;
1668
+ }
965
1669
  function dockerAgentHint(resolved, pending) {
966
1670
  const agent = DOCKER_AGENT_LABELS[pending.preferredDockerAgent ?? resolved.preferredDockerAgent];
967
1671
  return `${agent}, ${dockerResourcesSummary(resolved, pending)}`;
@@ -1003,8 +1707,10 @@ export async function runConfigCommand() {
1003
1707
  { value: 'resources', label: `Resource Limits (${resourceHint(resolved, pending)})` },
1004
1708
  { value: 'compact', label: `Auto-Compact (${autoCompactHint(resolved, pending)})` },
1005
1709
  { value: 'websearch', label: `Web Search (${webSearchHint(resolved, pending)})` },
1710
+ { value: 'modelProviders', label: `Model Providers (${modelProvidersHint(resolved, pending)})` },
1006
1711
  { value: 'credentials', label: `Server Credentials (${serverCredentialsHint(resolved, pending)})` },
1007
1712
  { value: 'memory', label: `Memory (${memoryHint(resolved, pending)})` },
1713
+ { value: 'snapshots', label: `Container Snapshots (${snapshotHint(resolved, pending)})` },
1008
1714
  { value: 'sessionMode', label: `Session Mode (${sessionModeHint(resolved, pending)})` },
1009
1715
  { value: 'dockerAgent', label: `Docker Agent (${dockerAgentHint(resolved, pending)})` },
1010
1716
  { value: 'save', label: 'Save & Exit', hint: changeCount(resolved, pending) },
@@ -1031,12 +1737,18 @@ export async function runConfigCommand() {
1031
1737
  case 'websearch':
1032
1738
  await handleWebSearch(resolved, pending);
1033
1739
  break;
1740
+ case 'modelProviders':
1741
+ await handleModelProviders(resolved, pending);
1742
+ break;
1034
1743
  case 'credentials':
1035
1744
  await handleServerCredentials(resolved, pending);
1036
1745
  break;
1037
1746
  case 'memory':
1038
1747
  await handleMemory(resolved, pending);
1039
1748
  break;
1749
+ case 'snapshots':
1750
+ await handleSnapshot(resolved, pending);
1751
+ break;
1040
1752
  case 'sessionMode':
1041
1753
  await handleSessionMode(resolved, pending);
1042
1754
  break;