@zergai/ztc 0.4.0-beta.1 → 0.5.0-beta.1

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 (718) hide show
  1. package/README.md +398 -45
  2. package/RUST_THIRD_PARTY_LICENSES.txt +1080 -822
  3. package/THIRD_PARTY_NOTICES.md +273 -12
  4. package/bin/zmux-darwin-arm64 +0 -0
  5. package/bin/ztc-audio-darwin-arm64 +0 -0
  6. package/bin/ztc-capture-darwin-arm64 +0 -0
  7. package/bin/ztc-computer-darwin-arm64 +0 -0
  8. package/bin/ztc-llm-darwin-arm64 +0 -0
  9. package/bin/ztc-tts-darwin-arm64 +0 -0
  10. package/bin/ztc-tui-darwin-arm64 +0 -0
  11. package/bin/ztc-zlicky-overlay-darwin-arm64 +0 -0
  12. package/bin/zterm-darwin-arm64 +0 -0
  13. package/dist/App.js +1206 -209
  14. package/dist/agent/agent.d.ts +74 -8
  15. package/dist/agent/agent.js +1073 -184
  16. package/dist/agent/backends/account_rotation.d.ts +4 -0
  17. package/dist/agent/backends/account_rotation.js +8 -2
  18. package/dist/agent/backends/anthropic.d.ts +13 -0
  19. package/dist/agent/backends/anthropic.js +169 -28
  20. package/dist/agent/backends/claude_cli.js +18 -8
  21. package/dist/agent/backends/codex_app_server.d.ts +1 -0
  22. package/dist/agent/backends/codex_app_server.js +55 -3
  23. package/dist/agent/backends/codex_auth.d.ts +1 -0
  24. package/dist/agent/backends/codex_auth.js +67 -13
  25. package/dist/agent/backends/codex_buffered_recovery.d.ts +27 -0
  26. package/dist/agent/backends/codex_buffered_recovery.js +293 -0
  27. package/dist/agent/backends/gemini.js +3 -1
  28. package/dist/agent/backends/http_error.d.ts +26 -0
  29. package/dist/agent/backends/http_error.js +221 -0
  30. package/dist/agent/backends/index.d.ts +9 -2
  31. package/dist/agent/backends/index.js +22 -2
  32. package/dist/agent/backends/openai.d.ts +0 -6
  33. package/dist/agent/backends/openai.js +5 -2
  34. package/dist/agent/backends/openai_compatible.d.ts +5 -4
  35. package/dist/agent/backends/openai_compatible.js +26 -19
  36. package/dist/agent/backends/openai_responses.d.ts +8 -3
  37. package/dist/agent/backends/openai_responses.js +77 -33
  38. package/dist/agent/backends/provider_ids.d.ts +1 -0
  39. package/dist/agent/backends/provider_ids.js +4 -0
  40. package/dist/agent/backends/request_metrics.js +12 -2
  41. package/dist/agent/backends/types.d.ts +109 -6
  42. package/dist/agent/backends/types.js +5 -1
  43. package/dist/agent/backends/zrouter_stream.d.ts +6 -1
  44. package/dist/agent/backends/zrouter_stream.js +160 -42
  45. package/dist/agent/background_registry.d.ts +2 -0
  46. package/dist/agent/background_registry.js +1 -0
  47. package/dist/agent/claim_observation.d.ts +6 -0
  48. package/dist/agent/claim_observation.js +54 -0
  49. package/dist/agent/commands/accounts.js +46 -177
  50. package/dist/agent/commands/attention.d.ts +2 -0
  51. package/dist/agent/commands/attention.js +67 -0
  52. package/dist/agent/commands/auth.js +18 -2
  53. package/dist/agent/commands/board.d.ts +2 -0
  54. package/dist/agent/commands/board.js +169 -0
  55. package/dist/agent/commands/clipboard.js +50 -1
  56. package/dist/agent/commands/compact.js +21 -13
  57. package/dist/agent/commands/config.js +4 -1
  58. package/dist/agent/commands/context.js +6 -1
  59. package/dist/agent/commands/desktop.d.ts +2 -0
  60. package/dist/agent/commands/desktop.js +29 -0
  61. package/dist/agent/commands/effort.js +14 -6
  62. package/dist/agent/commands/environment.d.ts +3 -0
  63. package/dist/agent/commands/environment.js +109 -0
  64. package/dist/agent/commands/evidence.d.ts +2 -0
  65. package/dist/agent/commands/evidence.js +9 -0
  66. package/dist/agent/commands/fork.js +4 -3
  67. package/dist/agent/commands/goal.d.ts +33 -1
  68. package/dist/agent/commands/goal.js +174 -14
  69. package/dist/agent/commands/goal_exit.d.ts +36 -0
  70. package/dist/agent/commands/goal_exit.js +120 -0
  71. package/dist/agent/commands/help.js +3 -1
  72. package/dist/agent/commands/history.js +3 -1
  73. package/dist/agent/commands/inbox.d.ts +2 -0
  74. package/dist/agent/commands/inbox.js +148 -0
  75. package/dist/agent/commands/index.js +16 -1
  76. package/dist/agent/commands/input_mode.js +8 -6
  77. package/dist/agent/commands/job.d.ts +2 -0
  78. package/dist/agent/commands/job.js +93 -0
  79. package/dist/agent/commands/keybindings.js +4 -1
  80. package/dist/agent/commands/label.d.ts +2 -0
  81. package/dist/agent/commands/label.js +77 -0
  82. package/dist/agent/commands/layout.d.ts +2 -0
  83. package/dist/agent/commands/layout.js +20 -0
  84. package/dist/agent/commands/mirror.js +16 -1
  85. package/dist/agent/commands/model.js +12 -2
  86. package/dist/agent/commands/permissions.d.ts +3 -1
  87. package/dist/agent/commands/permissions.js +79 -89
  88. package/dist/agent/commands/plan.js +5 -15
  89. package/dist/agent/commands/profile.js +21 -2
  90. package/dist/agent/commands/provider_selection.js +20 -1
  91. package/dist/agent/commands/rename.js +1 -1
  92. package/dist/agent/commands/resources.d.ts +2 -0
  93. package/dist/agent/commands/resources.js +67 -0
  94. package/dist/agent/commands/rewind.d.ts +3 -0
  95. package/dist/agent/commands/rewind.js +16 -0
  96. package/dist/agent/commands/runbooks.d.ts +2 -0
  97. package/dist/agent/commands/runbooks.js +73 -0
  98. package/dist/agent/commands/runbooks_lazy.d.ts +2 -0
  99. package/dist/agent/commands/runbooks_lazy.js +8 -0
  100. package/dist/agent/commands/sessions.d.ts +1 -1
  101. package/dist/agent/commands/sessions.js +33 -5
  102. package/dist/agent/commands/settings.js +185 -39
  103. package/dist/agent/commands/setup.d.ts +2 -0
  104. package/dist/agent/commands/setup.js +78 -0
  105. package/dist/agent/commands/share.d.ts +2 -0
  106. package/dist/agent/commands/share.js +64 -0
  107. package/dist/agent/commands/status.js +14 -2
  108. package/dist/agent/commands/types.d.ts +99 -6
  109. package/dist/agent/commands/update.js +8 -0
  110. package/dist/agent/commands/usage.d.ts +2 -0
  111. package/dist/agent/commands/usage.js +8 -0
  112. package/dist/agent/factory.d.ts +11 -0
  113. package/dist/agent/factory.js +113 -88
  114. package/dist/agent/loop_policy.d.ts +6 -3
  115. package/dist/agent/loop_policy.js +12 -3
  116. package/dist/agent/measurement_guidance.d.ts +2 -0
  117. package/dist/agent/measurement_guidance.js +5 -0
  118. package/dist/agent/request_encoder.d.ts +8 -1
  119. package/dist/agent/request_encoder.js +95 -23
  120. package/dist/agent/runtime/benchmark_comparison.d.ts +30 -0
  121. package/dist/agent/runtime/benchmark_comparison.js +94 -0
  122. package/dist/agent/runtime/benchmark_contract.d.ts +48 -0
  123. package/dist/agent/runtime/benchmark_contract.js +90 -0
  124. package/dist/agent/runtime/checked_commands.d.ts +32 -0
  125. package/dist/agent/runtime/checked_commands.js +119 -0
  126. package/dist/agent/runtime/command_execution_policy.d.ts +26 -0
  127. package/dist/agent/runtime/command_execution_policy.js +91 -0
  128. package/dist/agent/runtime/command_jobs.d.ts +36 -18
  129. package/dist/agent/runtime/command_jobs.js +151 -13
  130. package/dist/agent/runtime/command_panes.d.ts +59 -0
  131. package/dist/agent/runtime/command_panes.js +398 -0
  132. package/dist/agent/runtime/command_wait.d.ts +19 -0
  133. package/dist/agent/runtime/command_wait.js +167 -0
  134. package/dist/agent/runtime/cpu_benchmark.d.ts +30 -0
  135. package/dist/agent/runtime/cpu_benchmark.js +126 -0
  136. package/dist/agent/runtime/delegated_evidence.d.ts +26 -0
  137. package/dist/agent/runtime/delegated_evidence.js +80 -0
  138. package/dist/agent/runtime/evidence.d.ts +127 -0
  139. package/dist/agent/runtime/evidence.js +468 -0
  140. package/dist/agent/runtime/execution_context.d.ts +7 -0
  141. package/dist/agent/runtime/execution_context.js +39 -3
  142. package/dist/agent/runtime/json_verifier_recipe.d.ts +8 -0
  143. package/dist/agent/runtime/json_verifier_recipe.js +66 -0
  144. package/dist/agent/runtime/measurements.d.ts +110 -0
  145. package/dist/agent/runtime/measurements.js +317 -0
  146. package/dist/agent/runtime/policy.d.ts +4 -0
  147. package/dist/agent/runtime/policy.js +31 -20
  148. package/dist/agent/runtime/provider_native_evidence.d.ts +20 -0
  149. package/dist/agent/runtime/provider_native_evidence.js +86 -0
  150. package/dist/agent/runtime/recovery.d.ts +2 -0
  151. package/dist/agent/runtime/recovery.js +45 -2
  152. package/dist/agent/runtime/remote_broker_transport.d.ts +10 -0
  153. package/dist/agent/runtime/remote_broker_transport.js +84 -0
  154. package/dist/agent/runtime/remote_deletion_receipts.d.ts +3 -0
  155. package/dist/agent/runtime/remote_deletion_receipts.js +43 -0
  156. package/dist/agent/runtime/remote_journal_replay.d.ts +13 -0
  157. package/dist/agent/runtime/remote_journal_replay.js +233 -0
  158. package/dist/agent/runtime/remote_mutation_journal.d.ts +25 -0
  159. package/dist/agent/runtime/remote_mutation_journal.js +208 -0
  160. package/dist/agent/runtime/remote_resource_endpoint.d.ts +7 -0
  161. package/dist/agent/runtime/remote_resource_endpoint.js +6 -0
  162. package/dist/agent/runtime/remote_resource_errors.d.ts +5 -0
  163. package/dist/agent/runtime/remote_resource_errors.js +9 -0
  164. package/dist/agent/runtime/remote_resource_script.d.ts +2 -0
  165. package/dist/agent/runtime/remote_resource_script.js +1009 -0
  166. package/dist/agent/runtime/remote_resources.d.ts +29 -0
  167. package/dist/agent/runtime/remote_resources.js +160 -0
  168. package/dist/agent/runtime/source_verification.d.ts +12 -0
  169. package/dist/agent/runtime/source_verification.js +210 -0
  170. package/dist/agent/runtime/source_verification_contract.d.ts +75 -0
  171. package/dist/agent/runtime/source_verification_contract.js +203 -0
  172. package/dist/agent/runtime/tool_checkpoints.d.ts +11 -0
  173. package/dist/agent/runtime/tool_checkpoints.js +26 -0
  174. package/dist/agent/runtime/tool_effects.d.ts +5 -0
  175. package/dist/agent/runtime/tool_progress.d.ts +12 -0
  176. package/dist/agent/runtime/tool_progress.js +76 -0
  177. package/dist/agent/runtime/tool_runtime.d.ts +16 -1
  178. package/dist/agent/runtime/tool_runtime.js +204 -10
  179. package/dist/agent/runtime/tracing.d.ts +1 -1
  180. package/dist/agent/runtime/verifier_capture.d.ts +9 -0
  181. package/dist/agent/runtime/verifier_capture.js +100 -0
  182. package/dist/agent/runtime/verifier_contract.d.ts +24 -0
  183. package/dist/agent/runtime/verifier_contract.js +41 -0
  184. package/dist/agent/subagent.d.ts +20 -10
  185. package/dist/agent/subagent.js +266 -29
  186. package/dist/agent/tool_response_directory.d.ts +24 -0
  187. package/dist/agent/tool_response_directory.js +61 -0
  188. package/dist/agent/tool_response_source.d.ts +8 -0
  189. package/dist/agent/tool_response_source.js +49 -0
  190. package/dist/agent/tool_response_store.d.ts +75 -4
  191. package/dist/agent/tool_response_store.js +583 -64
  192. package/dist/agent/tools/ask_user.d.ts +3 -1
  193. package/dist/agent/tools/ask_user.js +30 -11
  194. package/dist/agent/tools/background.js +1 -0
  195. package/dist/agent/tools/benchmark_cpu.d.ts +2 -0
  196. package/dist/agent/tools/benchmark_cpu.js +102 -0
  197. package/dist/agent/tools/compare_benchmarks.d.ts +2 -0
  198. package/dist/agent/tools/compare_benchmarks.js +53 -0
  199. package/dist/agent/tools/deferred.js +1 -0
  200. package/dist/agent/tools/delegate.js +18 -2
  201. package/dist/agent/tools/helper_requirements.d.ts +7 -0
  202. package/dist/agent/tools/helper_requirements.js +19 -0
  203. package/dist/agent/tools/index.d.ts +1 -0
  204. package/dist/agent/tools/index.js +3 -0
  205. package/dist/agent/tools/measurement_report.d.ts +22 -0
  206. package/dist/agent/tools/measurement_report.js +190 -0
  207. package/dist/agent/tools/pane.d.ts +0 -7
  208. package/dist/agent/tools/pane.js +5 -4
  209. package/dist/agent/tools/project_context.d.ts +3 -0
  210. package/dist/agent/tools/project_context.js +45 -0
  211. package/dist/agent/tools/recall_tool_response.d.ts +3 -5
  212. package/dist/agent/tools/recall_tool_response.js +19 -29
  213. package/dist/agent/tools/registry.d.ts +1 -0
  214. package/dist/agent/tools/registry.js +23 -0
  215. package/dist/agent/tools/related_sessions.d.ts +2 -0
  216. package/dist/agent/tools/related_sessions.js +156 -0
  217. package/dist/agent/tools/routine.js +13 -3
  218. package/dist/agent/tools/session_evidence.d.ts +3 -0
  219. package/dist/agent/tools/session_evidence.js +72 -0
  220. package/dist/agent/tools/shell.d.ts +4 -3
  221. package/dist/agent/tools/shell.js +174 -47
  222. package/dist/agent/tools/tool_search.js +3 -2
  223. package/dist/agent/tools/types.d.ts +28 -0
  224. package/dist/agent/tools/verify_artifact.d.ts +2 -0
  225. package/dist/agent/tools/verify_artifact.js +60 -0
  226. package/dist/agent/tools/workflow.js +1 -0
  227. package/dist/app/activity.d.ts +6 -1
  228. package/dist/app/activity.js +81 -12
  229. package/dist/app/agent_runner.d.ts +6 -1
  230. package/dist/app/agent_runner.js +34 -19
  231. package/dist/app/command_context.js +106 -10
  232. package/dist/app/controller.d.ts +15 -4
  233. package/dist/app/controller.js +29 -3
  234. package/dist/app/escape_stop.d.ts +11 -0
  235. package/dist/app/escape_stop.js +15 -0
  236. package/dist/app/liveness.d.ts +6 -4
  237. package/dist/app/liveness.js +13 -3
  238. package/dist/app/model_catalog_cache.d.ts +5 -0
  239. package/dist/app/model_catalog_cache.js +138 -0
  240. package/dist/app/ordered_transcript.js +43 -4
  241. package/dist/app/prompt_turn.d.ts +14 -0
  242. package/dist/app/prompt_turn.js +53 -0
  243. package/dist/app/recap.d.ts +3 -0
  244. package/dist/app/recap.js +9 -3
  245. package/dist/app/restart.d.ts +3 -2
  246. package/dist/app/restart.js +22 -7
  247. package/dist/app/rewind.d.ts +3 -3
  248. package/dist/app/rewind.js +3 -3
  249. package/dist/app/selection_completion.d.ts +17 -2
  250. package/dist/app/selection_completion.js +84 -9
  251. package/dist/app/session_manager.d.ts +38 -6
  252. package/dist/app/session_manager.js +263 -60
  253. package/dist/app/session_messages.d.ts +3 -0
  254. package/dist/app/session_messages.js +60 -0
  255. package/dist/app/task_scope_router.d.ts +20 -0
  256. package/dist/app/task_scope_router.js +173 -0
  257. package/dist/app/transcript_window.d.ts +2 -0
  258. package/dist/app/transcript_window.js +4 -0
  259. package/dist/app/workspace_admission_details.d.ts +3 -0
  260. package/dist/app/workspace_admission_details.js +26 -0
  261. package/dist/app/workspace_layout_controller.d.ts +11 -0
  262. package/dist/app/workspace_layout_controller.js +88 -0
  263. package/dist/app/workspace_monitor.d.ts +1 -0
  264. package/dist/app/workspace_monitor.js +216 -0
  265. package/dist/app/workspace_presets.d.ts +29 -0
  266. package/dist/app/workspace_presets.js +192 -0
  267. package/dist/attention/queue.d.ts +37 -0
  268. package/dist/attention/queue.js +183 -0
  269. package/dist/bench/rf_shared.d.ts +2 -0
  270. package/dist/board/binding.d.ts +42 -0
  271. package/dist/board/binding.js +38 -0
  272. package/dist/board/client.d.ts +42 -0
  273. package/dist/board/client.js +60 -0
  274. package/dist/cli.js +1 -1
  275. package/dist/cli_args.js +19 -17
  276. package/dist/cli_main.js +110 -13
  277. package/dist/cli_ratatui.d.ts +183 -16
  278. package/dist/cli_ratatui.js +3989 -1255
  279. package/dist/components/ActivityLine.d.ts +3 -2
  280. package/dist/components/ActivityLine.js +2 -2
  281. package/dist/components/InputArea.d.ts +19 -1
  282. package/dist/components/InputArea.js +264 -190
  283. package/dist/components/MessageList.d.ts +2 -0
  284. package/dist/components/MessageList.js +3 -1
  285. package/dist/components/SingleMessage.d.ts +2 -0
  286. package/dist/components/SingleMessage.js +4 -2
  287. package/dist/components/StatusBar.d.ts +4 -2
  288. package/dist/components/StatusBar.js +3 -1
  289. package/dist/conductor_branch.js +3 -0
  290. package/dist/config/api_retry.d.ts +6 -1
  291. package/dist/config/api_retry.js +19 -1
  292. package/dist/config/defaults.d.ts +2 -1
  293. package/dist/config/defaults.js +9 -2
  294. package/dist/config/message_display.d.ts +1 -0
  295. package/dist/config/message_display.js +17 -2
  296. package/dist/config/permissions.d.ts +21 -0
  297. package/dist/config/permissions.js +89 -0
  298. package/dist/config/resolved.d.ts +2 -2
  299. package/dist/config/types.d.ts +32 -5
  300. package/dist/config.d.ts +38 -4
  301. package/dist/config.js +290 -42
  302. package/dist/context/project_intelligence.d.ts +44 -0
  303. package/dist/context/project_intelligence.js +246 -0
  304. package/dist/desktop/cli_launch.d.ts +8 -0
  305. package/dist/desktop/cli_launch.js +6 -0
  306. package/dist/desktop/cli_shim.d.ts +8 -0
  307. package/dist/desktop/cli_shim.js +120 -0
  308. package/dist/desktop/runtime.d.ts +16 -0
  309. package/dist/desktop/runtime.js +161 -0
  310. package/dist/desktop/worker_launch.d.ts +7 -0
  311. package/dist/desktop/worker_launch.js +7 -0
  312. package/dist/diagnostics/cargo_mutants_selection.d.ts +27 -0
  313. package/dist/diagnostics/cargo_mutants_selection.js +48 -0
  314. package/dist/diagnostics/native_terminal_benchmark.d.ts +58 -0
  315. package/dist/diagnostics/native_terminal_benchmark.js +1247 -0
  316. package/dist/diagnostics/session_evidence_package.d.ts +40 -0
  317. package/dist/diagnostics/session_evidence_package.js +234 -0
  318. package/dist/diagnostics/session_export.d.ts +93 -0
  319. package/dist/diagnostics/session_export.js +634 -0
  320. package/dist/emulation/tool_adapters.js +48 -18
  321. package/dist/emulation/ztc_addendum.d.ts +1 -0
  322. package/dist/emulation/ztc_addendum.js +13 -12
  323. package/dist/environment/service_runner.d.ts +1 -0
  324. package/dist/environment/service_runner.js +68 -0
  325. package/dist/environment/store.d.ts +120 -0
  326. package/dist/environment/store.js +387 -0
  327. package/dist/events.d.ts +40 -1
  328. package/dist/events.js +10 -17
  329. package/dist/goal_contract.d.ts +25 -0
  330. package/dist/goal_contract.js +44 -0
  331. package/dist/headless.d.ts +31 -0
  332. package/dist/headless.js +578 -70
  333. package/dist/headless_cli.js +8 -0
  334. package/dist/headless_cli_args.d.ts +1 -0
  335. package/dist/headless_cli_args.js +1 -0
  336. package/dist/intake/cli.d.ts +1 -0
  337. package/dist/intake/cli.js +33 -0
  338. package/dist/intake/format.d.ts +2 -0
  339. package/dist/intake/format.js +14 -0
  340. package/dist/intake/notifications.d.ts +16 -0
  341. package/dist/intake/notifications.js +132 -0
  342. package/dist/intake/store.d.ts +56 -0
  343. package/dist/intake/store.js +238 -0
  344. package/dist/library.d.ts +8 -0
  345. package/dist/library.js +19 -10
  346. package/dist/mcp/client.d.ts +1 -0
  347. package/dist/mcp/client.js +20 -0
  348. package/dist/modes/builtin/plan.yaml +2 -2
  349. package/dist/modes/handlers.js +1 -1
  350. package/dist/onboarding/first_run.d.ts +3 -0
  351. package/dist/onboarding/first_run.js +33 -25
  352. package/dist/onboarding/setup.d.ts +32 -0
  353. package/dist/onboarding/setup.js +133 -0
  354. package/dist/onboarding/setup_agent_health.d.ts +17 -0
  355. package/dist/onboarding/setup_agent_health.js +79 -0
  356. package/dist/onboarding/setup_environment.d.ts +66 -0
  357. package/dist/onboarding/setup_environment.js +219 -0
  358. package/dist/onboarding/setup_workspace.d.ts +17 -0
  359. package/dist/onboarding/setup_workspace.js +129 -0
  360. package/dist/optimize/adapters/index.d.ts +4 -1
  361. package/dist/optimize/adapters/index.js +7 -0
  362. package/dist/optimize/adapters/kernelbench.js +14 -5
  363. package/dist/optimize/adapters/popcorn.js +3 -2
  364. package/dist/optimize/adapters/profile_benchmark.js +13 -7
  365. package/dist/optimize/adapters/rfbench.js +25 -14
  366. package/dist/optimize/adapters/types.d.ts +3 -0
  367. package/dist/optimize/adapters/zergpod.js +5 -2
  368. package/dist/optimize/baseline.js +10 -21
  369. package/dist/optimize/branch.js +9 -12
  370. package/dist/optimize/combine.js +2 -3
  371. package/dist/optimize/executor.js +34 -48
  372. package/dist/optimize/measurements.d.ts +55 -0
  373. package/dist/optimize/measurements.js +173 -0
  374. package/dist/optimize/results.d.ts +1 -0
  375. package/dist/optimize/results.js +11 -33
  376. package/dist/optimize/tge.js +2 -1
  377. package/dist/panes/initial_input.d.ts +19 -0
  378. package/dist/panes/initial_input.js +206 -0
  379. package/dist/panes/labels.d.ts +16 -0
  380. package/dist/panes/labels.js +42 -0
  381. package/dist/panes/model_catalog.d.ts +3 -3
  382. package/dist/panes/model_catalog.js +28 -4
  383. package/dist/panes/pane_registry.js +6 -2
  384. package/dist/panes/types.d.ts +3 -0
  385. package/dist/panes/ztc_launch.js +19 -0
  386. package/dist/plugin/optimization.d.ts +2 -0
  387. package/dist/plugin/optimization.js +1 -0
  388. package/dist/plugin/runtime.d.ts +2 -0
  389. package/dist/plugin/simulation.d.ts +6 -1
  390. package/dist/plugin/simulation_contract.d.ts +836 -1
  391. package/dist/plugin/simulation_documents.d.ts +22 -0
  392. package/dist/plugin/simulation_documents.js +1 -0
  393. package/dist/plugin/simulation_interventions.d.ts +103 -0
  394. package/dist/plugin/simulation_interventions.js +1 -0
  395. package/dist/plugin/simulation_living.d.ts +1 -0
  396. package/dist/plugin/simulation_living.js +3 -0
  397. package/dist/plugin/simulation_meetings.d.ts +121 -0
  398. package/dist/plugin/simulation_meetings.js +1 -0
  399. package/dist/plugin/simulation_story_review.d.ts +111 -0
  400. package/dist/plugin/simulation_story_review.js +1 -0
  401. package/dist/plugin/simulation_storyteller.d.ts +91 -0
  402. package/dist/plugin/simulation_storyteller.js +1 -0
  403. package/dist/plugin/simulation_structured.d.ts +2 -0
  404. package/dist/plugin/simulation_structured.js +2 -0
  405. package/dist/plugin/simulation_toolkit.d.ts +21 -0
  406. package/dist/plugin/simulation_toolkit.js +24 -0
  407. package/dist/prompts/tool_delegate_task.j2 +1 -1
  408. package/dist/remote-broker/Dockerfile +10 -0
  409. package/dist/remote-broker/archive.py +1227 -0
  410. package/dist/remote-broker/cutover.py +761 -0
  411. package/dist/remote-broker/requirements.txt +5 -0
  412. package/dist/remote-broker/resources.json +6 -0
  413. package/dist/remote-broker/resources.write-staging.example.json +18 -0
  414. package/dist/remote-broker/retained.py +686 -0
  415. package/dist/remote-broker/retained_cleanup.py +550 -0
  416. package/dist/remote-broker/server.py +648 -0
  417. package/dist/remote-broker/worker.py +1007 -0
  418. package/dist/router_auth.d.ts +31 -0
  419. package/dist/router_auth.js +155 -9
  420. package/dist/runbooks/catalog.d.ts +3 -0
  421. package/dist/runbooks/catalog.js +26 -0
  422. package/dist/runbooks/library.d.ts +11 -0
  423. package/dist/runbooks/library.js +95 -0
  424. package/dist/runbooks/package.d.ts +43 -0
  425. package/dist/runbooks/package.js +102 -0
  426. package/dist/session.d.ts +65 -13
  427. package/dist/session.js +548 -43
  428. package/dist/session_handoff.d.ts +25 -0
  429. package/dist/session_handoff.js +322 -0
  430. package/dist/session_handoff_contract.d.ts +89 -0
  431. package/dist/session_handoff_contract.js +179 -0
  432. package/dist/session_media.d.ts +41 -0
  433. package/dist/session_media.js +357 -0
  434. package/dist/session_ownership.d.ts +16 -0
  435. package/dist/session_ownership.js +221 -0
  436. package/dist/session_public.d.ts +10 -2
  437. package/dist/session_public.js +6 -1
  438. package/dist/simulation/dynamic_history.d.ts +42 -0
  439. package/dist/simulation/dynamic_history.js +173 -0
  440. package/dist/simulation/engines/swarm/pane_manager.js +7 -6
  441. package/dist/simulation/inference.d.ts +3 -1
  442. package/dist/simulation/inference.js +19 -1
  443. package/dist/simulation/intervention_generation.d.ts +27 -0
  444. package/dist/simulation/intervention_generation.js +246 -0
  445. package/dist/simulation/intervention_material.d.ts +4 -0
  446. package/dist/simulation/intervention_material.js +81 -0
  447. package/dist/simulation/living/context.d.ts +3130 -0
  448. package/dist/simulation/living/context.js +944 -0
  449. package/dist/simulation/living/definer.d.ts +12 -0
  450. package/dist/simulation/living/definer.js +223 -0
  451. package/dist/simulation/living/definition.d.ts +257 -0
  452. package/dist/simulation/living/definition.js +341 -0
  453. package/dist/simulation/living/frame.d.ts +259 -0
  454. package/dist/simulation/living/frame.js +223 -0
  455. package/dist/simulation/living/identifiers.d.ts +2 -0
  456. package/dist/simulation/living/identifiers.js +12 -0
  457. package/dist/simulation/living/index.d.ts +15 -0
  458. package/dist/simulation/living/index.js +15 -0
  459. package/dist/simulation/living/inference.d.ts +219 -0
  460. package/dist/simulation/living/inference.js +576 -0
  461. package/dist/simulation/living/mechanics.d.ts +54 -0
  462. package/dist/simulation/living/mechanics.js +588 -0
  463. package/dist/simulation/living/model-input.d.ts +25 -0
  464. package/dist/simulation/living/model-input.js +215 -0
  465. package/dist/simulation/living/open-world.d.ts +257 -0
  466. package/dist/simulation/living/open-world.js +102 -0
  467. package/dist/simulation/living/projection.d.ts +7 -0
  468. package/dist/simulation/living/projection.js +31 -0
  469. package/dist/simulation/living/recall.d.ts +17 -0
  470. package/dist/simulation/living/recall.js +25 -0
  471. package/dist/simulation/living/reduce.d.ts +15 -0
  472. package/dist/simulation/living/reduce.js +1853 -0
  473. package/dist/simulation/living/replay.d.ts +6 -0
  474. package/dist/simulation/living/replay.js +22 -0
  475. package/dist/simulation/living/runtime.d.ts +140 -0
  476. package/dist/simulation/living/runtime.js +3020 -0
  477. package/dist/simulation/living/shared-kinds.d.ts +24 -0
  478. package/dist/simulation/living/shared-kinds.js +96 -0
  479. package/dist/simulation/living/types.d.ts +2785 -0
  480. package/dist/simulation/living/types.js +669 -0
  481. package/dist/simulation/llm.d.ts +3 -1
  482. package/dist/simulation/llm.js +90 -30
  483. package/dist/simulation/memory/embedding.d.ts +1 -1
  484. package/dist/simulation/memory/embedding.js +9 -3
  485. package/dist/simulation/memory/filesystem.d.ts +2 -0
  486. package/dist/simulation/memory/filesystem.js +5 -1
  487. package/dist/simulation/memory/filesystem_file_tree.d.ts +4 -1
  488. package/dist/simulation/memory/filesystem_file_tree.js +41 -10
  489. package/dist/simulation/memory/filesystem_invariants.js +1 -2
  490. package/dist/simulation/memory/filesystem_types.d.ts +1 -0
  491. package/dist/simulation/memory/memory.d.ts +2 -0
  492. package/dist/simulation/memory/memory.js +5 -1
  493. package/dist/simulation/orchestration/intent.js +0 -2
  494. package/dist/simulation/orchestration/lifecycle.js +0 -2
  495. package/dist/simulation/plugin_host.d.ts +25 -1
  496. package/dist/simulation/plugin_host.js +334 -17
  497. package/dist/simulation/session_documents.d.ts +20 -0
  498. package/dist/simulation/session_documents.js +62 -0
  499. package/dist/simulation/session_input_validation.d.ts +43 -0
  500. package/dist/simulation/session_input_validation.js +364 -0
  501. package/dist/simulation/session_inputs.d.ts +32 -0
  502. package/dist/simulation/session_inputs.js +154 -0
  503. package/dist/simulation/telemetry.d.ts +11 -14
  504. package/dist/simulation/telemetry.js +177 -54
  505. package/dist/simulation/toolkit/bounded_turns.d.ts +13 -0
  506. package/dist/simulation/toolkit/bounded_turns.js +80 -0
  507. package/dist/simulation/toolkit/dynamic_agents.d.ts +25 -0
  508. package/dist/simulation/toolkit/dynamic_agents.js +218 -0
  509. package/dist/simulation/toolkit/dynamic_digest.d.ts +1 -0
  510. package/dist/simulation/toolkit/dynamic_digest.js +8 -0
  511. package/dist/simulation/toolkit/dynamic_documents.d.ts +7 -0
  512. package/dist/simulation/toolkit/dynamic_documents.js +71 -0
  513. package/dist/simulation/toolkit/dynamic_driver.d.ts +8 -0
  514. package/dist/simulation/toolkit/dynamic_driver.js +269 -0
  515. package/dist/simulation/toolkit/dynamic_game.d.ts +53 -0
  516. package/dist/simulation/toolkit/dynamic_game.js +805 -0
  517. package/dist/simulation/toolkit/dynamic_knowledge.d.ts +15 -0
  518. package/dist/simulation/toolkit/dynamic_knowledge.js +91 -0
  519. package/dist/simulation/toolkit/dynamic_mechanics.d.ts +22 -0
  520. package/dist/simulation/toolkit/dynamic_mechanics.js +910 -0
  521. package/dist/simulation/toolkit/dynamic_meetings.d.ts +239 -0
  522. package/dist/simulation/toolkit/dynamic_meetings.js +350 -0
  523. package/dist/simulation/toolkit/dynamic_progress.d.ts +131 -0
  524. package/dist/simulation/toolkit/dynamic_progress.js +214 -0
  525. package/dist/simulation/toolkit/dynamic_projection.d.ts +9 -0
  526. package/dist/simulation/toolkit/dynamic_projection.js +219 -0
  527. package/dist/simulation/toolkit/dynamic_runtime.d.ts +412 -0
  528. package/dist/simulation/toolkit/dynamic_runtime.js +6536 -0
  529. package/dist/simulation/toolkit/dynamic_state.d.ts +111 -0
  530. package/dist/simulation/toolkit/dynamic_state.js +1967 -0
  531. package/dist/simulation/toolkit/dynamic_story_review.d.ts +508 -0
  532. package/dist/simulation/toolkit/dynamic_story_review.js +333 -0
  533. package/dist/simulation/toolkit/dynamic_storyteller.d.ts +77 -0
  534. package/dist/simulation/toolkit/dynamic_storyteller.js +114 -0
  535. package/dist/simulation/toolkit/dynamic_storyteller_runtime.d.ts +26 -0
  536. package/dist/simulation/toolkit/dynamic_storyteller_runtime.js +126 -0
  537. package/dist/simulation/toolkit/dynamic_world.d.ts +22 -0
  538. package/dist/simulation/toolkit/dynamic_world.js +377 -0
  539. package/dist/simulation/toolkit/structured_output.d.ts +5 -0
  540. package/dist/simulation/toolkit/structured_output.js +321 -0
  541. package/dist/simulation/types/world.d.ts +2 -0
  542. package/dist/simulation/world/artifact_store.js +1 -1
  543. package/dist/simulation/world/memory_manager.d.ts +5 -0
  544. package/dist/simulation/world/memory_manager.js +17 -2
  545. package/dist/simulation/world/state.d.ts +3 -2
  546. package/dist/simulation/world/state.js +15 -5
  547. package/dist/skills/bundled/measurement-validation/SKILL.md +144 -0
  548. package/dist/terminal_attach.d.ts +1 -0
  549. package/dist/terminal_attach.js +113 -0
  550. package/dist/tool_progress.d.ts +3 -0
  551. package/dist/tool_progress.js +27 -0
  552. package/dist/types.d.ts +145 -10
  553. package/dist/types.js +3 -0
  554. package/dist/ui/core/input.d.ts +3 -0
  555. package/dist/ui/core/input_segments.d.ts +18 -1
  556. package/dist/ui/core/input_segments.js +131 -47
  557. package/dist/ui/core/input_state.d.ts +12 -0
  558. package/dist/ui/core/layout_yoga.d.ts +1 -1
  559. package/dist/ui/core/turn_metadata.d.ts +5 -0
  560. package/dist/ui/core/turn_metadata.js +22 -0
  561. package/dist/ui/ink/render.d.ts +2 -1
  562. package/dist/ui/ink/render.js +6 -5
  563. package/dist/ui/ratatui/headless_pane_manager.d.ts +14 -1
  564. package/dist/ui/ratatui/headless_pane_manager.js +293 -104
  565. package/dist/ui/ratatui/index.d.ts +1 -0
  566. package/dist/ui/ratatui/input.d.ts +4 -0
  567. package/dist/ui/ratatui/input.js +15 -0
  568. package/dist/ui/ratatui/ipc_path.d.ts +2 -0
  569. package/dist/ui/ratatui/ipc_path.js +29 -1
  570. package/dist/ui/ratatui/mux_persistence.d.ts +3 -1
  571. package/dist/ui/ratatui/mux_persistence.js +139 -1
  572. package/dist/ui/ratatui/pane_relay.d.ts +1 -1
  573. package/dist/ui/ratatui/pane_relay.js +55 -27
  574. package/dist/ui/ratatui/process.d.ts +17 -1
  575. package/dist/ui/ratatui/process.js +80 -7
  576. package/dist/ui/ratatui/protocol.d.ts +236 -1
  577. package/dist/ui/ratatui/protocol.js +90 -0
  578. package/dist/ui/scrollback.d.ts +2 -1
  579. package/dist/ui/scrollback.js +9 -4
  580. package/dist/ui/stream_typewriter.d.ts +8 -3
  581. package/dist/ui/stream_typewriter.js +30 -1
  582. package/dist/ui/streaming_scrollback.d.ts +1 -0
  583. package/dist/ui/streaming_scrollback.js +1 -1
  584. package/dist/ui/transcript_render_cache.js +10 -0
  585. package/dist/ui/views/activity_line.d.ts +5 -3
  586. package/dist/ui/views/activity_line.js +65 -7
  587. package/dist/ui/views/app.d.ts +6 -3
  588. package/dist/ui/views/app.js +17 -6
  589. package/dist/ui/views/completion_menu.js +10 -5
  590. package/dist/ui/views/input_area.d.ts +7 -3
  591. package/dist/ui/views/input_area.js +15 -9
  592. package/dist/ui/views/message_list.d.ts +4 -1
  593. package/dist/ui/views/message_list.js +89 -19
  594. package/dist/ui/views/queue_panel.d.ts +3 -1
  595. package/dist/ui/views/queue_panel.js +9 -2
  596. package/dist/ui/views/status_bar.d.ts +4 -2
  597. package/dist/ui/views/status_bar.js +28 -13
  598. package/dist/ui/views/theme.d.ts +2 -0
  599. package/dist/ui/views/theme.js +2 -0
  600. package/dist/ui/widgets/color_panel.js +18 -0
  601. package/dist/ui/widgets/form.js +8 -0
  602. package/dist/ui/widgets/log_view.js +7 -0
  603. package/dist/ui/widgets/navigator_panel.js +8 -0
  604. package/dist/ui/widgets/prompt_viewer.js +2 -0
  605. package/dist/ui/widgets/questionnaire.d.ts +2 -0
  606. package/dist/ui/widgets/questionnaire.js +79 -16
  607. package/dist/ui/widgets/resume_panel.d.ts +2 -0
  608. package/dist/ui/widgets/resume_panel.js +26 -15
  609. package/dist/ui/widgets/rewind_panel.d.ts +2 -2
  610. package/dist/ui/widgets/rewind_panel.js +4 -2
  611. package/dist/ui/widgets/select.js +16 -27
  612. package/dist/ui/widgets/settings_panel.js +16 -0
  613. package/dist/ui/widgets/status_line_panel.d.ts +1 -0
  614. package/dist/ui/widgets/status_line_panel.js +30 -4
  615. package/dist/ui/widgets/text_input.d.ts +2 -0
  616. package/dist/ui/widgets/text_input.js +15 -1
  617. package/dist/ui/widgets/types.d.ts +2 -0
  618. package/dist/usage_projection.d.ts +24 -0
  619. package/dist/usage_projection.js +130 -0
  620. package/dist/usage_snapshot.d.ts +36 -0
  621. package/dist/usage_snapshot.js +131 -0
  622. package/dist/utils/assistant_message.d.ts +2 -0
  623. package/dist/utils/assistant_message.js +5 -0
  624. package/dist/utils/atomic_write.d.ts +6 -2
  625. package/dist/utils/atomic_write.js +29 -4
  626. package/dist/utils/checked_shell.d.ts +25 -0
  627. package/dist/utils/checked_shell.js +107 -0
  628. package/dist/utils/compaction.d.ts +4 -1
  629. package/dist/utils/compaction.js +31 -7
  630. package/dist/utils/context.d.ts +4 -4
  631. package/dist/utils/context.js +14 -12
  632. package/dist/utils/deepseek.d.ts +2 -2
  633. package/dist/utils/deepseek.js +13 -5
  634. package/dist/utils/ephemeral.js +4 -0
  635. package/dist/utils/ephemeral_tools.d.ts +18 -0
  636. package/dist/utils/ephemeral_tools.js +97 -0
  637. package/dist/utils/jev_client.d.ts +4 -0
  638. package/dist/utils/jev_client.js +15 -0
  639. package/dist/utils/model_cache.d.ts +15 -0
  640. package/dist/utils/model_cache.js +143 -0
  641. package/dist/utils/models.d.ts +4 -2
  642. package/dist/utils/models.js +12 -16
  643. package/dist/utils/reasoning.d.ts +8 -1
  644. package/dist/utils/reasoning.js +31 -11
  645. package/dist/utils/router_catalog.d.ts +49 -0
  646. package/dist/utils/router_catalog.js +271 -0
  647. package/dist/utils/sandbox.d.ts +9 -0
  648. package/dist/utils/sandbox.js +118 -15
  649. package/dist/utils/shell.d.ts +7 -0
  650. package/dist/utils/shell.js +98 -34
  651. package/dist/utils/subprocess_env.d.ts +3 -0
  652. package/dist/utils/subprocess_env.js +27 -1
  653. package/dist/web/accounts_mirror.d.ts +3 -1
  654. package/dist/web/accounts_mirror.js +13 -9
  655. package/dist/web/child_publication.d.ts +17 -0
  656. package/dist/web/child_publication.js +45 -0
  657. package/dist/web/mirror_authenticator.d.ts +31 -11
  658. package/dist/web/mirror_authenticator.js +67 -17
  659. package/dist/web/mirror_client.d.ts +31 -4
  660. package/dist/web/mirror_client.js +189 -23
  661. package/dist/web/mirror_stream.d.ts +84 -22
  662. package/dist/web/mirror_stream.js +676 -95
  663. package/dist/web/observer_query_redaction.d.ts +6 -0
  664. package/dist/web/observer_query_redaction.js +58 -0
  665. package/dist/web/pane_registry.d.ts +2 -0
  666. package/dist/web/pane_registry.js +86 -22
  667. package/dist/web/tool_image.d.ts +6 -0
  668. package/dist/web/tool_image.js +17 -0
  669. package/dist/web/view_plugin.d.ts +14 -0
  670. package/dist/web/view_plugin.js +23 -1
  671. package/dist/worker/executor.js +31 -12
  672. package/dist/worker/index.d.ts +1 -0
  673. package/dist/worker/toolsets.js +3 -1
  674. package/dist/worker/types.d.ts +12 -1
  675. package/dist/workflows/registry.js +1 -0
  676. package/dist/workflows/registry_handle.d.ts +2 -0
  677. package/dist/workflows/runtime.d.ts +2 -0
  678. package/dist/workflows/runtime.js +1 -0
  679. package/dist/workspace_agent.d.ts +7 -0
  680. package/dist/workspace_agent.js +36 -0
  681. package/dist/workspace_collision.d.ts +76 -0
  682. package/dist/workspace_collision.js +688 -0
  683. package/dist/workspace_layout.d.ts +18 -0
  684. package/dist/workspace_layout.js +121 -0
  685. package/dist/workspace_runtime.d.ts +44 -0
  686. package/dist/workspace_runtime.js +82 -0
  687. package/dist/workspace_session_context.d.ts +62 -0
  688. package/dist/workspace_session_context.js +160 -0
  689. package/dist/zlm-embed.mjs +32518 -9615
  690. package/dist/zlm.js +1 -0
  691. package/node_modules/@zerg/model-contracts/LICENSE.md +375 -0
  692. package/node_modules/@zerg/model-contracts/README.md +90 -0
  693. package/node_modules/@zerg/model-contracts/index.d.ts +20 -0
  694. package/node_modules/@zerg/model-contracts/index.js +223 -0
  695. package/node_modules/@zerg/model-contracts/package.json +8 -0
  696. package/node_modules/glob/node_modules/brace-expansion/index.js +78 -22
  697. package/node_modules/glob/node_modules/brace-expansion/package.json +1 -1
  698. package/node_modules/readdir-glob/node_modules/brace-expansion/index.js +78 -22
  699. package/node_modules/readdir-glob/node_modules/brace-expansion/package.json +1 -1
  700. package/package.json +45 -24
  701. package/packages/model-contracts/LICENSE.md +375 -0
  702. package/packages/model-contracts/README.md +90 -0
  703. package/packages/model-contracts/index.d.ts +20 -0
  704. package/packages/model-contracts/index.js +223 -0
  705. package/packages/model-contracts/package.json +8 -0
  706. package/remote-broker/Dockerfile +10 -0
  707. package/remote-broker/README.md +308 -0
  708. package/remote-broker/archive.py +1227 -0
  709. package/remote-broker/cutover.py +761 -0
  710. package/remote-broker/requirements.txt +5 -0
  711. package/remote-broker/resources.example.json +6 -0
  712. package/remote-broker/resources.write-staging.example.json +18 -0
  713. package/remote-broker/retained.py +686 -0
  714. package/remote-broker/retained_cleanup.py +550 -0
  715. package/remote-broker/server.py +648 -0
  716. package/scripts/build-remote-broker.mjs +13 -0
  717. package/scripts/ensure-local-packages.mjs +6 -0
  718. package/scripts/node-pty-helpers.mjs +103 -0
@@ -0,0 +1,1227 @@
1
+ """Operator-only confirmed receipt maintenance; no broker RPC exposes this CLI."""
2
+
3
+ import argparse
4
+ import contextlib
5
+ import fcntl
6
+ import hashlib
7
+ import json
8
+ import logging
9
+ import os
10
+ import runpy
11
+ import stat
12
+ import subprocess
13
+ import sys
14
+ import uuid
15
+ from pathlib import Path
16
+ from typing import Any, Iterator, Literal
17
+
18
+ from pydantic import BaseModel, ConfigDict, Field
19
+
20
+ L = logging.getLogger(__name__)
21
+ INSTALLATION = Path(__file__).resolve().parent
22
+ MAX_RECORD_BYTES = 1024 * 1024
23
+ MAX_SOURCE_BYTES = 256 * 1024 * 1024
24
+
25
+
26
+ class Endpoint(BaseModel):
27
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
28
+ host: str = Field(min_length=1, max_length=253)
29
+ user: str = Field(min_length=1, max_length=100)
30
+ port: int = Field(ge=1, le=65535)
31
+
32
+
33
+ class Resource(BaseModel):
34
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
35
+ id: str = Field(min_length=1, max_length=100)
36
+ root: str = Field(min_length=1, max_length=4096)
37
+ endpoint: Endpoint
38
+ write_staging_directory: str | None = Field(default=None, max_length=4096)
39
+
40
+
41
+ class Policy(BaseModel):
42
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
43
+ version: Literal[1]
44
+ receipt_archive_index: bool = False
45
+ resources: list[Resource] = Field(min_length=1, max_length=100)
46
+ legacy_write_staging_directories: list[str] = Field(
47
+ default_factory=list, max_length=100
48
+ )
49
+
50
+
51
+ class View(BaseModel):
52
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
53
+ configured_path: str = Field(min_length=1, max_length=4096)
54
+ directory: str = Field(min_length=1, max_length=4096)
55
+
56
+
57
+ class Manifest(BaseModel):
58
+ model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
59
+ version: Literal[1]
60
+ clients_quiesced: Literal[True]
61
+ state_directory: str = Field(min_length=1, max_length=4096)
62
+ resource_policy_file: str = Field(min_length=1, max_length=4096)
63
+ worker_file: str = Field(min_length=1, max_length=4096)
64
+ client_journals: list[str] = Field(min_length=1, max_length=100)
65
+ staging_views: list[View] = Field(min_length=1, max_length=201)
66
+ root_views: list[View] = Field(default_factory=list, max_length=100)
67
+
68
+
69
+ def encode(value: Any) -> bytes:
70
+ return json.dumps(
71
+ value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
72
+ ).encode("utf-8", errors="backslashreplace")
73
+
74
+
75
+ def sha(data: bytes) -> str:
76
+ return hashlib.sha256(data).hexdigest()
77
+
78
+
79
+ def sync_directory(path: Path) -> None:
80
+ descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
81
+ try:
82
+ os.fsync(descriptor)
83
+ finally:
84
+ os.close(descriptor)
85
+
86
+
87
+ def canonical(path: str, directory: bool = True, private: bool = True) -> Path:
88
+ result = Path(path)
89
+ if not result.is_absolute() or str(result.resolve(strict=True)) != path:
90
+ raise ValueError("Maintenance paths must be existing canonical absolute paths")
91
+ info = result.lstat()
92
+ if (
93
+ directory
94
+ and not stat.S_ISDIR(info.st_mode)
95
+ or not directory
96
+ and not stat.S_ISREG(info.st_mode)
97
+ ):
98
+ raise ValueError("Maintenance inputs must be regular files or real directories")
99
+ if private and (info.st_uid != os.geteuid() or info.st_mode & 0o077):
100
+ raise ValueError(
101
+ "Maintenance journals and staging must be private to the operator UID"
102
+ )
103
+ return result
104
+
105
+
106
+ def fingerprint(path: Path) -> dict[str, int]:
107
+ info = path.lstat()
108
+ return {
109
+ "device": info.st_dev,
110
+ "inode": info.st_ino,
111
+ "uid": info.st_uid,
112
+ "mode": info.st_mode,
113
+ }
114
+
115
+
116
+ def bounded(path: Path, limit: int = MAX_RECORD_BYTES) -> bytes:
117
+ descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
118
+ with os.fdopen(descriptor, "rb") as stream:
119
+ before = os.fstat(stream.fileno())
120
+ if not stat.S_ISREG(before.st_mode) or before.st_size > limit:
121
+ raise ValueError("Maintenance evidence exceeds its regular-file bound")
122
+ data = stream.read(limit + 1)
123
+ after = os.fstat(stream.fileno())
124
+ if len(data) > limit or (
125
+ before.st_dev,
126
+ before.st_ino,
127
+ before.st_size,
128
+ before.st_mtime_ns,
129
+ ) != (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns):
130
+ raise ValueError("Maintenance evidence changed during its bounded read")
131
+ return data
132
+
133
+
134
+ def process_namespace() -> dict[str, Any]:
135
+ if sys.platform == "linux":
136
+ boot = (
137
+ bounded(Path("/proc/sys/kernel/random/boot_id"), 4096)
138
+ .decode()
139
+ .strip()
140
+ .lower()
141
+ )
142
+ namespace = Path("/proc/self/ns/pid").stat()
143
+ result: dict[str, Any] = {
144
+ "platform": "linux",
145
+ "pid_namespace": [namespace.st_dev, namespace.st_ino],
146
+ }
147
+ elif sys.platform == "darwin":
148
+ query = subprocess.run(
149
+ ["/usr/sbin/sysctl", "-n", "kern.bootsessionuuid"],
150
+ capture_output=True,
151
+ text=True,
152
+ timeout=2,
153
+ check=True,
154
+ )
155
+ boot = query.stdout.strip().lower()
156
+ result = {"platform": "darwin"}
157
+ else:
158
+ raise ValueError(
159
+ "Operator lock recovery requires a supported process namespace"
160
+ )
161
+ if str(uuid.UUID(boot)) != boot:
162
+ raise ValueError("Invalid operator kernel boot identity")
163
+ return {**result, "boot_id": boot}
164
+
165
+
166
+ def overlaps(first: Path, second: Path) -> bool:
167
+ return first.is_relative_to(second) or second.is_relative_to(first)
168
+
169
+
170
+ def private_tree(path: Path) -> None:
171
+ missing: list[Path] = []
172
+ current = path
173
+ while not os.path.lexists(current):
174
+ missing.append(current)
175
+ current = current.parent
176
+ canonical(str(current), private=False)
177
+ for directory in reversed(missing):
178
+ directory.mkdir(mode=0o700)
179
+ sync_directory(directory)
180
+ sync_directory(directory.parent)
181
+
182
+
183
+ def publish(path: Path, data: bytes, replace: bool = False) -> None:
184
+ private_tree(path.parent)
185
+ canonical(str(path.parent))
186
+ temporary = path.parent / (".archive-" + str(uuid.uuid4()))
187
+ descriptor = os.open(
188
+ temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600
189
+ )
190
+ try:
191
+ with os.fdopen(descriptor, "wb") as stream:
192
+ stream.write(data)
193
+ stream.flush()
194
+ os.fsync(stream.fileno())
195
+ if replace:
196
+ os.replace(temporary, path)
197
+ else:
198
+ try:
199
+ os.link(temporary, path)
200
+ except FileExistsError:
201
+ if bounded(path, max(MAX_RECORD_BYTES, len(data))) != data:
202
+ raise ValueError(
203
+ "Existing archive evidence differs; explicit recovery is required"
204
+ )
205
+ sync_directory(path.parent)
206
+ finally:
207
+ with contextlib.suppress(FileNotFoundError):
208
+ temporary.unlink()
209
+
210
+
211
+ class JournalArchive:
212
+ def __init__(self, manifest_file: str) -> None:
213
+ self._init_manifest_system(manifest_file)
214
+ self._init_worker_system()
215
+ self._init_policy_system()
216
+ self._init_binding_system()
217
+
218
+ def _init_manifest_system(self, manifest_file: str) -> None:
219
+ self.manifest_path = canonical(manifest_file, directory=False, private=False)
220
+ self.manifest_bytes = bounded(self.manifest_path)
221
+ self.manifest = Manifest.model_validate_json(self.manifest_bytes)
222
+ self.control = canonical(self.manifest.state_directory)
223
+ self.clients = [canonical(path) for path in self.manifest.client_journals]
224
+ if len(set(self.clients)) != len(self.clients):
225
+ raise ValueError("Client journals must be unique")
226
+ for index, first in enumerate([self.control, *self.clients]):
227
+ if any(
228
+ overlaps(first, second)
229
+ for second in [self.control, *self.clients][index + 1 :]
230
+ ):
231
+ raise ValueError("Host and client journal namespaces must be disjoint")
232
+ self.fences = [
233
+ self.control / "journal-maintenance.json",
234
+ *[Path(str(path) + ".maintenance.json") for path in self.clients],
235
+ ]
236
+
237
+ def _init_worker_system(self) -> None:
238
+ self.worker = canonical(
239
+ self.manifest.worker_file, directory=False, private=False
240
+ )
241
+ packaged = canonical(
242
+ str(INSTALLATION / "worker.py"), directory=False, private=False
243
+ )
244
+ self.worker_bytes = bounded(self.worker, 8 * MAX_RECORD_BYTES)
245
+ if self.worker_bytes != bounded(packaged, 8 * MAX_RECORD_BYTES):
246
+ raise ValueError(
247
+ "Maintenance requires the matching packaged immutable worker"
248
+ )
249
+ self.worker_api = runpy.run_path(str(packaged))
250
+
251
+ def _init_policy_system(self) -> None:
252
+ self.policy_path = canonical(
253
+ self.manifest.resource_policy_file, directory=False, private=False
254
+ )
255
+ self.policy_bytes = bounded(self.policy_path)
256
+ self.policy = Policy.model_validate_json(self.policy_bytes)
257
+ if not self.policy.receipt_archive_index:
258
+ raise ValueError(
259
+ "Enable receipt_archive_index in immutable operator policy before maintenance"
260
+ )
261
+ self.resources = {resource.id: resource for resource in self.policy.resources}
262
+ if len(self.resources) != len(self.policy.resources):
263
+ raise ValueError("Policy resource IDs must be unique")
264
+ configured = {
265
+ "/data/.ztc-delete-quarantine",
266
+ *self.policy.legacy_write_staging_directories,
267
+ }
268
+ configured.update(
269
+ resource.write_staging_directory
270
+ for resource in self.policy.resources
271
+ if resource.write_staging_directory is not None
272
+ )
273
+ self.staging = {
274
+ view.configured_path: canonical(view.directory)
275
+ for view in self.manifest.staging_views
276
+ }
277
+ if (
278
+ set(self.staging) != configured
279
+ or len(self.staging) != len(self.manifest.staging_views)
280
+ or len(set(self.staging.values())) != len(self.staging)
281
+ ):
282
+ raise ValueError(
283
+ "A complete unique default/current/historical staging inventory is required"
284
+ )
285
+ views = {
286
+ view.configured_path: view.directory for view in self.manifest.root_views
287
+ }
288
+ if len(views) != len(self.manifest.root_views) or set(views) - {
289
+ resource.root for resource in self.policy.resources
290
+ }:
291
+ raise ValueError("Root views must have unique configured policy paths")
292
+ self.roots = {
293
+ resource.id: canonical(
294
+ views.get(resource.root, resource.root), private=False
295
+ )
296
+ for resource in self.policy.resources
297
+ }
298
+ for root in self.roots.values():
299
+ if any(
300
+ overlaps(root, protected)
301
+ for protected in [self.control, *self.clients, *self.staging.values()]
302
+ ):
303
+ raise ValueError(
304
+ "Resource views overlap maintenance journals or private staging"
305
+ )
306
+
307
+ def _init_binding_system(self) -> None:
308
+ paths = [
309
+ self.control,
310
+ *self.clients,
311
+ *self.staging.values(),
312
+ *self.roots.values(),
313
+ self.worker,
314
+ self.policy_path,
315
+ ]
316
+ self.bindings = {str(path): fingerprint(path) for path in paths}
317
+ self.process_namespace = process_namespace()
318
+ self.records: dict[str, dict[str, Any]] = {}
319
+ self.source_bytes = 0
320
+
321
+ @contextlib.contextmanager
322
+ def locked(self, digest: str | None = None) -> Iterator[None]:
323
+ with contextlib.ExitStack() as handles:
324
+ for journal in sorted(self.clients):
325
+ lock = Path(str(journal) + ".admission-lock")
326
+ try:
327
+ lock.mkdir(mode=0o700)
328
+ except FileExistsError:
329
+ if digest is None:
330
+ raise ValueError(
331
+ "Client journal is locked; inspect its owner before confirmed recovery"
332
+ )
333
+ self.recover_dead_operator_lock(lock, journal, digest)
334
+ lock.mkdir(mode=0o700)
335
+ metadata = encode(
336
+ {
337
+ "version": 1,
338
+ "nonce": str(uuid.uuid4()),
339
+ "pid": os.getpid(),
340
+ "operator": "ztc-archive",
341
+ "digest": digest,
342
+ "manifest_sha256": sha(self.manifest_bytes),
343
+ "journal_identity": fingerprint(journal),
344
+ "process_namespace": self.process_namespace,
345
+ }
346
+ )
347
+ publish(lock / "owner.json", metadata)
348
+ identity = fingerprint(lock)
349
+ handles.callback(self.release_lock, lock, identity, metadata)
350
+ sync_directory(lock.parent)
351
+ descriptor = os.open(
352
+ self.control / "lock",
353
+ os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK,
354
+ 0o600,
355
+ )
356
+ handles.callback(os.close, descriptor)
357
+ if not stat.S_ISREG(os.fstat(descriptor).st_mode):
358
+ raise ValueError("Broker lock must be a regular file")
359
+ fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
360
+ sync_directory(self.control)
361
+ yield
362
+
363
+ def recover_dead_operator_lock(
364
+ self, lock: Path, journal: Path, digest: str
365
+ ) -> None:
366
+ canonical(str(lock))
367
+ identity = fingerprint(lock)
368
+ data = bounded(lock / "owner.json")
369
+ owner = json.loads(data)
370
+ if (
371
+ not isinstance(owner, dict)
372
+ or type(owner.get("version")) is not int
373
+ or owner["version"] != 1
374
+ or owner.get("operator") != "ztc-archive"
375
+ or owner.get("digest") != digest
376
+ or owner.get("manifest_sha256") != sha(self.manifest_bytes)
377
+ or owner.get("journal_identity") != fingerprint(journal)
378
+ or owner.get("process_namespace") != self.process_namespace
379
+ or type(owner.get("pid")) is not int
380
+ or owner["pid"] <= 0
381
+ or not isinstance(owner.get("nonce"), str)
382
+ or str(uuid.UUID(owner["nonce"])) != owner["nonce"]
383
+ ):
384
+ raise ValueError(
385
+ "Client journal lock requires explicit reviewed recovery; its owner is unknown or belongs to another namespace"
386
+ )
387
+ try:
388
+ os.kill(owner["pid"], 0)
389
+ except ProcessLookupError:
390
+ pass
391
+ else:
392
+ raise ValueError(
393
+ "Operator journal lock owner is still live; confirmed resume cannot steal it"
394
+ )
395
+ retained = Path(str(lock) + ".retained-" + owner["nonce"])
396
+ descriptor = os.open(lock.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
397
+ try:
398
+ if fingerprint(lock) != identity or bounded(lock / "owner.json") != data:
399
+ raise ValueError(
400
+ "Operator lock changed during review; preserve its evidence"
401
+ )
402
+ self.worker_api["rename_without_replacement"](
403
+ descriptor, lock.name, descriptor, retained.name
404
+ )
405
+ os.fsync(descriptor)
406
+ if (
407
+ fingerprint(retained) != identity
408
+ or bounded(retained / "owner.json") != data
409
+ ):
410
+ with contextlib.suppress(OSError):
411
+ self.worker_api["rename_without_replacement"](
412
+ descriptor, retained.name, descriptor, lock.name
413
+ )
414
+ os.fsync(descriptor)
415
+ raise ValueError(
416
+ "Operator lock recovery changed; original evidence was retained"
417
+ )
418
+ finally:
419
+ os.close(descriptor)
420
+
421
+ @staticmethod
422
+ def release_lock(path: Path, identity: dict[str, int], metadata: bytes) -> None:
423
+ if fingerprint(path) != identity or bounded(path / "owner.json") != metadata:
424
+ raise ValueError(
425
+ "Maintenance lock changed; explicit operator recovery is required"
426
+ )
427
+ (path / "owner.json").unlink()
428
+ path.rmdir()
429
+ sync_directory(path.parent)
430
+
431
+ def capture(self, path: Path) -> bytes:
432
+ data = bounded(path)
433
+ self.source_bytes += len(data)
434
+ if self.source_bytes > MAX_SOURCE_BYTES:
435
+ raise ValueError("Maintenance export exceeds its aggregate 256 MiB bound")
436
+ return data
437
+
438
+ def scan(self, directory: Path, source: str) -> list[dict[str, Any]]:
439
+ canonical(str(directory))
440
+ names = sorted(directory.iterdir())
441
+ if len(names) > 10000:
442
+ raise ValueError(
443
+ "Active journal exceeds its 10,000-entry bound; preserve and inspect it"
444
+ )
445
+ result: list[dict[str, Any]] = []
446
+ for path in names:
447
+ if path.suffix != ".json" or str(uuid.UUID(path.stem)) != path.stem:
448
+ raise ValueError("Invalid active journal record name")
449
+ canonical(str(path), directory=False)
450
+ data = self.capture(path)
451
+ value = json.loads(data)
452
+ if (
453
+ not isinstance(value, dict)
454
+ or value.get("version") != 1
455
+ or value.get("id") != path.stem
456
+ or value.get("operation") not in ["write", "delete"]
457
+ ):
458
+ raise ValueError("Invalid active journal operation identity")
459
+ requested = value.get("requested_path")
460
+ if (
461
+ not isinstance(requested, str)
462
+ or not requested
463
+ or os.path.isabs(requested)
464
+ or "\x00" in requested
465
+ ):
466
+ raise ValueError("Invalid original relative target path")
467
+ endpoint = Endpoint.model_validate(value.get("endpoint"))
468
+ owner = value.get("owner_id")
469
+ call = value.get("tool_call_id")
470
+ if (
471
+ not isinstance(owner, str)
472
+ or not owner
473
+ or len(owner) > 200
474
+ or call is not None
475
+ and (not isinstance(call, str) or not call or len(call) > 200)
476
+ ):
477
+ raise ValueError("Invalid original owner or originating-call linkage")
478
+ target_evidence = value.get("target")
479
+ if source == "host":
480
+ if not isinstance(target_evidence, dict):
481
+ raise ValueError("Invalid original target verification evidence")
482
+ target = target_evidence.get("path")
483
+ else:
484
+ target = target_evidence
485
+ statuses = (
486
+ ["pending", "resolved", "not_dispatched"]
487
+ if source != "host"
488
+ else [
489
+ "pending",
490
+ "verified",
491
+ "failed",
492
+ "unverified",
493
+ "dry_run",
494
+ "cancelled",
495
+ ]
496
+ )
497
+ if (
498
+ value.get("status") not in statuses
499
+ or type(value.get("version")) is not int
500
+ ):
501
+ raise ValueError("Invalid original journal status or schema version")
502
+ if not isinstance(target, str) or not os.path.isabs(target):
503
+ raise ValueError("Invalid original target evidence")
504
+ record = {
505
+ "source": source,
506
+ "id": path.stem,
507
+ "sha256": sha(data),
508
+ "size": len(data),
509
+ "identity": fingerprint(path),
510
+ "status": value.get("status"),
511
+ "owner_id": owner,
512
+ "tool_call_id": call,
513
+ "endpoint": endpoint.model_dump(),
514
+ "resource_id": value.get("resource_id"),
515
+ "operation": value["operation"],
516
+ "target": target,
517
+ "requested_path": value.get("requested_path"),
518
+ "archive": False,
519
+ "reason": "requires recovery",
520
+ }
521
+ self.records[source + "/" + path.stem] = {
522
+ "path": path,
523
+ "bytes": data,
524
+ "value": value,
525
+ "record": record,
526
+ }
527
+ result.append(record)
528
+ return result
529
+
530
+ def preview(self) -> dict[str, Any]:
531
+ if any(os.path.lexists(path) for path in self.fences) or os.path.lexists(
532
+ self.control / "cutover-fence.json"
533
+ ):
534
+ raise ValueError(
535
+ "Maintenance or cutover is frozen; inspect and resume its confirmed transaction"
536
+ )
537
+ self.records = {}
538
+ self.source_bytes = 0
539
+ receipts = self.control / "receipts"
540
+ if not receipts.exists():
541
+ raise ValueError(
542
+ "Receipt journal must already exist; initialization is a separate host setup operation"
543
+ )
544
+ host = self.scan(receipts, "host")
545
+ clients = [
546
+ self.scan(path, "client-" + str(index))
547
+ for index, path in enumerate(self.clients)
548
+ ]
549
+ if sum(len(records) for records in clients) > 10000:
550
+ raise ValueError(
551
+ "Participating client journals exceed the aggregate 10,000-entry maintenance bound"
552
+ )
553
+ staged: set[str] = set()
554
+ blockers: list[str] = []
555
+ for configured, directory in sorted(self.staging.items()):
556
+ entries = list(directory.iterdir())
557
+ if len(entries) > 10000:
558
+ raise ValueError("Private staging exceeds its bounded inventory")
559
+ for entry in entries:
560
+ try:
561
+ if str(uuid.UUID(entry.name)) != entry.name:
562
+ raise ValueError("Unknown staging entry")
563
+ except ValueError:
564
+ blockers.append(
565
+ "Unknown retained staging evidence at " + configured
566
+ )
567
+ continue
568
+ staged.add(entry.name)
569
+ by_id = {record["id"]: record for record in host}
570
+ if staged - set(by_id):
571
+ blockers.append("Unlinked private staging evidence requires recovery")
572
+ client_records = [record for records in clients for record in records]
573
+ client_linkage: dict[str, list[dict[str, Any]]] = {}
574
+ for record in client_records:
575
+ client_linkage.setdefault(record["id"], []).append(record)
576
+ for record in host:
577
+ value = self.records["host/" + record["id"]]["value"]
578
+ linked = client_linkage.get(record["id"], [])
579
+ if record["status"] not in ["verified", "failed", "dry_run", "cancelled"]:
580
+ record["reason"] = "pending or unverified remote outcome"
581
+ elif value.get("admission_resolved") is not True:
582
+ record["reason"] = "unresolved remote admission or recovery"
583
+ elif record["id"] in staged:
584
+ record["reason"] = "private staging still retains operation evidence"
585
+ elif not linked or any(other["status"] != "resolved" for other in linked):
586
+ record["reason"] = "missing or unresolved participating client linkage"
587
+ elif any(not self.same_origin(record, other) for other in linked):
588
+ record["reason"] = "original client/host linkage differs"
589
+ elif record["status"] == "verified" and (
590
+ not isinstance(value.get("verification"), dict)
591
+ or not (
592
+ value["verification"].get("target_matches") is True
593
+ or value["verification"].get("target_absent") is True
594
+ )
595
+ ):
596
+ record["reason"] = (
597
+ "verified receipt lacks its original target verification"
598
+ )
599
+ elif record["status"] == "cancelled" and (
600
+ not isinstance(value.get("resolution"), dict)
601
+ or value["resolution"].get("decision") != "accept_current_state"
602
+ ):
603
+ record["reason"] = (
604
+ "cancellation lacks its confirmed original reconciliation"
605
+ )
606
+ else:
607
+ record.update(
608
+ archive=True,
609
+ reason="terminal acknowledged outcome with exact client linkage",
610
+ )
611
+ for record in client_records:
612
+ receipt = by_id.get(record["id"])
613
+ if record["id"] in staged:
614
+ record["reason"] = "private staging still retains operation evidence"
615
+ elif record["status"] == "pending":
616
+ record["reason"] = "unresolved client admission"
617
+ elif record["status"] == "not_dispatched" and receipt is None:
618
+ record.update(
619
+ archive=True,
620
+ reason="terminal not-dispatched origin; retain host replay protection",
621
+ )
622
+ elif (
623
+ record["status"] == "resolved"
624
+ and receipt is not None
625
+ and receipt["archive"]
626
+ and self.same_origin(record, receipt)
627
+ ):
628
+ record.update(
629
+ archive=True,
630
+ reason="terminal client linked to archived acknowledged remote receipt",
631
+ )
632
+ else:
633
+ record["reason"] = "missing, uncertain or conflicting remote linkage"
634
+ origins: dict[str, str] = {}
635
+ for record in [*host, *client_records]:
636
+ if record["tool_call_id"] is not None:
637
+ endpoint = record["endpoint"]
638
+ key = sha(
639
+ encode(
640
+ [
641
+ "call",
642
+ endpoint["host"],
643
+ endpoint["user"],
644
+ endpoint["port"],
645
+ record["owner_id"],
646
+ record["tool_call_id"],
647
+ ]
648
+ )
649
+ )
650
+ if key in origins and origins[key] != record["id"]:
651
+ blockers.append(
652
+ "Conflicting original operation IDs for an originating call"
653
+ )
654
+ origins[key] = record["id"]
655
+ index_states: dict[str, str | None] = {}
656
+ for source, directory, archives in [
657
+ ("host", self.control / "replay-index", self.control / "receipt-archives"),
658
+ *[
659
+ (
660
+ "client-" + str(i),
661
+ Path(str(path) + ".replay"),
662
+ Path(str(path) + ".archives"),
663
+ )
664
+ for i, path in enumerate(self.clients)
665
+ ],
666
+ ]:
667
+ required = os.path.lexists(archives) or (
668
+ source != "host"
669
+ and os.path.lexists(
670
+ Path(str(self.clients[int(source[7:])]) + ".replay-required.json")
671
+ )
672
+ )
673
+ header = self.worker_api["replay_manifest"](str(directory), required)
674
+ if header is not None:
675
+ for prefix, bucket in header["buckets"].items():
676
+ self.worker_api["replay_bucket"](str(directory), prefix, bucket)
677
+ index_states[source] = sha(bounded(directory / "manifest.json"))
678
+ else:
679
+ index_states[source] = None
680
+ leases = self.control / "leases.json"
681
+ lease_bytes = bounded(leases, 16 * MAX_RECORD_BYTES) if leases.exists() else b""
682
+ plan = {
683
+ "version": 1,
684
+ "manifest_sha256": sha(self.manifest_bytes),
685
+ "worker_sha256": sha(self.worker_bytes),
686
+ "policy_sha256": sha(self.policy_bytes),
687
+ "operator_sha256": sha(bounded(Path(__file__), 8 * MAX_RECORD_BYTES)),
688
+ "bindings": self.bindings,
689
+ "lease_sha256": sha(lease_bytes),
690
+ "journals": {
691
+ "host": host,
692
+ **{"client-" + str(i): records for i, records in enumerate(clients)},
693
+ },
694
+ "replay_before": index_states,
695
+ "blockers": blockers,
696
+ }
697
+ return {
698
+ "ready": not blockers,
699
+ "digest": sha(encode(plan)),
700
+ "plan": plan,
701
+ "archive_count": sum(
702
+ record["archive"] for record in [*host, *client_records]
703
+ ),
704
+ "preserved_count": sum(
705
+ not record["archive"] for record in [*host, *client_records]
706
+ ),
707
+ "index_initialization": [
708
+ source for source, value in index_states.items() if value is None
709
+ ],
710
+ }
711
+
712
+ def archive_roots(self, digest: str) -> dict[str, Path]:
713
+ return {
714
+ "host": self.control / "receipt-archives" / digest,
715
+ **{
716
+ "client-" + str(index): Path(str(path) + ".archives") / digest
717
+ for index, path in enumerate(self.clients)
718
+ },
719
+ }
720
+
721
+ def journal_roots(self) -> dict[str, Path]:
722
+ return {
723
+ "host": self.control / "receipts",
724
+ **{"client-" + str(index): path for index, path in enumerate(self.clients)},
725
+ }
726
+
727
+ def index_roots(self) -> dict[str, Path]:
728
+ return {
729
+ "host": self.control / "replay-index",
730
+ **{
731
+ "client-" + str(index): Path(str(path) + ".replay")
732
+ for index, path in enumerate(self.clients)
733
+ },
734
+ }
735
+
736
+ def fence_bytes(self, digest: str) -> bytes:
737
+ return encode(
738
+ {
739
+ "version": 1,
740
+ "digest": digest,
741
+ "manifest_sha256": sha(self.manifest_bytes),
742
+ "worker_sha256": sha(self.worker_bytes),
743
+ "policy_sha256": sha(self.policy_bytes),
744
+ }
745
+ )
746
+
747
+ def require_fences(self, digest: str) -> None:
748
+ expected = self.fence_bytes(digest)
749
+ for path in self.fences:
750
+ if not os.path.lexists(path) or bounded(path) != expected:
751
+ raise ValueError(
752
+ "Maintenance fence differs or is missing; preserve evidence for explicit recovery"
753
+ )
754
+
755
+ def record_export_path(self, root: Path, source: str, identifier: str) -> Path:
756
+ return (
757
+ root
758
+ / ("receipts" if source == "host" else "records")
759
+ / (identifier + ".json")
760
+ )
761
+
762
+ def export(self, plan: dict[str, Any], digest: str) -> None:
763
+ roots = self.archive_roots(digest)
764
+ payload = encode(plan)
765
+ if len(payload) > 16 * MAX_RECORD_BYTES:
766
+ raise ValueError("Concrete archive plan exceeds its 16 MiB bound")
767
+ for root in roots.values():
768
+ publish(root / "plan.json", payload)
769
+ for name, data in [
770
+ ("manifest.json", self.manifest_bytes),
771
+ ("resources.json", self.policy_bytes),
772
+ ("worker.py", self.worker_bytes),
773
+ ("operator.py", bounded(Path(__file__), 8 * MAX_RECORD_BYTES)),
774
+ ]:
775
+ publish(roots["host"] / name, data)
776
+ for source, records in plan["journals"].items():
777
+ for record in records:
778
+ data = self.records[source + "/" + record["id"]]["bytes"]
779
+ publish(
780
+ self.record_export_path(roots[source], source, record["id"]), data
781
+ )
782
+ if source != "host":
783
+ publish(
784
+ roots["host"]
785
+ / "clients"
786
+ / source
787
+ / "records"
788
+ / (record["id"] + ".json"),
789
+ data,
790
+ )
791
+ (roots[source] / "compacted").mkdir(mode=0o700, exist_ok=True)
792
+ # Copy the exact old replay header and objects before any index publication.
793
+ # Resume builds from this immutable view even after one live header changes.
794
+ for source, directory in self.index_roots().items():
795
+ before = roots[source] / "replay-before"
796
+ before.mkdir(mode=0o700, exist_ok=True)
797
+ if plan["replay_before"][source] is None:
798
+ publish(
799
+ before / "manifest.json",
800
+ encode({"version": 1, "total": 0, "buckets": {}}),
801
+ )
802
+ elif not (before / "manifest.json").exists():
803
+ data = bounded(directory / "manifest.json")
804
+ if sha(data) != plan["replay_before"][source]:
805
+ raise ValueError("Original replay manifest changed before export")
806
+ header = self.worker_api["replay_manifest"](str(directory), True)
807
+ for bucket in header["buckets"].values():
808
+ publish(
809
+ before / "objects" / (bucket["sha256"] + ".json"),
810
+ bounded(
811
+ directory / "objects" / (bucket["sha256"] + ".json"),
812
+ 2 * MAX_RECORD_BYTES,
813
+ ),
814
+ )
815
+ publish(before / "manifest.json", data)
816
+ self.sync_export_roots(roots)
817
+
818
+ @staticmethod
819
+ def sync_export_roots(roots: dict[str, Path]) -> None:
820
+ for root in roots.values():
821
+ # Each file was fsynced before publication. Settle every directory
822
+ # binding, including archive ancestors, before active records move.
823
+ directories: list[Path] = []
824
+ for path, names, files in os.walk(root, followlinks=False):
825
+ directory = canonical(path)
826
+ directories.append(directory)
827
+ if len(names) + len(files) > 20000:
828
+ raise ValueError("Archive directory exceeds its inventory bound")
829
+ for directory in reversed(directories):
830
+ sync_directory(directory)
831
+ sync_directory(root.parent)
832
+ sync_directory(root.parent.parent)
833
+
834
+ def replay_after(
835
+ self, plan: dict[str, Any], digest: str
836
+ ) -> dict[str, tuple[bytes, dict[str, bytes]]]:
837
+ roots = self.archive_roots(digest)
838
+ additions: dict[str, dict[str, dict[str, Any]]] = {
839
+ source: {} for source in roots
840
+ }
841
+ host_ids = {
842
+ record["id"]: record
843
+ for record in plan["journals"]["host"]
844
+ if record["archive"]
845
+ }
846
+ for source, records in plan["journals"].items():
847
+ for record in records:
848
+ if not record["archive"]:
849
+ continue
850
+ pointer = {
851
+ "id": record["id"],
852
+ "archive": digest,
853
+ "sha256": record["sha256"],
854
+ "kind": "receipt" if source == "host" else "client",
855
+ }
856
+ keys = [self.worker_api["replay_key"](["operation", record["id"]])]
857
+ endpoint = record["endpoint"]
858
+ if record["tool_call_id"] is not None:
859
+ keys.append(
860
+ self.worker_api["replay_key"](
861
+ [
862
+ "call",
863
+ endpoint["host"],
864
+ endpoint["user"],
865
+ endpoint["port"],
866
+ record["owner_id"],
867
+ record["tool_call_id"],
868
+ ]
869
+ )
870
+ )
871
+ for key in keys:
872
+ previous = additions[source].get(key)
873
+ if previous is not None and previous != pointer:
874
+ raise ValueError(
875
+ "Conflicting original replay linkage; preserve both full records"
876
+ )
877
+ additions[source][key] = pointer
878
+ if source != "host" and record["id"] not in host_ids:
879
+ previous = additions["host"].get(key)
880
+ if previous is not None and previous != pointer:
881
+ raise ValueError(
882
+ "Ambiguous client-origin archive evidence; preserve both records"
883
+ )
884
+ additions["host"][key] = pointer
885
+ publish(
886
+ roots["host"] / "client-records" / (record["id"] + ".json"),
887
+ self.records[source + "/" + record["id"]]["bytes"],
888
+ )
889
+ after: dict[str, tuple[bytes, dict[str, bytes]]] = {}
890
+ aggregate_bytes = 0
891
+ for source, pending in additions.items():
892
+ directory = roots[source] / "replay-before"
893
+ header = self.worker_api["replay_manifest"](str(directory), True)
894
+ groups: dict[str, dict[str, Any]] = {}
895
+ for key, pointer in pending.items():
896
+ groups.setdefault(key[:3], {})[key] = pointer
897
+ buckets: dict[str, dict[str, Any]] = {}
898
+ objects: dict[str, bytes] = {}
899
+ total = 0
900
+ for prefix in sorted(set(header["buckets"]) | set(groups)):
901
+ entries = (
902
+ self.worker_api["replay_bucket"](
903
+ str(directory), prefix, header["buckets"][prefix]
904
+ )
905
+ if prefix in header["buckets"]
906
+ else {}
907
+ )
908
+ for key, pointer in groups.get(prefix, {}).items():
909
+ if key in entries and entries[key] != pointer:
910
+ raise ValueError(
911
+ "An active origin already appears in retired replay protection; preserve and inspect it"
912
+ )
913
+ entries[key] = pointer
914
+ data = encode({"version": 1, "entries": entries})
915
+ if len(entries) > 4096 or len(data) > 2 * MAX_RECORD_BYTES:
916
+ raise ValueError(
917
+ "Replay bucket is full; existing protection cannot be discarded"
918
+ )
919
+ total += len(entries)
920
+ aggregate_bytes += len(data)
921
+ if total > 1000000 or aggregate_bytes > 256 * MAX_RECORD_BYTES:
922
+ raise ValueError(
923
+ "Replay maintenance exceeds its bounded key or byte budget"
924
+ )
925
+ checksum = sha(data)
926
+ objects[checksum + ".json"] = data
927
+ buckets[prefix] = {"sha256": checksum, "count": len(entries)}
928
+ data = encode({"version": 1, "total": total, "buckets": buckets})
929
+ if len(data) > MAX_RECORD_BYTES:
930
+ raise ValueError("Replay manifest exceeds its 1 MiB bound")
931
+ after[source] = (data, objects)
932
+ for name, body in objects.items():
933
+ publish(roots[source] / "replay-after" / "objects" / name, body)
934
+ publish(roots[source] / "replay-after" / "manifest.json", data)
935
+ self.sync_export_roots(roots)
936
+ return after
937
+
938
+ def load_plan(self, digest: str) -> dict[str, Any]:
939
+ root = self.archive_roots(digest)["host"]
940
+ data = bounded(root / "plan.json", 16 * MAX_RECORD_BYTES)
941
+ if sha(data) != digest:
942
+ raise ValueError("Confirmed archive plan checksum changed")
943
+ plan = json.loads(data)
944
+ if (
945
+ not isinstance(plan, dict)
946
+ or plan.get("manifest_sha256") != sha(self.manifest_bytes)
947
+ or plan.get("worker_sha256") != sha(self.worker_bytes)
948
+ or plan.get("policy_sha256") != sha(self.policy_bytes)
949
+ or plan.get("operator_sha256")
950
+ != sha(bounded(Path(__file__), 8 * MAX_RECORD_BYTES))
951
+ or plan.get("bindings") != self.bindings
952
+ ):
953
+ raise ValueError(
954
+ "Original archive source, policy or namespace bindings changed"
955
+ )
956
+ return dict(plan)
957
+
958
+ def restore_snapshot(self, plan: dict[str, Any], digest: str) -> None:
959
+ self.records = {}
960
+ roots = self.archive_roots(digest)
961
+ sources = self.journal_roots()
962
+ for source, records in plan["journals"].items():
963
+ expected_names = {record["id"] + ".json" for record in records}
964
+ if set(os.listdir(sources[source])) - expected_names:
965
+ raise ValueError(
966
+ "Active journal acquired unreviewed records; preserve the maintenance fence"
967
+ )
968
+ for record in records:
969
+ exported = self.record_export_path(roots[source], source, record["id"])
970
+ active = sources[source] / (record["id"] + ".json")
971
+ captured = roots[source] / "compacted" / (record["id"] + ".json")
972
+ if exported.exists():
973
+ data = bounded(exported)
974
+ elif active.exists() and fingerprint(active) == record["identity"]:
975
+ data = bounded(active)
976
+ else:
977
+ raise ValueError(
978
+ "Original full archive export is missing; preserve remaining active evidence"
979
+ )
980
+ if sha(data) != record["sha256"]:
981
+ raise ValueError("Original archived record checksum changed")
982
+ if active.exists():
983
+ if (
984
+ fingerprint(active) != record["identity"]
985
+ or sha(bounded(active)) != record["sha256"]
986
+ ):
987
+ raise ValueError(
988
+ "Original active record changed; preserve the maintenance fence"
989
+ )
990
+ elif (
991
+ not record["archive"]
992
+ or not captured.exists()
993
+ or fingerprint(captured) != record["identity"]
994
+ or sha(bounded(captured)) != record["sha256"]
995
+ ):
996
+ raise ValueError(
997
+ "Original active record disappeared without its durable archive capture"
998
+ )
999
+ self.records[source + "/" + record["id"]] = {
1000
+ "path": active,
1001
+ "bytes": data,
1002
+ "value": json.loads(data),
1003
+ "record": record,
1004
+ }
1005
+
1006
+ def compact(self, plan: dict[str, Any], digest: str) -> None:
1007
+ roots = self.archive_roots(digest)
1008
+ for source, records in plan["journals"].items():
1009
+ with contextlib.ExitStack() as handles:
1010
+ journal = os.open(
1011
+ self.journal_roots()[source],
1012
+ os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
1013
+ )
1014
+ destination = os.open(
1015
+ roots[source] / "compacted",
1016
+ os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
1017
+ )
1018
+ handles.callback(os.close, journal)
1019
+ handles.callback(os.close, destination)
1020
+ for record in records:
1021
+ if not record["archive"]:
1022
+ continue
1023
+ name = record["id"] + ".json"
1024
+ path = roots[source] / "compacted" / name
1025
+ active = self.journal_roots()[source] / name
1026
+ if not os.path.lexists(path):
1027
+ self.worker_api["rename_without_replacement"](
1028
+ journal, name, destination, name
1029
+ )
1030
+ os.fsync(journal)
1031
+ os.fsync(destination)
1032
+ if (
1033
+ fingerprint(path) != record["identity"]
1034
+ or sha(bounded(path)) != record["sha256"]
1035
+ ):
1036
+ # Capture may have raced an external administrator. Keep
1037
+ # all bytes, restoring only into an unoccupied old name.
1038
+ with contextlib.suppress(OSError):
1039
+ self.worker_api["rename_without_replacement"](
1040
+ destination, name, journal, name
1041
+ )
1042
+ os.fsync(journal)
1043
+ os.fsync(destination)
1044
+ raise ValueError(
1045
+ "Archive capture changed; replacement evidence was retained for explicit recovery"
1046
+ )
1047
+ if os.path.lexists(active):
1048
+ raise ValueError(
1049
+ "An unreviewed active replacement exists; preserve both records"
1050
+ )
1051
+
1052
+ def clear_matching_fences(self, digest: str) -> None:
1053
+ expected = self.fence_bytes(digest)
1054
+ existing = [path for path in self.fences if os.path.lexists(path)]
1055
+ if any(bounded(path) != expected for path in existing):
1056
+ raise ValueError(
1057
+ "A different maintenance fence is present; preserve its authority"
1058
+ )
1059
+ for path in existing:
1060
+ path.unlink()
1061
+ sync_directory(path.parent)
1062
+
1063
+ def apply(self, digest: str) -> dict[str, Any]:
1064
+ if len(digest) != 64 or any(
1065
+ character not in "0123456789abcdef" for character in digest
1066
+ ):
1067
+ raise ValueError("Confirmation must be the exact archive preview digest")
1068
+ roots = self.archive_roots(digest)
1069
+ completed = roots["host"] / "complete.json"
1070
+ if completed.exists():
1071
+ plan = self.load_plan(digest)
1072
+ for source, records in plan["journals"].items():
1073
+ for record in records:
1074
+ if (
1075
+ sha(
1076
+ bounded(
1077
+ self.record_export_path(
1078
+ roots[source], source, record["id"]
1079
+ )
1080
+ )
1081
+ )
1082
+ != record["sha256"]
1083
+ ):
1084
+ raise ValueError("Completed archive evidence checksum changed")
1085
+ result = json.loads(bounded(completed))
1086
+ if result.get("digest") != digest or result.get("state") != "archived":
1087
+ raise ValueError("Completed archive receipt differs")
1088
+ self.clear_matching_fences(digest)
1089
+ return {**result, "historical": True}
1090
+ prepared = roots["host"] / "plan.json"
1091
+ if prepared.exists():
1092
+ plan = self.load_plan(digest)
1093
+ self.restore_snapshot(plan, digest)
1094
+ leases = self.control / "leases.json"
1095
+ lease_bytes = (
1096
+ bounded(leases, 16 * MAX_RECORD_BYTES) if leases.exists() else b""
1097
+ )
1098
+ if sha(lease_bytes) != plan["lease_sha256"]:
1099
+ raise ValueError(
1100
+ "Original lease state changed; inspect a fresh archive preview"
1101
+ )
1102
+ expected = self.fence_bytes(digest)
1103
+ if any(
1104
+ os.path.lexists(path) and bounded(path) != expected
1105
+ for path in self.fences
1106
+ ):
1107
+ raise ValueError(
1108
+ "A different maintenance fence is present; preserve its authority"
1109
+ )
1110
+ for path in self.fences:
1111
+ publish(path, expected)
1112
+ else:
1113
+ preview = self.preview()
1114
+ if preview["digest"] != digest:
1115
+ raise ValueError(
1116
+ "Confirmation changed; inspect the exact current preview"
1117
+ )
1118
+ if not preview["ready"]:
1119
+ raise ValueError(
1120
+ "Maintenance is blocked by retained or conflicting evidence"
1121
+ )
1122
+ plan = preview["plan"]
1123
+ # The full reviewed plan is durable before the first persistent fence.
1124
+ for root in roots.values():
1125
+ publish(root / "plan.json", encode(plan))
1126
+ self.sync_export_roots(roots)
1127
+ for path in self.fences:
1128
+ publish(path, self.fence_bytes(digest))
1129
+ self.require_fences(digest)
1130
+ self.export(plan, digest)
1131
+ after = self.replay_after(plan, digest)
1132
+ for source, directory in self.index_roots().items():
1133
+ expected_before = plan["replay_before"][source]
1134
+ live = directory / "manifest.json"
1135
+ if live.exists() and sha(bounded(live)) not in [
1136
+ expected_before,
1137
+ sha(after[source][0]),
1138
+ ]:
1139
+ raise ValueError(
1140
+ "Live replay manifest changed outside this transaction"
1141
+ )
1142
+ if not live.exists() and expected_before is not None:
1143
+ raise ValueError("Original live replay manifest disappeared")
1144
+ for name, body in after[source][1].items():
1145
+ publish(directory / "objects" / name, body)
1146
+ publish(live, after[source][0], replace=True)
1147
+ if source != "host":
1148
+ publish(
1149
+ Path(str(self.clients[int(source[7:])]) + ".replay-required.json"),
1150
+ encode({"version": 1}),
1151
+ replace=False,
1152
+ )
1153
+ for source, directory in self.index_roots().items():
1154
+ if sha(bounded(directory / "manifest.json")) != sha(after[source][0]):
1155
+ raise ValueError(
1156
+ "Replay publication is unresolved; preserve the maintenance fences"
1157
+ )
1158
+ self.restore_snapshot(plan, digest)
1159
+ self.compact(plan, digest)
1160
+ result = {
1161
+ "ok": True,
1162
+ "version": 1,
1163
+ "state": "archived",
1164
+ "digest": digest,
1165
+ "archived_counts": {
1166
+ source: sum(record["archive"] for record in records)
1167
+ for source, records in plan["journals"].items()
1168
+ },
1169
+ "historical": False,
1170
+ "target_effects": "none",
1171
+ "reclaimed_bytes": None,
1172
+ }
1173
+ for root in roots.values():
1174
+ publish(root / "complete.json", encode(result))
1175
+ self.sync_export_roots(roots)
1176
+ self.require_fences(digest)
1177
+ self.clear_matching_fences(digest)
1178
+ return result
1179
+
1180
+ @staticmethod
1181
+ def same_origin(first: dict[str, Any], second: dict[str, Any]) -> bool:
1182
+ return all(
1183
+ first[key] == second[key]
1184
+ for key in [
1185
+ "id",
1186
+ "owner_id",
1187
+ "tool_call_id",
1188
+ "endpoint",
1189
+ "resource_id",
1190
+ "operation",
1191
+ "target",
1192
+ "requested_path",
1193
+ ]
1194
+ )
1195
+
1196
+
1197
+ def main() -> None:
1198
+ parser = argparse.ArgumentParser(description=__doc__)
1199
+ parser.add_argument("operation", choices=["preview", "apply"])
1200
+ parser.add_argument("--manifest", required=True)
1201
+ parser.add_argument("--confirm")
1202
+ args = parser.parse_args()
1203
+ try:
1204
+ archive = JournalArchive(args.manifest)
1205
+ with archive.locked(args.confirm):
1206
+ if args.operation == "apply":
1207
+ if args.confirm is None:
1208
+ raise ValueError(
1209
+ "Inspect preview and confirm its exact digest before archival"
1210
+ )
1211
+ result = archive.apply(args.confirm)
1212
+ print(json.dumps(result))
1213
+ else:
1214
+ print(json.dumps({"ok": True, "preview": archive.preview()}))
1215
+ except (
1216
+ ValueError,
1217
+ OSError,
1218
+ KeyError,
1219
+ TypeError,
1220
+ subprocess.SubprocessError,
1221
+ ) as error:
1222
+ print(json.dumps({"ok": False, "error": str(error)}))
1223
+ sys.exit(2)
1224
+
1225
+
1226
+ if __name__ == "__main__":
1227
+ main()