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