@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,1009 @@
1
+ /** Fixed remote broker. Requests are JSON data; no model-provided shell runs. */
2
+ export const REMOTE_RESOURCE_SCRIPT = String.raw `
3
+ import contextlib, ctypes, errno, fcntl, hashlib, json, math, os, re, stat, sys, tempfile, time, uuid
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ admitted = False
7
+
8
+ def sync_directory(path: str) -> None:
9
+ descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
10
+ try: os.fsync(descriptor)
11
+ finally: os.close(descriptor)
12
+
13
+ def rename_without_replacement(source_directory: int, source: str, target_directory: int, target: str) -> None:
14
+ # The kernel must refuse an occupied destination. A stat/rename fallback
15
+ # would introduce another replacement race, including for directories.
16
+ library = ctypes.CDLL(None, use_errno=True)
17
+ if sys.platform == 'linux':
18
+ function, flag = library.renameat2, 1
19
+ elif sys.platform == 'darwin':
20
+ function, flag = library.renameatx_np, 4
21
+ else:
22
+ raise OSError(errno.ENOTSUP, 'Deletion requires atomic no-replace rename support')
23
+ function.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
24
+ function.restype = ctypes.c_int
25
+ if function(source_directory, os.fsencode(source), target_directory, os.fsencode(target), flag) != 0:
26
+ error = ctypes.get_errno()
27
+ raise OSError(error, os.strerror(error))
28
+
29
+ def decode_owner_presence(data: bytes) -> Dict[str, float]:
30
+ if len(data) > 16 * 1024 * 1024:
31
+ raise ValueError('Owner presence registry exceeds its bounded read size')
32
+ document = json.loads(data)
33
+ if not isinstance(document, dict) or len(document) > 4096:
34
+ raise ValueError('Invalid resource owner presence registry')
35
+ result: Dict[str, float] = {}
36
+ for name, seen in document.items():
37
+ if not isinstance(name, str) or not name or len(name) > 200 or type(seen) not in [int, float]:
38
+ raise ValueError('Invalid resource owner presence entry')
39
+ try: timestamp = float(seen)
40
+ except (ValueError, OverflowError) as error:
41
+ raise ValueError('Invalid resource owner presence timestamp') from error
42
+ if not math.isfinite(timestamp):
43
+ raise ValueError('Invalid resource owner presence timestamp')
44
+ result[name] = timestamp
45
+ return result
46
+
47
+ def register_owner_presence(control: str, owner: str, now: float) -> Dict[str, Any]:
48
+ # Recent presence is an advisory transfer prerequisite, never lease authority.
49
+ # A malformed cache is retained once and rebuilt solely from this request's
50
+ # immutable ID. Every other pane must register itself again before transfer.
51
+ owners_path = os.path.join(control, 'owners.json')
52
+ damaged_path = os.path.join(control, 'owners.json.damaged')
53
+ owners: Dict[str, float] = {}
54
+ recovered = False
55
+ try:
56
+ descriptor = os.open(owners_path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
57
+ except FileNotFoundError: descriptor = None
58
+ if descriptor is not None:
59
+ with os.fdopen(descriptor, 'rb') as stream:
60
+ opened = os.fstat(stream.fileno())
61
+ if not stat.S_ISREG(opened.st_mode):
62
+ raise ValueError('Owner presence registry must be a regular file; operator recovery required')
63
+ data = stream.read(16 * 1024 * 1024 + 1)
64
+ try: owners = decode_owner_presence(data)
65
+ except (ValueError, UnicodeDecodeError):
66
+ current = os.stat(owners_path, follow_symlinks=False)
67
+ if (opened.st_dev, opened.st_ino, opened.st_mtime_ns, opened.st_size) != (current.st_dev, current.st_ino, current.st_mtime_ns, current.st_size):
68
+ raise ValueError('Owner presence registry changed; inspect and retry registration')
69
+ os.fsync(stream.fileno())
70
+ directory = os.open(control, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
71
+ try:
72
+ try: rename_without_replacement(directory, 'owners.json', directory, 'owners.json.damaged')
73
+ except FileExistsError as error:
74
+ raise ValueError('A retained damaged owner registry already exists; operator recovery required') from error
75
+ os.fsync(directory)
76
+ finally: os.close(directory)
77
+ recovered = True
78
+ owners = {name: seen for name, seen in owners.items() if seen > now - 3600}
79
+ if owner not in owners and len(owners) >= 4096:
80
+ raise ValueError('Resource owner registry is full')
81
+ owners[owner] = now
82
+ fd, temporary = tempfile.mkstemp(dir=control)
83
+ published = False
84
+ try:
85
+ with os.fdopen(fd, 'w') as stream:
86
+ json.dump(owners, stream); stream.flush(); os.fsync(stream.fileno())
87
+ os.replace(temporary, owners_path)
88
+ published = True
89
+ sync_directory(control)
90
+ except OSError as error:
91
+ if published:
92
+ raise RuntimeError('Owner presence publication outcome is unresolved; inspect status after storage recovers before retrying: ' + str(error)) from error
93
+ raise
94
+ finally:
95
+ with contextlib.suppress(FileNotFoundError): os.unlink(temporary)
96
+ return {'registered_owner_id': owner, 'recovered_in_this_request': recovered,
97
+ 'retained_copy_entry_present': os.path.lexists(damaged_path), 'retained_copy_name': 'owners.json.damaged',
98
+ 'recent_owners': owners}
99
+
100
+ def read_archive_bytes(path: str, limit: int) -> bytes:
101
+ descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
102
+ with os.fdopen(descriptor, 'rb') as stream:
103
+ info = os.fstat(stream.fileno())
104
+ if not stat.S_ISREG(info.st_mode) or info.st_size > limit:
105
+ raise ValueError('Replay index or archived evidence exceeds its regular-file bound')
106
+ data = stream.read(limit + 1)
107
+ if len(data) > limit: raise ValueError('Replay index or archived evidence exceeds its byte bound')
108
+ return data
109
+
110
+ def replay_manifest(directory: str, required: bool = False) -> Optional[Dict[str, Any]]:
111
+ if not required and not os.path.lexists(directory): return None
112
+ try:
113
+ if not stat.S_ISDIR(os.lstat(directory).st_mode): raise ValueError('Replay index must be a real directory')
114
+ value = json.loads(read_archive_bytes(os.path.join(directory, 'manifest.json'), 1048576))
115
+ if (not isinstance(value, dict) or set(value) != {'version', 'total', 'buckets'}
116
+ or type(value['version']) is not int or value['version'] != 1
117
+ or type(value['total']) is not int or not 0 <= value['total'] <= 1000000
118
+ or not isinstance(value['buckets'], dict) or len(value['buckets']) > 4096):
119
+ raise ValueError('Invalid bounded replay index manifest')
120
+ count = 0
121
+ for prefix, bucket in value['buckets'].items():
122
+ if (not isinstance(prefix, str) or re.fullmatch(r'[a-f0-9]{3}', prefix) is None
123
+ or not isinstance(bucket, dict) or set(bucket) != {'sha256', 'count'}
124
+ or not isinstance(bucket['sha256'], str) or re.fullmatch(r'[a-f0-9]{64}', bucket['sha256']) is None
125
+ or type(bucket['count']) is not int or not 1 <= bucket['count'] <= 4096):
126
+ raise ValueError('Invalid replay index manifest bucket')
127
+ count += bucket['count']
128
+ if count != value['total']: raise ValueError('Replay index manifest count changed')
129
+ return dict(value)
130
+ except (ValueError, OSError, UnicodeDecodeError) as error:
131
+ raise ValueError('Required replay index manifest is unavailable or invalid') from error
132
+
133
+ def replay_key(parts: List[Any]) -> str:
134
+ return hashlib.sha256(json.dumps(parts, ensure_ascii=False, separators=(',', ':')).encode('utf-8', errors='backslashreplace')).hexdigest()
135
+
136
+ def replay_bucket(directory: str, prefix: str, bucket: Dict[str, Any]) -> Dict[str, Any]:
137
+ try:
138
+ objects = os.path.join(directory, 'objects')
139
+ if not stat.S_ISDIR(os.lstat(objects).st_mode): raise ValueError('Replay objects must be a real directory')
140
+ data = read_archive_bytes(os.path.join(objects, bucket['sha256'] + '.json'), 2097152)
141
+ if hashlib.sha256(data).hexdigest() != bucket['sha256']: raise ValueError('Replay bucket checksum changed')
142
+ value = json.loads(data)
143
+ if (not isinstance(value, dict) or set(value) != {'version', 'entries'} or type(value['version']) is not int
144
+ or value['version'] != 1 or not isinstance(value['entries'], dict) or len(value['entries']) != bucket['count']):
145
+ raise ValueError('Invalid replay index bucket')
146
+ for key, pointer in value['entries'].items():
147
+ if (not isinstance(key, str) or re.fullmatch(r'[a-f0-9]{64}', key) is None or not key.startswith(prefix)
148
+ or not isinstance(pointer, dict) or set(pointer) != {'id', 'archive', 'sha256', 'kind'}
149
+ or not isinstance(pointer['id'], str) or str(uuid.UUID(pointer['id'])) != pointer['id']
150
+ or not isinstance(pointer['archive'], str) or re.fullmatch(r'[a-f0-9]{64}', pointer['archive']) is None
151
+ or not isinstance(pointer['sha256'], str) or re.fullmatch(r'[a-f0-9]{64}', pointer['sha256']) is None
152
+ or pointer['kind'] not in ['client', 'receipt']):
153
+ raise ValueError('Invalid replay index pointer')
154
+ return dict(value['entries'])
155
+ except (ValueError, OSError, UnicodeDecodeError) as error:
156
+ raise ValueError('Required replay index bucket is unavailable or invalid') from error
157
+
158
+ def replay_lookup(directory: str, parts: List[Any], required: bool = False) -> Optional[Dict[str, Any]]:
159
+ header = replay_manifest(directory, required)
160
+ if header is None: return None
161
+ key = replay_key(parts)
162
+ bucket = header['buckets'].get(key[:3])
163
+ if bucket is None: return None
164
+ pointer = replay_bucket(directory, key[:3], bucket).get(key)
165
+ return dict(pointer) if pointer is not None else None
166
+
167
+ def archived_receipt(control: str, pointer: Dict[str, Any]) -> Dict[str, Any]:
168
+ if pointer['kind'] != 'receipt': raise ValueError('Operation archived without a remote receipt; inspect its original client export')
169
+ directory = os.path.join(control, 'receipt-archives')
170
+ for path in [directory, os.path.join(directory, pointer['archive']), os.path.join(directory, pointer['archive'], 'receipts')]:
171
+ if not stat.S_ISDIR(os.lstat(path).st_mode): raise ValueError('Archived receipt directory changed')
172
+ data = read_archive_bytes(os.path.join(directory, pointer['archive'], 'receipts', pointer['id'] + '.json'), 1048576)
173
+ if hashlib.sha256(data).hexdigest() != pointer['sha256']: raise ValueError('Archived receipt checksum changed')
174
+ value = json.loads(data)
175
+ if not isinstance(value, dict) or value.get('id') != pointer['id'] or value.get('version') != 1:
176
+ raise ValueError('Invalid archived receipt identity')
177
+ return dict(value)
178
+
179
+ def runtime_private_directory(path: str) -> None:
180
+ os.makedirs(path, mode=0o700, exist_ok=True)
181
+ info = os.lstat(path)
182
+ if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.geteuid() or info.st_mode & 0o077:
183
+ raise ValueError('Runtime archive requires a private real directory')
184
+ sync_directory(os.path.dirname(path))
185
+
186
+ def publish_runtime_file(path: str, data: bytes, replace: bool = False) -> None:
187
+ if not replace and os.path.lexists(path):
188
+ if read_archive_bytes(path, 2097152) != data: raise ValueError('Immutable runtime archive changed')
189
+ return
190
+ fd, temporary = tempfile.mkstemp(dir=os.path.dirname(path))
191
+ try:
192
+ with os.fdopen(fd, 'wb') as stream:
193
+ stream.write(data); stream.flush(); os.fsync(stream.fileno())
194
+ if replace: os.replace(temporary, path)
195
+ else: os.link(temporary, path)
196
+ sync_directory(os.path.dirname(path))
197
+ finally:
198
+ with contextlib.suppress(FileNotFoundError): os.unlink(temporary)
199
+
200
+ def retain_terminal_receipts(control: str, selected: List[Dict[str, Any]]) -> None:
201
+ # The caller holds the lease lock and selects only acknowledged terminal
202
+ # records without retained staging. Never discard full historical evidence.
203
+ if not selected: return
204
+ receipts = os.path.join(control, 'receipts')
205
+ originals = []
206
+ byte_count = 0
207
+ for receipt in selected:
208
+ data = read_archive_bytes(os.path.join(receipts, receipt['id'] + '.json'), 1048576)
209
+ byte_count += len(data)
210
+ if byte_count > 128 * 1048576: raise ValueError('Runtime archive exceeds its bounded batch byte budget')
211
+ originals.append((receipt, data, hashlib.sha256(data).hexdigest()))
212
+ encoded = lambda value: json.dumps(value, ensure_ascii=False, separators=(',', ':')).encode('utf-8')
213
+ batch = hashlib.sha256(encoded([[receipt['id'], checksum] for receipt, data, checksum in originals])).hexdigest()
214
+ directory = os.path.join(control, 'replay-index')
215
+ header = replay_manifest(directory) or {'version': 1, 'total': 0, 'buckets': {}}
216
+ runtime_private_directory(directory)
217
+ runtime_private_directory(os.path.join(directory, 'objects'))
218
+ manifest_path = os.path.join(directory, 'manifest.json')
219
+ if not os.path.lexists(manifest_path): publish_runtime_file(manifest_path, encoded(header))
220
+ archive_root = os.path.join(control, 'receipt-archives')
221
+ archive = os.path.join(archive_root, batch, 'receipts')
222
+ for path in [archive_root, os.path.dirname(archive), archive]: runtime_private_directory(path)
223
+ additions: Dict[str, Dict[str, Any]] = {}
224
+ for receipt, data, checksum in originals:
225
+ publish_runtime_file(os.path.join(archive, receipt['id'] + '.json'), data)
226
+ pointer = {'id': receipt['id'], 'archive': batch, 'sha256': checksum, 'kind': 'receipt'}
227
+ keys: List[List[Any]] = [['operation', receipt['id']]]
228
+ if receipt.get('tool_call_id') is not None:
229
+ endpoint = receipt['endpoint']
230
+ keys.append(['call', endpoint['host'], endpoint['user'], endpoint['port'], receipt['owner_id'], receipt['tool_call_id']])
231
+ for parts in keys:
232
+ key = replay_key(parts)
233
+ prefix = key[:3]
234
+ if prefix not in additions:
235
+ additions[prefix] = replay_bucket(directory, prefix, header['buckets'][prefix]) if prefix in header['buckets'] else {}
236
+ previous = additions[prefix].get(key)
237
+ if previous is not None and (previous['id'] != pointer['id'] or previous['sha256'] != checksum or previous['kind'] != 'receipt'):
238
+ raise ValueError('Conflicting original runtime archive linkage; preserve active receipts')
239
+ if previous is None: additions[prefix][key] = pointer
240
+ for prefix, entries in additions.items():
241
+ data = encoded({'version': 1, 'entries': entries})
242
+ if len(entries) > 4096 or len(data) > 2097152:
243
+ raise ValueError('Replay bucket is full; existing protection cannot be discarded')
244
+ header['total'] += len(entries) - header['buckets'].get(prefix, {}).get('count', 0)
245
+ checksum = hashlib.sha256(data).hexdigest()
246
+ publish_runtime_file(os.path.join(directory, 'objects', checksum + '.json'), data)
247
+ header['buckets'][prefix] = {'sha256': checksum, 'count': len(entries)}
248
+ data = encoded(header)
249
+ if header['total'] > 1000000 or len(data) > 1048576:
250
+ raise ValueError('Runtime replay index exceeds its bounded key budget')
251
+ publish_runtime_file(manifest_path, data, replace=True)
252
+ for receipt, data, checksum in originals:
253
+ path = os.path.join(receipts, receipt['id'] + '.json')
254
+ if read_archive_bytes(path, 1048576) != data: raise ValueError('Active runtime receipt changed before retirement')
255
+ os.unlink(path)
256
+ sync_directory(receipts)
257
+
258
+ def main(q: Dict[str, Any]) -> Dict[str, Any]:
259
+ global admitted
260
+ root = os.path.realpath(q['root'])
261
+ if not os.path.isabs(q['root']) or not os.path.isdir(root):
262
+ raise ValueError('Resource root must be an existing absolute directory')
263
+ owner = q['owner_id']
264
+ if not isinstance(owner, str) or not owner or len(owner) > 200:
265
+ raise ValueError('Invalid immutable owner ID')
266
+ control = os.path.realpath(os.path.expanduser('~/.ztc/resource-leases'))
267
+ os.makedirs(control, mode=0o700, exist_ok=True)
268
+ state_path = os.path.join(control, 'leases.json')
269
+ client_journal = q.get('client_journal')
270
+ if client_journal is not None and (not isinstance(client_journal, str) or not os.path.isabs(client_journal)):
271
+ raise ValueError('Invalid host-bound client journal')
272
+ staging = q.get('host_delete_staging', os.path.join(control, 'deletion-staging'))
273
+ if not isinstance(staging, str) or not os.path.isabs(staging) or os.path.normpath(staging) != staging:
274
+ raise ValueError('Invalid host-bound deletion staging directory')
275
+ write_staging = q.get('host_write_staging', staging)
276
+ if not isinstance(write_staging, str) or not os.path.isabs(write_staging) or os.path.normpath(write_staging) != write_staging:
277
+ raise ValueError('Invalid host-bound write staging directory')
278
+ staging_bindings: Dict[str, Any] = {staging: q.get('host_delete_staging_identity')}
279
+ host_staging_bindings = q.get('host_write_staging_directories')
280
+ if host_staging_bindings is not None:
281
+ if not isinstance(host_staging_bindings, dict) or len(host_staging_bindings) > 201:
282
+ raise ValueError('Invalid host-bound write staging inventory')
283
+ for stage_path, stage_identity in host_staging_bindings.items():
284
+ if (not isinstance(stage_path, str) or not os.path.isabs(stage_path) or os.path.normpath(stage_path) != stage_path
285
+ or not isinstance(stage_identity, list) or len(stage_identity) != 2
286
+ or any(type(item) is not int or item < 0 for item in stage_identity)):
287
+ raise ValueError('Invalid host-bound write staging identity')
288
+ if (staging not in host_staging_bindings or write_staging not in host_staging_bindings
289
+ or host_staging_bindings[staging] != q.get('host_delete_staging_identity')
290
+ or host_staging_bindings[write_staging] != q.get('host_write_staging_identity')):
291
+ raise ValueError('Host write staging selection lacks its pinned identity')
292
+ staging_bindings.update(host_staging_bindings)
293
+ elif write_staging != staging:
294
+ raise ValueError('Configured write staging requires a pinned host inventory')
295
+ protected = [control, *staging_bindings] + ([os.path.realpath(client_journal + suffix)
296
+ for suffix in ['', '.archives', '.replay', '.replay-required.json', '.admission-lock', '.maintenance.json']] if client_journal else [])
297
+ def protected_target(target: str) -> bool:
298
+ return any(os.path.commonpath([path, target]) in [path, target] for path in protected)
299
+ key = hashlib.sha256(root.encode()).hexdigest()
300
+ def identity(path: str) -> List[int]:
301
+ s = os.stat(path)
302
+ return [s.st_dev, s.st_ino]
303
+ def ancestors(path: str) -> List[List[int]]:
304
+ result = []
305
+ while True:
306
+ result.append(identity(path))
307
+ parent = os.path.dirname(path)
308
+ if parent == path: return result
309
+ path = parent
310
+ root_id, root_ancestors = identity(root), ancestors(root)
311
+ if q.get('host_root_identity') is not None and q['host_root_identity'] != root_id:
312
+ raise ValueError('Host broker resource identity changed')
313
+ host_region: Optional[Dict[str, Any]] = q.get('host_filesystem_region')
314
+ mountinfo_sha256 = q.get('host_mountinfo_sha256')
315
+ def valid_region(value: Any) -> bool:
316
+ return (isinstance(value, dict) and set(value) == {'version', 'device', 'path'}
317
+ and type(value['version']) is int and value['version'] == 1
318
+ and isinstance(value['device'], str) and re.fullmatch(r'[0-9]+:[0-9]+', value['device']) is not None
319
+ and isinstance(value['path'], str) and len(value['path']) <= 4096 and '\x00' not in value['path']
320
+ and os.path.isabs(value['path']) and os.path.normpath(value['path']) == value['path'])
321
+ def proven_region(record: Dict[str, Any]) -> bool:
322
+ return (record.get('mountinfo_sha256') == mountinfo_sha256
323
+ and valid_region(record.get('filesystem_region')))
324
+ def overlaps(record: Dict[str, Any]) -> bool:
325
+ if host_region is not None:
326
+ # Missing or stale evidence cannot establish that two roots are disjoint.
327
+ if not proven_region(record): return True
328
+ other = record['filesystem_region']
329
+ if (other['device'] == host_region['device']
330
+ and os.path.commonpath([other['path'], host_region['path']]) in [other['path'], host_region['path']]):
331
+ return True
332
+ return record['identity'] in root_ancestors or root_id in record['ancestors']
333
+ with open(os.path.join(control, 'lock'), 'a') as lock:
334
+ fcntl.flock(lock, fcntl.LOCK_EX)
335
+ # Operator cutover leaves the old authority frozen across restarts.
336
+ # Any entry (including damaged evidence) requires explicit recovery;
337
+ # a request cannot supply an override or silently clear this fence.
338
+ if os.path.lexists(os.path.join(control, 'cutover-fence.json')):
339
+ raise ValueError('Resource cutover authority is frozen for operator review')
340
+ if os.path.lexists(os.path.join(control, 'journal-maintenance.json')):
341
+ raise ValueError('Resource journal maintenance is frozen; inspect and resume the confirmed archive')
342
+ replay_directory = os.path.join(control, 'replay-index')
343
+ replay_required = q.get('host_receipt_archive_index') is True or os.path.lexists(os.path.join(control, 'receipt-archives'))
344
+ if q['operation'] in ['claim', 'transfer', 'stop', 'write', 'delete', 'ack', 'resolve']:
345
+ replay_manifest(replay_directory, replay_required)
346
+ if q['operation'] in ['write', 'delete']:
347
+ retired_id = q.get('operation_id')
348
+ pointer = replay_lookup(replay_directory, ['operation', retired_id], replay_required)
349
+ call = q.get('tool_call_id')
350
+ if pointer is None and q['operation'] in ['write', 'delete'] and call is not None:
351
+ endpoint = q['endpoint']
352
+ pointer = replay_lookup(replay_directory, ['call', endpoint['host'], endpoint['user'], endpoint['port'], owner, call], replay_required)
353
+ if pointer is not None:
354
+ raise ValueError('Operation archived as ' + pointer['id'] + '; inspect original evidence instead of replaying')
355
+ # Settle an earlier unacknowledged rename before using its lease. Sync
356
+ # every actual ancestor so newly created control paths (including a
357
+ # previous interrupted initialization) survive directory-entry loss.
358
+ control_ancestor = control
359
+ while True:
360
+ sync_directory(control_ancestor)
361
+ parent_directory = os.path.dirname(control_ancestor)
362
+ if parent_directory == control_ancestor: break
363
+ control_ancestor = parent_directory
364
+ if host_region is not None or mountinfo_sha256 is not None:
365
+ if (not valid_region(host_region) or not isinstance(mountinfo_sha256, str)
366
+ or re.fullmatch(r'[a-f0-9]{64}', mountinfo_sha256) is None):
367
+ raise ValueError('Invalid host mount evidence')
368
+ host_roots = q.get('host_resource_roots')
369
+ if not isinstance(host_roots, dict) or q['root'] not in host_roots:
370
+ raise ValueError('Invalid host resource root evidence')
371
+ if root != q['root'] or root_id != host_roots[q['root']]:
372
+ raise ValueError('Host broker resource canonical path or identity changed before locking')
373
+ # Check every configured root: moving another owner's root can
374
+ # change overlap even when the requested root itself is unchanged.
375
+ for configured_root, configured_identity in host_roots.items():
376
+ if (not isinstance(configured_root, str) or not os.path.isabs(configured_root)
377
+ or os.path.realpath(configured_root) != configured_root
378
+ or not os.path.isdir(configured_root) or identity(configured_root) != configured_identity):
379
+ raise ValueError('Host broker resource canonical path or identity changed')
380
+ with open('/proc/self/mountinfo', 'rb') as mount_stream:
381
+ mount_snapshot = mount_stream.read(8 * 1024 * 1024 + 1)
382
+ if (len(mount_snapshot) > 8 * 1024 * 1024
383
+ or hashlib.sha256(mount_snapshot).hexdigest() != mountinfo_sha256):
384
+ raise ValueError('Host mount topology changed before lease admission')
385
+ records: Dict[str, Dict[str, Any]]
386
+ record: Optional[Dict[str, Any]]
387
+ try:
388
+ with open(state_path) as lease_stream: records = json.load(lease_stream)
389
+ except FileNotFoundError: records = {}
390
+ if not isinstance(records, dict): raise ValueError('Invalid resource lease registry')
391
+ for record in records.values():
392
+ if (not isinstance(record, dict) or not isinstance(record.get('generation'), int)
393
+ or not isinstance(record.get('expires_at'), (int, float))
394
+ or not math.isfinite(record['expires_at']) or not isinstance(record.get('root'), str)
395
+ or not isinstance(record.get('identity'), list) or not isinstance(record.get('ancestors'), list)
396
+ or record.get('owner_id') is not None and not isinstance(record['owner_id'], str)):
397
+ raise ValueError('Invalid resource lease registry')
398
+ now = time.time()
399
+ presence = register_owner_presence(control, owner, now)
400
+ owners = presence.pop('recent_owners')
401
+ record = records.get(key)
402
+ if record is not None and record['identity'] != root_id:
403
+ raise ValueError('Resource identity changed; configure and confirm the replacement resource')
404
+ op = q['operation']
405
+ def active(record: Dict[str, Any]) -> bool:
406
+ return bool(record['owner_id'] is not None and record['expires_at'] > now)
407
+ conflicts = [r for r in records.values() if overlaps(r) and active(r) and r['owner_id'] != owner]
408
+ status = {'root': root, 'owner_id': record['owner_id'] if record and active(record) else None,
409
+ 'generation': record['generation'] if record else 0,
410
+ 'expires_at': record['expires_at'] if record else None,
411
+ 'authorized': bool(record and active(record) and record['owner_id'] == owner and not conflicts),
412
+ 'conflicts': [{'root': r['root'], 'owner_id': r['owner_id']} for r in conflicts],
413
+ 'owner_presence': presence}
414
+ receipts = os.path.join(control, 'receipts')
415
+ def persist_receipt(receipt: Dict[str, Any]) -> None:
416
+ os.makedirs(receipts, mode=0o700, exist_ok=True)
417
+ directory = os.open(control, os.O_RDONLY | os.O_DIRECTORY)
418
+ try: os.fsync(directory)
419
+ finally: os.close(directory)
420
+ fd, temporary = tempfile.mkstemp(dir=receipts)
421
+ try:
422
+ with os.fdopen(fd, 'w') as f:
423
+ json.dump(receipt, f); f.flush(); os.fsync(f.fileno())
424
+ os.replace(temporary, os.path.join(receipts, receipt['id'] + '.json'))
425
+ directory = os.open(receipts, os.O_RDONLY | os.O_DIRECTORY)
426
+ try: os.fsync(directory)
427
+ finally: os.close(directory)
428
+ finally:
429
+ with contextlib.suppress(FileNotFoundError): os.unlink(temporary)
430
+ def journal() -> List[Dict[str, Any]]:
431
+ try: names = os.listdir(receipts)
432
+ except FileNotFoundError: return []
433
+ if len(names) > 10000: raise ValueError('Receipt journal is full; confirmed maintenance is required')
434
+ result = []
435
+ for name in names:
436
+ if not name.endswith('.json') or str(uuid.UUID(name[:-5])) != name[:-5]:
437
+ raise ValueError('Invalid receipt journal entry; recovery is required')
438
+ with open(os.path.join(receipts, name)) as f: receipt = json.load(f)
439
+ if (receipt.get('version') != 1 or receipt.get('id') + '.json' != name
440
+ or receipt.get('operation') not in ['delete', 'write']
441
+ or not isinstance(receipt.get('target'), dict) or not isinstance(receipt['target'].get('path'), str)
442
+ or not os.path.isabs(receipt['target']['path'])
443
+ or receipt.get('status') not in ['pending', 'verified', 'failed', 'unverified', 'dry_run', 'cancelled']
444
+ or 'admission_resolved' in receipt and not isinstance(receipt['admission_resolved'], bool)):
445
+ raise ValueError('Invalid receipt journal record; recovery is required')
446
+ result.append(receipt)
447
+ return result
448
+ def inspect_staging(receipt: Dict[str, Any], sample: bool = False,
449
+ operation: str = 'deletion', leaf: str = 'target') -> Optional[Dict[str, Any]]:
450
+ stage = receipt.get(operation + '_staging')
451
+ if stage is None: return None
452
+ receipt_id = receipt['id']
453
+ path_key = 'payload_path' if leaf == 'payload' else 'path'
454
+ if not isinstance(stage, dict) or not isinstance(stage.get('directory_identity'), list):
455
+ raise ValueError('Invalid ' + operation + ' staging receipt; operator recovery is required')
456
+ stage_choices = [path for path in (staging_bindings if operation == 'write' else [staging])
457
+ if stage.get(path_key) == os.path.join(path, receipt_id, leaf)]
458
+ if len(stage_choices) != 1:
459
+ raise ValueError('Invalid ' + operation + ' staging receipt; operator recovery is required')
460
+ receipt_staging = stage_choices[0]
461
+ expected_staging_identity = staging_bindings[receipt_staging]
462
+ retained: Dict[str, Any] = {'path': stage[path_key], 'recorded_state': stage['state'], 'exists': False}
463
+ with contextlib.ExitStack() as stage_handles:
464
+ stage_root = os.open(receipt_staging, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
465
+ stage_handles.callback(os.close, stage_root)
466
+ private = os.fstat(stage_root)
467
+ if (private.st_uid != os.geteuid() or private.st_mode & 0o077
468
+ or expected_staging_identity is not None
469
+ and expected_staging_identity != [private.st_dev, private.st_ino]):
470
+ raise ValueError('Host ' + operation + ' staging changed during reconciliation')
471
+ try:
472
+ stage_directory = os.open(receipt_id, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=stage_root)
473
+ stage_handles.callback(os.close, stage_directory)
474
+ private_operation = os.fstat(stage_directory)
475
+ if ([private_operation.st_dev, private_operation.st_ino] != stage['directory_identity']
476
+ or private_operation.st_uid != os.geteuid() or private_operation.st_mode & 0o077):
477
+ raise ValueError(operation.capitalize() + ' staging operation identity changed')
478
+ info = os.stat(leaf, dir_fd=stage_directory, follow_symlinks=False)
479
+ retained.update(exists=True, device=info.st_dev, inode=info.st_ino,
480
+ mode=info.st_mode, size=info.st_size, mtime_ns=info.st_mtime_ns, ctime_ns=info.st_ctime_ns)
481
+ if sample and stat.S_ISREG(info.st_mode):
482
+ fd = os.open(leaf, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=stage_directory)
483
+ with os.fdopen(fd, 'rb') as retained_stream:
484
+ opened = os.fstat(retained_stream.fileno())
485
+ if [opened.st_dev, opened.st_ino] != [info.st_dev, info.st_ino]:
486
+ raise ValueError('Retained ' + operation + ' target changed during reconciliation')
487
+ retained['sample_sha256'] = hashlib.sha256(retained_stream.read(1048576)).hexdigest()
488
+ elif stat.S_ISLNK(info.st_mode):
489
+ retained['link_target'] = os.readlink(leaf, dir_fd=stage_directory)
490
+ except FileNotFoundError: pass
491
+ return retained
492
+ def retain_receipts(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
493
+ if len(entries) < 512: return entries
494
+ selected = []
495
+ for receipt in sorted(entries, key=lambda value: (value.get('completed_at') or 0, value['id'])):
496
+ if len(selected) >= 64: break
497
+ if receipt.get('admission_resolved') is not True or receipt['status'] not in ['verified', 'failed', 'dry_run', 'cancelled']:
498
+ continue
499
+ verification = receipt.get('verification')
500
+ if receipt['status'] == 'verified' and (not isinstance(verification, dict)
501
+ or verification.get('target_matches') is not True and verification.get('target_absent') is not True):
502
+ continue
503
+ resolution = receipt.get('resolution')
504
+ if receipt['status'] == 'cancelled' and (not isinstance(resolution, dict) or resolution.get('decision') != 'accept_current_state'):
505
+ continue
506
+ if any(retained is not None and retained['exists'] for retained in [
507
+ inspect_staging(receipt), inspect_staging(receipt, operation='write'),
508
+ inspect_staging(receipt, operation='write', leaf='payload')]):
509
+ continue
510
+ selected.append(receipt)
511
+ retain_terminal_receipts(control, selected)
512
+ selected_ids = {receipt['id'] for receipt in selected}
513
+ return [receipt for receipt in entries if receipt['id'] not in selected_ids]
514
+ if op == 'status':
515
+ try:
516
+ pending = [r for r in journal() if not r.get('admission_resolved', r['status'] in ['verified', 'failed', 'dry_run'])
517
+ and (r['target']['path'] == root or r['target']['path'].startswith(root + os.sep) or root.startswith(r['target']['path'] + os.sep))]
518
+ status['unresolved_operations'] = [{'id': r['id'], 'operation': r['operation'], 'status': r['status'],
519
+ 'target': r['target']['path'], 'owner_id': r['owner_id']} for r in pending[:100]]
520
+ status['unresolved_operation_count'] = len(pending)
521
+ except (ValueError, OSError, TypeError, KeyError) as error:
522
+ status['mutation_journal_error'] = str(error)[:2000]
523
+ return status
524
+ def save() -> None:
525
+ global admitted
526
+ fd, path = tempfile.mkstemp(dir=control)
527
+ try:
528
+ with os.fdopen(fd, 'w') as f:
529
+ json.dump(records, f); f.flush(); os.fsync(f.fileno())
530
+ # Replacement can take effect even when its response is lost.
531
+ # Never describe a post-dispatch publication error as refusal.
532
+ admitted = True
533
+ os.replace(path, state_path)
534
+ sync_directory(control)
535
+ except OSError as error:
536
+ if admitted:
537
+ raise RuntimeError('Lease publication outcome is unresolved; inspect current owner and generation before retrying: ' + str(error)) from error
538
+ raise
539
+ finally:
540
+ with contextlib.suppress(FileNotFoundError): os.unlink(path)
541
+ if op in ['claim', 'transfer', 'stop']:
542
+ expected = q['expected_generation']
543
+ if not isinstance(expected, int) or isinstance(expected, bool) or expected != status['generation']:
544
+ raise ValueError('Lease changed; inspect current owner and retry the confirmed action')
545
+ own_active_record = bool(record and active(record) and record['owner_id'] == owner)
546
+ if (op == 'stop' and not own_active_record) or (op == 'transfer' and not status['authorized']):
547
+ raise ValueError('Only the recorded current owner may stop or transfer this lease')
548
+ if op == 'claim' and conflicts:
549
+ if host_region is not None and any(not proven_region(other) for other in conflicts):
550
+ raise ValueError('Resource overlap requires recovery: confirmed owners must stop or renew unproven leases')
551
+ raise ValueError('Resource is owned by another agent: ' + json.dumps(status['conflicts']))
552
+ ttl = q.get('ttl_seconds', 900)
553
+ if not isinstance(ttl, int) or isinstance(ttl, bool) or ttl < 1 or ttl > 3600:
554
+ raise ValueError('Lease duration must be 1 to 3600 seconds')
555
+ target = q.get('target_owner_id') if op == 'transfer' else owner
556
+ if op == 'transfer' and (not isinstance(target, str) or not target or len(target) > 200 or target == owner):
557
+ raise ValueError('Transfer requires a different host-assigned owner label')
558
+ if op == 'transfer' and target not in owners:
559
+ raise ValueError('Target owner must be registered by its own recent host broker request; inspect status in the target pane')
560
+ if op == 'transfer' and any(other is not record and active(other) and overlaps(other) and other['owner_id'] != target for other in records.values()):
561
+ raise ValueError('Stop overlapping leases before transferring this resource')
562
+ records[key] = {'root': root, 'identity': root_id, 'ancestors': root_ancestors,
563
+ 'owner_id': None if op == 'stop' else target, 'expires_at': 0 if op == 'stop' else now + ttl,
564
+ 'generation': status['generation'] + 1,
565
+ 'filesystem_region': host_region, 'mountinfo_sha256': mountinfo_sha256}
566
+ save()
567
+ return records[key]
568
+ if record is None or not status['authorized']:
569
+ raise ValueError('Resource lease required; current owner: ' + str(status['owner_id']) + '; conflicts: ' + json.dumps(status['conflicts']))
570
+ if op in ['receipt', 'ack', 'reconcile', 'resolve']:
571
+ receipt_id = q.get('receipt_id')
572
+ if not isinstance(receipt_id, str): raise ValueError('Receipt ID must be a canonical operation UUID')
573
+ try: parsed_id = str(uuid.UUID(receipt_id))
574
+ except ValueError: raise ValueError('Receipt ID must be a canonical operation UUID')
575
+ if parsed_id != receipt_id: raise ValueError('Receipt ID must be a canonical operation UUID')
576
+ receipt_archive: Optional[Dict[str, Any]] = None
577
+ try:
578
+ with open(os.path.join(receipts, receipt_id + '.json')) as receipt_stream: receipt = json.load(receipt_stream)
579
+ except FileNotFoundError:
580
+ pointer = replay_lookup(replay_directory, ['operation', receipt_id], replay_required)
581
+ if pointer is not None:
582
+ receipt = archived_receipt(control, pointer)
583
+ if receipt.get('admission_resolved') is not True:
584
+ raise ValueError('Archived operation lacks its resolved admission; operator recovery required')
585
+ receipt_archive = {'historical': True, 'digest': pointer['archive'], 'record_sha256': pointer['sha256']}
586
+ else:
587
+ if op not in ['reconcile', 'resolve']: raise
588
+ receipt = None
589
+ if receipt is not None and receipt['root_identity'] != root_id:
590
+ raise ValueError('Receipt does not belong to this physical resource')
591
+ if op in ['receipt', 'ack']:
592
+ if receipt['owner_id'] != owner or receipt['resource_id'] != q['resource_id']:
593
+ raise ValueError('Receipt does not belong to this owner and resource')
594
+ if op == 'ack':
595
+ if receipt['status'] not in ['verified', 'failed', 'dry_run']:
596
+ raise ValueError('Unresolved outcome requires confirmed reconciliation')
597
+ if receipt_archive is None:
598
+ receipt['admission_resolved'] = True
599
+ persist_receipt(receipt)
600
+ return {'receipt': receipt, **({'archive': receipt_archive} if receipt_archive is not None else {})}
601
+ path = receipt['requested_path'] if receipt else q.get('path')
602
+ if not isinstance(path, str) or not path or os.path.isabs(path) or '\x00' in path:
603
+ raise ValueError('Reconciliation requires the original relative target path')
604
+ target = os.path.normpath(os.path.join(root, path))
605
+ if target == root or os.path.commonpath([root, target]) != root or protected_target(target):
606
+ raise ValueError('Reconciliation target escapes the resource or its protected boundary')
607
+ # Inspect held, no-follow descriptors. Never probe through a substituted parent.
608
+ with contextlib.ExitStack() as handles:
609
+ parent = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
610
+ handles.callback(os.close, parent)
611
+ parts = os.path.relpath(target, root).split(os.sep)
612
+ if [os.fstat(parent).st_dev, os.fstat(parent).st_ino] != root_id:
613
+ raise ValueError('Resource identity changed during reconciliation')
614
+ try:
615
+ for part in parts[:-1]:
616
+ parent = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=parent)
617
+ handles.callback(os.close, parent)
618
+ info = os.stat(parts[-1], dir_fd=parent, follow_symlinks=False)
619
+ current: Dict[str, Any] = {'exists': True, 'device': info.st_dev, 'inode': info.st_ino,
620
+ 'mode': info.st_mode, 'size': info.st_size, 'mtime_ns': info.st_mtime_ns, 'ctime_ns': info.st_ctime_ns}
621
+ if stat.S_ISREG(info.st_mode):
622
+ fd = os.open(parts[-1], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent)
623
+ with os.fdopen(fd, 'rb') as reconciliation_stream:
624
+ observed = os.fstat(reconciliation_stream.fileno())
625
+ if [observed.st_dev, observed.st_ino] != [info.st_dev, info.st_ino]:
626
+ raise ValueError('Target changed during reconciliation')
627
+ current['sample_sha256'] = hashlib.sha256(reconciliation_stream.read(1048576)).hexdigest()
628
+ except FileNotFoundError: current = {'exists': False}
629
+ observation = {'receipt_id': receipt_id, 'receipt_status': receipt['status'] if receipt else 'not_recorded',
630
+ 'admission_resolved': receipt.get('admission_resolved', False) if receipt else False,
631
+ 'root_identity': root_id, 'target': target, 'current_target': current, 'generation': record['generation']}
632
+ retained = inspect_staging(receipt, sample=True) if receipt is not None else None
633
+ if retained is not None: observation['current_staging'] = retained
634
+ if receipt is not None and 'write_staging' in receipt:
635
+ observation['current_write_staging'] = {
636
+ name: inspect_staging(receipt, sample=True, operation='write', leaf=name)
637
+ for name in ['target', 'payload']}
638
+ token = hashlib.sha256(json.dumps(observation, sort_keys=True).encode()).hexdigest()
639
+ if op == 'resolve':
640
+ if q.get('observation_token') != token:
641
+ raise ValueError('Reconciliation changed; inspect the current receipt and target before confirming')
642
+ if receipt_archive is not None:
643
+ return {'receipt': receipt, 'archive': receipt_archive, 'observation': observation,
644
+ 'observation_token': token, 'decision_required': False, 'decision': 'accept_current_state'}
645
+ if receipt is None:
646
+ if len(journal()) >= 10000: raise ValueError('Receipt journal is full; confirmed maintenance is required')
647
+ original_operation = q.get('original_operation')
648
+ if original_operation not in ['delete', 'write']: raise ValueError('Original mutation is required for cancellation')
649
+ original_owner = q.get('original_owner_id', owner)
650
+ original_call = q.get('original_tool_call_id')
651
+ if not isinstance(original_owner, str) or not original_owner or len(original_owner) > 200:
652
+ raise ValueError('Invalid original mutation owner')
653
+ if original_call is not None and (not isinstance(original_call, str) or not original_call or len(original_call) > 200):
654
+ raise ValueError('Invalid original mutation call')
655
+ # A durable tombstone denies a delayed request with this ID, even if it never arrived before inspection.
656
+ receipt = {'version': 1, 'id': receipt_id, 'status': 'cancelled', 'operation': original_operation,
657
+ 'owner_id': original_owner, 'tool_call_id': original_call, 'resource_id': q['resource_id'], 'root_identity': root_id,
658
+ 'configured_root': q['root'], 'root': root, 'requested_path': path,
659
+ 'target': {'path': target}, 'endpoint': q['endpoint']}
660
+ receipt['admission_resolved'] = True
661
+ receipt['resolution'] = {'decision': 'accept_current_state', 'confirmed_by': owner,
662
+ 'confirmed_at': time.time(), 'observation': observation, 'observation_token': token}
663
+ persist_receipt(receipt)
664
+ return {'receipt': receipt, 'observation': observation, 'observation_token': token,
665
+ 'decision_required': op == 'reconcile', 'decision': 'accept_current_state' if op == 'resolve' else None}
666
+ if op not in ['read', 'list', 'stat', 'write', 'delete']:
667
+ raise ValueError('Unsupported managed resource operation')
668
+ path = q.get('path', '.')
669
+ if not isinstance(path, str) or not path or os.path.isabs(path) or '\x00' in path:
670
+ raise ValueError('Resource path must be relative to the configured root')
671
+ target = os.path.realpath(os.path.join(root, path))
672
+ if os.path.commonpath([root, target]) != root:
673
+ raise ValueError('Resource path escapes the configured root')
674
+ if protected_target(target):
675
+ raise ValueError('Resource lease control files are protected')
676
+ if op in ['write', 'delete'] and target == root:
677
+ raise ValueError('Cannot mutate the resource root itself')
678
+ dry = q.get('dry_run', False)
679
+ if not isinstance(dry, bool): raise ValueError('dry_run must be a boolean')
680
+ if dry and op != 'delete': return dict(status, path=target, operation=op, dry_run=True)
681
+ # The lease lock spans verification and the complete filesystem action.
682
+ # Transfers and overlapping operations cannot cross this boundary.
683
+ # Walk held directory descriptors without following links. A path swap
684
+ # after validation cannot redirect the filesystem operation outside its
685
+ # verified root, even if another process renames a parent directory.
686
+ with contextlib.ExitStack() as handles:
687
+ def held(fd: int) -> int:
688
+ handles.callback(os.close, fd)
689
+ return fd
690
+ flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
691
+ parent = held(os.open(root, flags))
692
+ s = os.fstat(parent)
693
+ if [s.st_dev, s.st_ino] != root_id: raise ValueError('Resource identity changed during execution')
694
+ parts = os.path.relpath(target, root).split(os.sep)
695
+ namespaces = []
696
+ directory_ids = [root_id]
697
+ for part in parts[:-1]:
698
+ info = os.fstat(parent)
699
+ namespaces.append({'parent': [info.st_dev, info.st_ino], 'leaf': part})
700
+ parent = held(os.open(part, flags, dir_fd=parent))
701
+ info = os.fstat(parent)
702
+ directory_ids.append([info.st_dev, info.st_ino])
703
+ leaf = parts[-1]
704
+ info = os.fstat(parent)
705
+ namespace = {'parent': [info.st_dev, info.st_ino], 'leaf': leaf}
706
+ namespaces.append(namespace)
707
+ if op == 'list':
708
+ directory = held(os.open(leaf, flags, dir_fd=parent))
709
+ return {'path': target, 'entries': sorted(os.listdir(directory))[:1000]}
710
+ if op == 'stat':
711
+ s = os.stat(leaf, dir_fd=parent, follow_symlinks=False)
712
+ if stat.S_ISLNK(s.st_mode): raise ValueError('Resource target changed to a symbolic link')
713
+ return {'path': target, 'size': s.st_size, 'directory': stat.S_ISDIR(s.st_mode)}
714
+ if op == 'read':
715
+ with os.fdopen(os.open(leaf, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent), 'rb') as read_stream:
716
+ if not stat.S_ISREG(os.fstat(read_stream.fileno()).st_mode): raise ValueError('Read requires a regular file')
717
+ data = read_stream.read(1048577)
718
+ if len(data) > 1048576: raise ValueError('Read exceeds 1 MiB; narrow the resource file')
719
+ return {'path': target, 'content': data.decode('utf-8')}
720
+ # The shared lease lock admits a durable operation before any effect.
721
+ entries = retain_receipts(journal())
722
+ unresolved_physical: Dict[tuple[int, int], str] = {}
723
+ if not dry:
724
+ for previous in entries:
725
+ resolved = previous.get('admission_resolved', previous['status'] in ['verified', 'failed', 'dry_run'])
726
+ affected = previous['target']['path']
727
+ physical = previous['target']
728
+ try:
729
+ info = os.stat(leaf, dir_fd=parent, follow_symlinks=False)
730
+ current_identity = [info.st_dev, info.st_ino]
731
+ except FileNotFoundError: current_identity = None
732
+ same_physical = current_identity == [physical.get('device'), physical.get('inode')]
733
+ same_captured = False
734
+ captured_directory = False
735
+ if not resolved:
736
+ physical_ids = [[physical.get('device'), physical.get('inode')]]
737
+ for operation in ['deletion', 'write']:
738
+ captured_record = previous.get(operation + '_staging', {})
739
+ physical_ids.extend([captured_record.get('intended_identity'), captured_record.get('captured_identity')])
740
+ captured_directory = captured_directory or (captured_record.get('captured_identity') in directory_ids
741
+ and stat.S_ISDIR(captured_record.get('captured_mode', 0)))
742
+ for name in (['target', 'payload'] if operation == 'write' else ['target']):
743
+ retained = inspect_staging(previous, operation=operation, leaf=name)
744
+ if retained is not None and retained['exists']:
745
+ retained_identity = [retained['device'], retained['inode']]
746
+ physical_ids.append(retained_identity)
747
+ captured_directory = captured_directory or (retained_identity in directory_ids and stat.S_ISDIR(retained['mode']))
748
+ same_captured = current_identity is not None and current_identity in physical_ids
749
+ for physical_id in physical_ids:
750
+ if (isinstance(physical_id, list) and len(physical_id) == 2
751
+ and all(isinstance(value, int) and not isinstance(value, bool) for value in physical_id)):
752
+ unresolved_physical[(physical_id[0], physical_id[1])] = previous['id']
753
+ same_namespace = physical.get('namespace') in namespaces
754
+ within_directory = physical.get('kind') == 'directory' and [physical.get('device'), physical.get('inode')] in directory_ids
755
+ # Older receipts still identify a relative pathname under the same physical root.
756
+ relative_alias = False
757
+ if previous['root_identity'] == root_id:
758
+ prior_relative = os.path.relpath(affected, previous['root'])
759
+ current_relative = os.path.relpath(target, root)
760
+ relative_alias = (current_relative == prior_relative or current_relative.startswith(prior_relative + os.sep)
761
+ or prior_relative.startswith(current_relative + os.sep))
762
+ if not resolved and (same_physical or same_captured or captured_directory or same_namespace or within_directory or relative_alias
763
+ or target == affected or target.startswith(affected + os.sep) or affected.startswith(target + os.sep)):
764
+ raise ValueError('Remote mutation fenced by unresolved operation ' + previous['id'] + '; reconcile through /resources')
765
+ content = q.get('content')
766
+ if op == 'write':
767
+ if not isinstance(content, str) or len(content.encode('utf-8')) > 1048576:
768
+ raise ValueError('Write requires UTF-8 content of at most 1 MiB')
769
+ write_content = content
770
+ operation_id = q.get('operation_id')
771
+ if not isinstance(operation_id, str) or str(uuid.UUID(operation_id)) != operation_id:
772
+ raise ValueError('Mutation requires a host-generated operation UUID')
773
+ call_id = q.get('tool_call_id')
774
+ if call_id is not None and (not isinstance(call_id, str) or not call_id or len(call_id) > 200):
775
+ raise ValueError('Invalid originating tool call ID')
776
+ retired = replay_lookup(replay_directory, ['operation', operation_id], replay_required)
777
+ if retired is None and call_id is not None:
778
+ endpoint = q['endpoint']
779
+ retired = replay_lookup(replay_directory, ['call', endpoint['host'], endpoint['user'], endpoint['port'], owner, call_id], replay_required)
780
+ if retired is not None:
781
+ raise ValueError('Operation archived as ' + retired['id'] + '; inspect original evidence instead of replaying')
782
+ if any(previous['id'] == operation_id for previous in entries):
783
+ raise ValueError('Operation already recorded; inspect its receipt instead of replaying')
784
+ if call_id is not None and any(previous['owner_id'] == owner and previous.get('tool_call_id') == call_id for previous in entries):
785
+ raise ValueError('Tool call already recorded; inspect its receipt instead of replaying')
786
+ if len(entries) >= 10000: raise ValueError('Receipt journal is full; archive verified receipts before admitting new mutations')
787
+ intended_identity = None
788
+ if op == 'write':
789
+ try: existing = os.stat(leaf, dir_fd=parent, follow_symlinks=False)
790
+ except FileNotFoundError: existing = None
791
+ if existing is not None:
792
+ if not stat.S_ISREG(existing.st_mode): raise ValueError('Write requires an absent target or a regular file')
793
+ intended = held(os.open(leaf, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent))
794
+ pinned = os.fstat(intended)
795
+ intended_identity = [pinned.st_dev, pinned.st_ino]
796
+ if intended_identity != [existing.st_dev, existing.st_ino]: raise ValueError('Write target changed during inspection')
797
+ pending_id = unresolved_physical.get((pinned.st_dev, pinned.st_ino))
798
+ if pending_id is not None:
799
+ raise ValueError('Remote mutation fenced by unresolved operation ' + pending_id + '; reconcile through /resources')
800
+ receipt = {'version': 1, 'id': operation_id, 'status': 'pending', 'operation': op,
801
+ 'admission_resolved': dry, 'tool_call_id': call_id, 'resource_id': q['resource_id'], 'owner_id': owner,
802
+ 'generation': record['generation'], 'root': root, 'root_identity': root_id,
803
+ 'configured_root': q['root'], 'requested_path': path, 'endpoint': q['endpoint'],
804
+ 'target': {'path': target, 'device': None, 'inode': None, 'kind': None, 'namespace': namespace},
805
+ 'removed_bytes': 0, 'started_at': time.time(), 'completed_at': None,
806
+ 'verification': {'target_absent': None, 'checked_at': None},
807
+ 'space': {'reclaimed_bytes': None, 'attribution': 'unknown', 'source': 'not measured', 'interval': None}}
808
+ persist_receipt(receipt)
809
+ admitted = True
810
+ if op == 'write':
811
+ write_parent: Optional[int] = None
812
+ write_directory: Optional[int] = None
813
+ stage_name = operation_id
814
+ try:
815
+ os.makedirs(write_staging, mode=0o700, exist_ok=True)
816
+ write_parent = held(os.open(write_staging, flags))
817
+ private = os.fstat(write_parent)
818
+ if (private.st_uid != os.geteuid() or private.st_mode & 0o077
819
+ or private.st_dev != os.fstat(parent).st_dev
820
+ or staging_bindings[write_staging] is not None
821
+ and staging_bindings[write_staging] != [private.st_dev, private.st_ino]):
822
+ raise ValueError('Write requires a private same-filesystem host staging directory')
823
+ os.mkdir(stage_name, mode=0o700, dir_fd=write_parent)
824
+ os.fsync(write_parent)
825
+ write_directory = held(os.open(stage_name, flags, dir_fd=write_parent))
826
+ private_operation = os.fstat(write_directory)
827
+ receipt['write_staging'] = {'path': os.path.join(write_staging, stage_name, 'target'),
828
+ 'payload_path': os.path.join(write_staging, stage_name, 'payload'),
829
+ 'directory_identity': [private_operation.st_dev, private_operation.st_ino],
830
+ 'intended_identity': intended_identity, 'state': 'prepared'}
831
+ # Persist both private names before creating or moving data.
832
+ # A reconnect can inspect them even if a syscall never returns.
833
+ persist_receipt(receipt)
834
+ fd = held(os.open('payload', os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=write_directory))
835
+ with os.fdopen(os.dup(fd), 'w') as write_stream:
836
+ write_stream.write(write_content); write_stream.flush(); os.fsync(write_stream.fileno())
837
+ info = os.fstat(fd)
838
+ os.fsync(write_directory)
839
+ receipt['target'].update(device=info.st_dev, inode=info.st_ino, kind='file')
840
+ receipt['write_staging']['state'] = 'payload_ready'
841
+ persist_receipt(receipt)
842
+ if intended_identity is not None:
843
+ rename_without_replacement(parent, leaf, write_directory, 'target')
844
+ os.fsync(parent); os.fsync(write_directory)
845
+ moved = os.stat('target', dir_fd=write_directory, follow_symlinks=False)
846
+ receipt['write_staging'].update(state='captured', captured_identity=[moved.st_dev, moved.st_ino], captured_mode=moved.st_mode)
847
+ persist_receipt(receipt)
848
+ if [moved.st_dev, moved.st_ino] != intended_identity or not stat.S_ISREG(moved.st_mode):
849
+ raise ValueError('Write target replaced before capture; replacement data preserved')
850
+ # Both names are protected: a private source cannot be replaced
851
+ # by managed writes, and the kernel refuses any public newcomer.
852
+ try: rename_without_replacement(write_directory, 'payload', parent, leaf)
853
+ except FileExistsError as error:
854
+ raise ValueError('Write target changed before publication; replacement data preserved') from error
855
+ os.fsync(parent); os.fsync(write_directory)
856
+ receipt['write_staging']['state'] = 'published'
857
+ persist_receipt(receipt)
858
+ visible = os.stat(leaf, dir_fd=parent, follow_symlinks=False)
859
+ same_parent = identity(os.path.dirname(target)) == [os.fstat(parent).st_dev, os.fstat(parent).st_ino]
860
+ verified = (identity(root) == root_id and same_parent and stat.S_ISREG(visible.st_mode)
861
+ and [visible.st_dev, visible.st_ino] == [info.st_dev, info.st_ino])
862
+ if verified:
863
+ if intended_identity is not None: os.unlink('target', dir_fd=write_directory)
864
+ os.fsync(write_directory)
865
+ receipt['write_staging']['state'] = 'completed'
866
+ receipt['status'] = 'verified' if verified else 'unverified'
867
+ receipt['written_bytes'] = len(write_content.encode('utf-8'))
868
+ receipt['content_sha256'] = hashlib.sha256(write_content.encode('utf-8')).hexdigest()
869
+ receipt['verification'] = {'target_matches': verified, 'checked_at': time.time()}
870
+ receipt['completed_at'] = time.time()
871
+ persist_receipt(receipt)
872
+ except Exception as error:
873
+ if write_directory is not None and 'write_staging' in receipt:
874
+ # Inspect the private name, not a post-syscall flag: the
875
+ # capture or publication might have applied before raising.
876
+ try:
877
+ moved = os.stat('target', dir_fd=write_directory, follow_symlinks=False)
878
+ except FileNotFoundError: pass
879
+ else:
880
+ receipt['write_staging'].update(captured_identity=[moved.st_dev, moved.st_ino], captured_mode=moved.st_mode)
881
+ try:
882
+ rename_without_replacement(write_directory, 'target', parent, leaf)
883
+ os.fsync(parent); os.fsync(write_directory)
884
+ receipt['write_staging']['state'] = 'restored'
885
+ except Exception as restoration:
886
+ receipt['write_staging']['restoration_error'] = str(restoration)[:2000]
887
+ receipt['status'] = 'unverified' if 'write_staging' in receipt else 'failed'
888
+ receipt['error'] = str(error)[:2000]
889
+ receipt['completed_at'] = time.time()
890
+ persist_receipt(receipt)
891
+ raise
892
+ finally:
893
+ if write_parent is not None and write_directory is not None:
894
+ # Never purge retained data, including an unpublished payload.
895
+ with contextlib.suppress(OSError):
896
+ os.rmdir(stage_name, dir_fd=write_parent); os.fsync(write_parent)
897
+ return {'path': target, 'operation': 'write', 'written_bytes': receipt['written_bytes'],
898
+ 'generation': record['generation'], 'receipt': receipt}
899
+ removed = False
900
+ captured = False
901
+ deletion_parent: Optional[int] = None
902
+ deletion_directory: Optional[int] = None
903
+ stage_name = operation_id
904
+ def restore_captured() -> None:
905
+ # Even a replacement captured during the race is data: never
906
+ # discard it or overwrite a newer entry in the public directory.
907
+ if deletion_directory is None: raise RuntimeError('Missing private deletion capture')
908
+ rename_without_replacement(deletion_directory, 'target', parent, leaf)
909
+ os.fsync(parent); os.fsync(deletion_directory)
910
+ receipt['deletion_staging']['state'] = 'restored'
911
+ try:
912
+ s = os.stat(leaf, dir_fd=parent, follow_symlinks=False)
913
+ if stat.S_ISLNK(s.st_mode): raise ValueError('Deletion target changed to a symbolic link')
914
+ pending_id = unresolved_physical.get((s.st_dev, s.st_ino))
915
+ if not dry and pending_id is not None:
916
+ raise ValueError('Remote mutation fenced by unresolved operation ' + pending_id + '; reconcile through /resources')
917
+ if not stat.S_ISDIR(s.st_mode) and not stat.S_ISREG(s.st_mode):
918
+ raise ValueError('Deletion requires a regular file or empty directory')
919
+ # Keep the intended inode alive so its number cannot be reused
920
+ # while the public name is being captured.
921
+ intended = held(os.open(leaf, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent))
922
+ observed = os.fstat(intended)
923
+ if [observed.st_dev, observed.st_ino] != [s.st_dev, s.st_ino]:
924
+ raise ValueError('Deletion target changed during inspection')
925
+ kind = 'directory' if stat.S_ISDIR(s.st_mode) else 'file'
926
+ receipt['target'].update(device=s.st_dev, inode=s.st_ino, kind=kind)
927
+ persist_receipt(receipt)
928
+ if not dry and kind == 'directory' and os.listdir(intended):
929
+ raise OSError(errno.ENOTEMPTY, 'Deletion requires an empty directory')
930
+ if dry:
931
+ receipt['status'] = 'dry_run'
932
+ else:
933
+ os.makedirs(staging, mode=0o700, exist_ok=True)
934
+ deletion_parent = held(os.open(staging, flags))
935
+ private = os.fstat(deletion_parent)
936
+ if (private.st_uid != os.geteuid() or private.st_mode & 0o077
937
+ or private.st_dev != s.st_dev
938
+ or q.get('host_delete_staging_identity') is not None
939
+ and q['host_delete_staging_identity'] != [private.st_dev, private.st_ino]):
940
+ raise ValueError('Deletion requires a private same-filesystem host staging directory')
941
+ os.mkdir(stage_name, mode=0o700, dir_fd=deletion_parent)
942
+ os.fsync(deletion_parent)
943
+ deletion_directory = held(os.open(stage_name, flags, dir_fd=deletion_parent))
944
+ private_operation = os.fstat(deletion_directory)
945
+ receipt['deletion_staging'] = {'path': os.path.join(staging, stage_name, 'target'),
946
+ 'directory_identity': [private_operation.st_dev, private_operation.st_ino], 'state': 'prepared'}
947
+ persist_receipt(receipt)
948
+ rename_without_replacement(parent, leaf, deletion_directory, 'target')
949
+ captured = True
950
+ receipt['deletion_staging']['state'] = 'captured'
951
+ os.fsync(parent); os.fsync(deletion_directory)
952
+ moved = os.stat('target', dir_fd=deletion_directory, follow_symlinks=False)
953
+ receipt['deletion_staging']['captured_identity'] = [moved.st_dev, moved.st_ino]
954
+ receipt['deletion_staging']['captured_mode'] = moved.st_mode
955
+ persist_receipt(receipt)
956
+ if [moved.st_dev, moved.st_ino] != [s.st_dev, s.st_ino] or stat.S_IFMT(moved.st_mode) != stat.S_IFMT(s.st_mode):
957
+ raise ValueError('Deletion target replaced before capture; replacement data preserved')
958
+ if stat.S_ISDIR(moved.st_mode): os.rmdir('target', dir_fd=deletion_directory)
959
+ else: os.unlink('target', dir_fd=deletion_directory)
960
+ removed = True
961
+ receipt['deletion_staging']['state'] = 'removed'
962
+ receipt['removed_bytes'] = 0 if kind == 'directory' else moved.st_size
963
+ os.fsync(deletion_directory)
964
+ try:
965
+ os.stat(leaf, dir_fd=parent, follow_symlinks=False)
966
+ absent = False
967
+ except FileNotFoundError: absent = True
968
+ # A held descriptor prevents escaping the root. Completion
969
+ # also requires that the named path still maps to that same
970
+ # parent/root, and that no other process recreated the target.
971
+ same_parent = identity(os.path.dirname(target)) == [os.fstat(parent).st_dev, os.fstat(parent).st_ino]
972
+ same_root = identity(root) == root_id
973
+ visible_absent = not os.path.lexists(target)
974
+ verified = absent and same_parent and same_root and visible_absent
975
+ receipt['verification'] = {'target_absent': verified, 'checked_at': time.time()}
976
+ receipt['status'] = 'verified' if verified else 'unverified'
977
+ receipt['completed_at'] = time.time()
978
+ persist_receipt(receipt)
979
+ except Exception as error:
980
+ if captured and not removed:
981
+ try: restore_captured()
982
+ except Exception as restoration:
983
+ receipt['deletion_staging']['restoration_error'] = str(restoration)[:2000]
984
+ receipt['status'] = 'unverified' if captured else 'failed'
985
+ receipt['error'] = str(error)[:2000]
986
+ receipt['completed_at'] = time.time()
987
+ # Failure to save after a mutation leaves the durable pending
988
+ # record. It never acknowledges deletion as verified.
989
+ persist_receipt(receipt)
990
+ raise
991
+ finally:
992
+ if deletion_parent is not None and deletion_directory is not None:
993
+ # Only an empty operation directory is removable. Retained
994
+ # data survives restart and is never automatically purged.
995
+ with contextlib.suppress(OSError):
996
+ os.rmdir(stage_name, dir_fd=deletion_parent); os.fsync(deletion_parent)
997
+ return {'path': target, 'operation': 'delete', 'removed_bytes': receipt['removed_bytes'],
998
+ 'generation': record['generation'], 'dry_run': dry, 'receipt': receipt}
999
+
1000
+
1001
+ if __name__ == '__main__':
1002
+ try:
1003
+ request = json.load(sys.stdin)
1004
+ result = main(request)
1005
+ print(json.dumps({'ok': True, 'result': result}))
1006
+ except Exception as e:
1007
+ print(json.dumps({'ok': False, 'error': str(e), 'admitted': admitted, 'operation_id': request.get('operation_id') if 'request' in globals() else None}))
1008
+ sys.exit(1)
1009
+ `;