@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.
- package/README.md +398 -45
- package/RUST_THIRD_PARTY_LICENSES.txt +1080 -822
- package/THIRD_PARTY_NOTICES.md +273 -12
- package/bin/zmux-darwin-arm64 +0 -0
- package/bin/ztc-audio-darwin-arm64 +0 -0
- package/bin/ztc-capture-darwin-arm64 +0 -0
- package/bin/ztc-computer-darwin-arm64 +0 -0
- package/bin/ztc-llm-darwin-arm64 +0 -0
- package/bin/ztc-tts-darwin-arm64 +0 -0
- package/bin/ztc-tui-darwin-arm64 +0 -0
- package/bin/ztc-zlicky-overlay-darwin-arm64 +0 -0
- package/bin/zterm-darwin-arm64 +0 -0
- package/dist/App.js +1206 -209
- package/dist/agent/agent.d.ts +74 -8
- package/dist/agent/agent.js +1073 -184
- package/dist/agent/backends/account_rotation.d.ts +4 -0
- package/dist/agent/backends/account_rotation.js +8 -2
- package/dist/agent/backends/anthropic.d.ts +13 -0
- package/dist/agent/backends/anthropic.js +169 -28
- package/dist/agent/backends/claude_cli.js +18 -8
- package/dist/agent/backends/codex_app_server.d.ts +1 -0
- package/dist/agent/backends/codex_app_server.js +55 -3
- package/dist/agent/backends/codex_auth.d.ts +1 -0
- package/dist/agent/backends/codex_auth.js +67 -13
- package/dist/agent/backends/codex_buffered_recovery.d.ts +27 -0
- package/dist/agent/backends/codex_buffered_recovery.js +293 -0
- package/dist/agent/backends/gemini.js +3 -1
- package/dist/agent/backends/http_error.d.ts +26 -0
- package/dist/agent/backends/http_error.js +221 -0
- package/dist/agent/backends/index.d.ts +9 -2
- package/dist/agent/backends/index.js +22 -2
- package/dist/agent/backends/openai.d.ts +0 -6
- package/dist/agent/backends/openai.js +5 -2
- package/dist/agent/backends/openai_compatible.d.ts +5 -4
- package/dist/agent/backends/openai_compatible.js +26 -19
- package/dist/agent/backends/openai_responses.d.ts +8 -3
- package/dist/agent/backends/openai_responses.js +77 -33
- package/dist/agent/backends/provider_ids.d.ts +1 -0
- package/dist/agent/backends/provider_ids.js +4 -0
- package/dist/agent/backends/request_metrics.js +12 -2
- package/dist/agent/backends/types.d.ts +109 -6
- package/dist/agent/backends/types.js +5 -1
- package/dist/agent/backends/zrouter_stream.d.ts +6 -1
- package/dist/agent/backends/zrouter_stream.js +160 -42
- package/dist/agent/background_registry.d.ts +2 -0
- package/dist/agent/background_registry.js +1 -0
- package/dist/agent/claim_observation.d.ts +6 -0
- package/dist/agent/claim_observation.js +54 -0
- package/dist/agent/commands/accounts.js +46 -177
- package/dist/agent/commands/attention.d.ts +2 -0
- package/dist/agent/commands/attention.js +67 -0
- package/dist/agent/commands/auth.js +18 -2
- package/dist/agent/commands/board.d.ts +2 -0
- package/dist/agent/commands/board.js +169 -0
- package/dist/agent/commands/clipboard.js +50 -1
- package/dist/agent/commands/compact.js +21 -13
- package/dist/agent/commands/config.js +4 -1
- package/dist/agent/commands/context.js +6 -1
- package/dist/agent/commands/desktop.d.ts +2 -0
- package/dist/agent/commands/desktop.js +29 -0
- package/dist/agent/commands/effort.js +14 -6
- package/dist/agent/commands/environment.d.ts +3 -0
- package/dist/agent/commands/environment.js +109 -0
- package/dist/agent/commands/evidence.d.ts +2 -0
- package/dist/agent/commands/evidence.js +9 -0
- package/dist/agent/commands/fork.js +4 -3
- package/dist/agent/commands/goal.d.ts +33 -1
- package/dist/agent/commands/goal.js +174 -14
- package/dist/agent/commands/goal_exit.d.ts +36 -0
- package/dist/agent/commands/goal_exit.js +120 -0
- package/dist/agent/commands/help.js +3 -1
- package/dist/agent/commands/history.js +3 -1
- package/dist/agent/commands/inbox.d.ts +2 -0
- package/dist/agent/commands/inbox.js +148 -0
- package/dist/agent/commands/index.js +16 -1
- package/dist/agent/commands/input_mode.js +8 -6
- package/dist/agent/commands/job.d.ts +2 -0
- package/dist/agent/commands/job.js +93 -0
- package/dist/agent/commands/keybindings.js +4 -1
- package/dist/agent/commands/label.d.ts +2 -0
- package/dist/agent/commands/label.js +77 -0
- package/dist/agent/commands/layout.d.ts +2 -0
- package/dist/agent/commands/layout.js +20 -0
- package/dist/agent/commands/mirror.js +16 -1
- package/dist/agent/commands/model.js +12 -2
- package/dist/agent/commands/permissions.d.ts +3 -1
- package/dist/agent/commands/permissions.js +79 -89
- package/dist/agent/commands/plan.js +5 -15
- package/dist/agent/commands/profile.js +21 -2
- package/dist/agent/commands/provider_selection.js +20 -1
- package/dist/agent/commands/rename.js +1 -1
- package/dist/agent/commands/resources.d.ts +2 -0
- package/dist/agent/commands/resources.js +67 -0
- package/dist/agent/commands/rewind.d.ts +3 -0
- package/dist/agent/commands/rewind.js +16 -0
- package/dist/agent/commands/runbooks.d.ts +2 -0
- package/dist/agent/commands/runbooks.js +73 -0
- package/dist/agent/commands/runbooks_lazy.d.ts +2 -0
- package/dist/agent/commands/runbooks_lazy.js +8 -0
- package/dist/agent/commands/sessions.d.ts +1 -1
- package/dist/agent/commands/sessions.js +33 -5
- package/dist/agent/commands/settings.js +185 -39
- package/dist/agent/commands/setup.d.ts +2 -0
- package/dist/agent/commands/setup.js +78 -0
- package/dist/agent/commands/share.d.ts +2 -0
- package/dist/agent/commands/share.js +64 -0
- package/dist/agent/commands/status.js +14 -2
- package/dist/agent/commands/types.d.ts +99 -6
- package/dist/agent/commands/update.js +8 -0
- package/dist/agent/commands/usage.d.ts +2 -0
- package/dist/agent/commands/usage.js +8 -0
- package/dist/agent/factory.d.ts +11 -0
- package/dist/agent/factory.js +113 -88
- package/dist/agent/loop_policy.d.ts +6 -3
- package/dist/agent/loop_policy.js +12 -3
- package/dist/agent/measurement_guidance.d.ts +2 -0
- package/dist/agent/measurement_guidance.js +5 -0
- package/dist/agent/request_encoder.d.ts +8 -1
- package/dist/agent/request_encoder.js +95 -23
- package/dist/agent/runtime/benchmark_comparison.d.ts +30 -0
- package/dist/agent/runtime/benchmark_comparison.js +94 -0
- package/dist/agent/runtime/benchmark_contract.d.ts +48 -0
- package/dist/agent/runtime/benchmark_contract.js +90 -0
- package/dist/agent/runtime/checked_commands.d.ts +32 -0
- package/dist/agent/runtime/checked_commands.js +119 -0
- package/dist/agent/runtime/command_execution_policy.d.ts +26 -0
- package/dist/agent/runtime/command_execution_policy.js +91 -0
- package/dist/agent/runtime/command_jobs.d.ts +36 -18
- package/dist/agent/runtime/command_jobs.js +151 -13
- package/dist/agent/runtime/command_panes.d.ts +59 -0
- package/dist/agent/runtime/command_panes.js +398 -0
- package/dist/agent/runtime/command_wait.d.ts +19 -0
- package/dist/agent/runtime/command_wait.js +167 -0
- package/dist/agent/runtime/cpu_benchmark.d.ts +30 -0
- package/dist/agent/runtime/cpu_benchmark.js +126 -0
- package/dist/agent/runtime/delegated_evidence.d.ts +26 -0
- package/dist/agent/runtime/delegated_evidence.js +80 -0
- package/dist/agent/runtime/evidence.d.ts +127 -0
- package/dist/agent/runtime/evidence.js +468 -0
- package/dist/agent/runtime/execution_context.d.ts +7 -0
- package/dist/agent/runtime/execution_context.js +39 -3
- package/dist/agent/runtime/json_verifier_recipe.d.ts +8 -0
- package/dist/agent/runtime/json_verifier_recipe.js +66 -0
- package/dist/agent/runtime/measurements.d.ts +110 -0
- package/dist/agent/runtime/measurements.js +317 -0
- package/dist/agent/runtime/policy.d.ts +4 -0
- package/dist/agent/runtime/policy.js +31 -20
- package/dist/agent/runtime/provider_native_evidence.d.ts +20 -0
- package/dist/agent/runtime/provider_native_evidence.js +86 -0
- package/dist/agent/runtime/recovery.d.ts +2 -0
- package/dist/agent/runtime/recovery.js +45 -2
- package/dist/agent/runtime/remote_broker_transport.d.ts +10 -0
- package/dist/agent/runtime/remote_broker_transport.js +84 -0
- package/dist/agent/runtime/remote_deletion_receipts.d.ts +3 -0
- package/dist/agent/runtime/remote_deletion_receipts.js +43 -0
- package/dist/agent/runtime/remote_journal_replay.d.ts +13 -0
- package/dist/agent/runtime/remote_journal_replay.js +233 -0
- package/dist/agent/runtime/remote_mutation_journal.d.ts +25 -0
- package/dist/agent/runtime/remote_mutation_journal.js +208 -0
- package/dist/agent/runtime/remote_resource_endpoint.d.ts +7 -0
- package/dist/agent/runtime/remote_resource_endpoint.js +6 -0
- package/dist/agent/runtime/remote_resource_errors.d.ts +5 -0
- package/dist/agent/runtime/remote_resource_errors.js +9 -0
- package/dist/agent/runtime/remote_resource_script.d.ts +2 -0
- package/dist/agent/runtime/remote_resource_script.js +1009 -0
- package/dist/agent/runtime/remote_resources.d.ts +29 -0
- package/dist/agent/runtime/remote_resources.js +160 -0
- package/dist/agent/runtime/source_verification.d.ts +12 -0
- package/dist/agent/runtime/source_verification.js +210 -0
- package/dist/agent/runtime/source_verification_contract.d.ts +75 -0
- package/dist/agent/runtime/source_verification_contract.js +203 -0
- package/dist/agent/runtime/tool_checkpoints.d.ts +11 -0
- package/dist/agent/runtime/tool_checkpoints.js +26 -0
- package/dist/agent/runtime/tool_effects.d.ts +5 -0
- package/dist/agent/runtime/tool_progress.d.ts +12 -0
- package/dist/agent/runtime/tool_progress.js +76 -0
- package/dist/agent/runtime/tool_runtime.d.ts +16 -1
- package/dist/agent/runtime/tool_runtime.js +204 -10
- package/dist/agent/runtime/tracing.d.ts +1 -1
- package/dist/agent/runtime/verifier_capture.d.ts +9 -0
- package/dist/agent/runtime/verifier_capture.js +100 -0
- package/dist/agent/runtime/verifier_contract.d.ts +24 -0
- package/dist/agent/runtime/verifier_contract.js +41 -0
- package/dist/agent/subagent.d.ts +20 -10
- package/dist/agent/subagent.js +266 -29
- package/dist/agent/tool_response_directory.d.ts +24 -0
- package/dist/agent/tool_response_directory.js +61 -0
- package/dist/agent/tool_response_source.d.ts +8 -0
- package/dist/agent/tool_response_source.js +49 -0
- package/dist/agent/tool_response_store.d.ts +75 -4
- package/dist/agent/tool_response_store.js +583 -64
- package/dist/agent/tools/ask_user.d.ts +3 -1
- package/dist/agent/tools/ask_user.js +30 -11
- package/dist/agent/tools/background.js +1 -0
- package/dist/agent/tools/benchmark_cpu.d.ts +2 -0
- package/dist/agent/tools/benchmark_cpu.js +102 -0
- package/dist/agent/tools/compare_benchmarks.d.ts +2 -0
- package/dist/agent/tools/compare_benchmarks.js +53 -0
- package/dist/agent/tools/deferred.js +1 -0
- package/dist/agent/tools/delegate.js +18 -2
- package/dist/agent/tools/helper_requirements.d.ts +7 -0
- package/dist/agent/tools/helper_requirements.js +19 -0
- package/dist/agent/tools/index.d.ts +1 -0
- package/dist/agent/tools/index.js +3 -0
- package/dist/agent/tools/measurement_report.d.ts +22 -0
- package/dist/agent/tools/measurement_report.js +190 -0
- package/dist/agent/tools/pane.d.ts +0 -7
- package/dist/agent/tools/pane.js +5 -4
- package/dist/agent/tools/project_context.d.ts +3 -0
- package/dist/agent/tools/project_context.js +45 -0
- package/dist/agent/tools/recall_tool_response.d.ts +3 -5
- package/dist/agent/tools/recall_tool_response.js +19 -29
- package/dist/agent/tools/registry.d.ts +1 -0
- package/dist/agent/tools/registry.js +23 -0
- package/dist/agent/tools/related_sessions.d.ts +2 -0
- package/dist/agent/tools/related_sessions.js +156 -0
- package/dist/agent/tools/routine.js +13 -3
- package/dist/agent/tools/session_evidence.d.ts +3 -0
- package/dist/agent/tools/session_evidence.js +72 -0
- package/dist/agent/tools/shell.d.ts +4 -3
- package/dist/agent/tools/shell.js +174 -47
- package/dist/agent/tools/tool_search.js +3 -2
- package/dist/agent/tools/types.d.ts +28 -0
- package/dist/agent/tools/verify_artifact.d.ts +2 -0
- package/dist/agent/tools/verify_artifact.js +60 -0
- package/dist/agent/tools/workflow.js +1 -0
- package/dist/app/activity.d.ts +6 -1
- package/dist/app/activity.js +81 -12
- package/dist/app/agent_runner.d.ts +6 -1
- package/dist/app/agent_runner.js +34 -19
- package/dist/app/command_context.js +106 -10
- package/dist/app/controller.d.ts +15 -4
- package/dist/app/controller.js +29 -3
- package/dist/app/escape_stop.d.ts +11 -0
- package/dist/app/escape_stop.js +15 -0
- package/dist/app/liveness.d.ts +6 -4
- package/dist/app/liveness.js +13 -3
- package/dist/app/model_catalog_cache.d.ts +5 -0
- package/dist/app/model_catalog_cache.js +138 -0
- package/dist/app/ordered_transcript.js +43 -4
- package/dist/app/prompt_turn.d.ts +14 -0
- package/dist/app/prompt_turn.js +53 -0
- package/dist/app/recap.d.ts +3 -0
- package/dist/app/recap.js +9 -3
- package/dist/app/restart.d.ts +3 -2
- package/dist/app/restart.js +22 -7
- package/dist/app/rewind.d.ts +3 -3
- package/dist/app/rewind.js +3 -3
- package/dist/app/selection_completion.d.ts +17 -2
- package/dist/app/selection_completion.js +84 -9
- package/dist/app/session_manager.d.ts +38 -6
- package/dist/app/session_manager.js +263 -60
- package/dist/app/session_messages.d.ts +3 -0
- package/dist/app/session_messages.js +60 -0
- package/dist/app/task_scope_router.d.ts +20 -0
- package/dist/app/task_scope_router.js +173 -0
- package/dist/app/transcript_window.d.ts +2 -0
- package/dist/app/transcript_window.js +4 -0
- package/dist/app/workspace_admission_details.d.ts +3 -0
- package/dist/app/workspace_admission_details.js +26 -0
- package/dist/app/workspace_layout_controller.d.ts +11 -0
- package/dist/app/workspace_layout_controller.js +88 -0
- package/dist/app/workspace_monitor.d.ts +1 -0
- package/dist/app/workspace_monitor.js +216 -0
- package/dist/app/workspace_presets.d.ts +29 -0
- package/dist/app/workspace_presets.js +192 -0
- package/dist/attention/queue.d.ts +37 -0
- package/dist/attention/queue.js +183 -0
- package/dist/bench/rf_shared.d.ts +2 -0
- package/dist/board/binding.d.ts +42 -0
- package/dist/board/binding.js +38 -0
- package/dist/board/client.d.ts +42 -0
- package/dist/board/client.js +60 -0
- package/dist/cli.js +1 -1
- package/dist/cli_args.js +19 -17
- package/dist/cli_main.js +110 -13
- package/dist/cli_ratatui.d.ts +183 -16
- package/dist/cli_ratatui.js +3989 -1255
- package/dist/components/ActivityLine.d.ts +3 -2
- package/dist/components/ActivityLine.js +2 -2
- package/dist/components/InputArea.d.ts +19 -1
- package/dist/components/InputArea.js +264 -190
- package/dist/components/MessageList.d.ts +2 -0
- package/dist/components/MessageList.js +3 -1
- package/dist/components/SingleMessage.d.ts +2 -0
- package/dist/components/SingleMessage.js +4 -2
- package/dist/components/StatusBar.d.ts +4 -2
- package/dist/components/StatusBar.js +3 -1
- package/dist/conductor_branch.js +3 -0
- package/dist/config/api_retry.d.ts +6 -1
- package/dist/config/api_retry.js +19 -1
- package/dist/config/defaults.d.ts +2 -1
- package/dist/config/defaults.js +9 -2
- package/dist/config/message_display.d.ts +1 -0
- package/dist/config/message_display.js +17 -2
- package/dist/config/permissions.d.ts +21 -0
- package/dist/config/permissions.js +89 -0
- package/dist/config/resolved.d.ts +2 -2
- package/dist/config/types.d.ts +32 -5
- package/dist/config.d.ts +38 -4
- package/dist/config.js +290 -42
- package/dist/context/project_intelligence.d.ts +44 -0
- package/dist/context/project_intelligence.js +246 -0
- package/dist/desktop/cli_launch.d.ts +8 -0
- package/dist/desktop/cli_launch.js +6 -0
- package/dist/desktop/cli_shim.d.ts +8 -0
- package/dist/desktop/cli_shim.js +120 -0
- package/dist/desktop/runtime.d.ts +16 -0
- package/dist/desktop/runtime.js +161 -0
- package/dist/desktop/worker_launch.d.ts +7 -0
- package/dist/desktop/worker_launch.js +7 -0
- package/dist/diagnostics/cargo_mutants_selection.d.ts +27 -0
- package/dist/diagnostics/cargo_mutants_selection.js +48 -0
- package/dist/diagnostics/native_terminal_benchmark.d.ts +58 -0
- package/dist/diagnostics/native_terminal_benchmark.js +1247 -0
- package/dist/diagnostics/session_evidence_package.d.ts +40 -0
- package/dist/diagnostics/session_evidence_package.js +234 -0
- package/dist/diagnostics/session_export.d.ts +93 -0
- package/dist/diagnostics/session_export.js +634 -0
- package/dist/emulation/tool_adapters.js +48 -18
- package/dist/emulation/ztc_addendum.d.ts +1 -0
- package/dist/emulation/ztc_addendum.js +13 -12
- package/dist/environment/service_runner.d.ts +1 -0
- package/dist/environment/service_runner.js +68 -0
- package/dist/environment/store.d.ts +120 -0
- package/dist/environment/store.js +387 -0
- package/dist/events.d.ts +40 -1
- package/dist/events.js +10 -17
- package/dist/goal_contract.d.ts +25 -0
- package/dist/goal_contract.js +44 -0
- package/dist/headless.d.ts +31 -0
- package/dist/headless.js +578 -70
- package/dist/headless_cli.js +8 -0
- package/dist/headless_cli_args.d.ts +1 -0
- package/dist/headless_cli_args.js +1 -0
- package/dist/intake/cli.d.ts +1 -0
- package/dist/intake/cli.js +33 -0
- package/dist/intake/format.d.ts +2 -0
- package/dist/intake/format.js +14 -0
- package/dist/intake/notifications.d.ts +16 -0
- package/dist/intake/notifications.js +132 -0
- package/dist/intake/store.d.ts +56 -0
- package/dist/intake/store.js +238 -0
- package/dist/library.d.ts +8 -0
- package/dist/library.js +19 -10
- package/dist/mcp/client.d.ts +1 -0
- package/dist/mcp/client.js +20 -0
- package/dist/modes/builtin/plan.yaml +2 -2
- package/dist/modes/handlers.js +1 -1
- package/dist/onboarding/first_run.d.ts +3 -0
- package/dist/onboarding/first_run.js +33 -25
- package/dist/onboarding/setup.d.ts +32 -0
- package/dist/onboarding/setup.js +133 -0
- package/dist/onboarding/setup_agent_health.d.ts +17 -0
- package/dist/onboarding/setup_agent_health.js +79 -0
- package/dist/onboarding/setup_environment.d.ts +66 -0
- package/dist/onboarding/setup_environment.js +219 -0
- package/dist/onboarding/setup_workspace.d.ts +17 -0
- package/dist/onboarding/setup_workspace.js +129 -0
- package/dist/optimize/adapters/index.d.ts +4 -1
- package/dist/optimize/adapters/index.js +7 -0
- package/dist/optimize/adapters/kernelbench.js +14 -5
- package/dist/optimize/adapters/popcorn.js +3 -2
- package/dist/optimize/adapters/profile_benchmark.js +13 -7
- package/dist/optimize/adapters/rfbench.js +25 -14
- package/dist/optimize/adapters/types.d.ts +3 -0
- package/dist/optimize/adapters/zergpod.js +5 -2
- package/dist/optimize/baseline.js +10 -21
- package/dist/optimize/branch.js +9 -12
- package/dist/optimize/combine.js +2 -3
- package/dist/optimize/executor.js +34 -48
- package/dist/optimize/measurements.d.ts +55 -0
- package/dist/optimize/measurements.js +173 -0
- package/dist/optimize/results.d.ts +1 -0
- package/dist/optimize/results.js +11 -33
- package/dist/optimize/tge.js +2 -1
- package/dist/panes/initial_input.d.ts +19 -0
- package/dist/panes/initial_input.js +206 -0
- package/dist/panes/labels.d.ts +16 -0
- package/dist/panes/labels.js +42 -0
- package/dist/panes/model_catalog.d.ts +3 -3
- package/dist/panes/model_catalog.js +28 -4
- package/dist/panes/pane_registry.js +6 -2
- package/dist/panes/types.d.ts +3 -0
- package/dist/panes/ztc_launch.js +19 -0
- package/dist/plugin/optimization.d.ts +2 -0
- package/dist/plugin/optimization.js +1 -0
- package/dist/plugin/runtime.d.ts +2 -0
- package/dist/plugin/simulation.d.ts +6 -1
- package/dist/plugin/simulation_contract.d.ts +836 -1
- package/dist/plugin/simulation_documents.d.ts +22 -0
- package/dist/plugin/simulation_documents.js +1 -0
- package/dist/plugin/simulation_interventions.d.ts +103 -0
- package/dist/plugin/simulation_interventions.js +1 -0
- package/dist/plugin/simulation_living.d.ts +1 -0
- package/dist/plugin/simulation_living.js +3 -0
- package/dist/plugin/simulation_meetings.d.ts +121 -0
- package/dist/plugin/simulation_meetings.js +1 -0
- package/dist/plugin/simulation_story_review.d.ts +111 -0
- package/dist/plugin/simulation_story_review.js +1 -0
- package/dist/plugin/simulation_storyteller.d.ts +91 -0
- package/dist/plugin/simulation_storyteller.js +1 -0
- package/dist/plugin/simulation_structured.d.ts +2 -0
- package/dist/plugin/simulation_structured.js +2 -0
- package/dist/plugin/simulation_toolkit.d.ts +21 -0
- package/dist/plugin/simulation_toolkit.js +24 -0
- package/dist/prompts/tool_delegate_task.j2 +1 -1
- package/dist/remote-broker/Dockerfile +10 -0
- package/dist/remote-broker/archive.py +1227 -0
- package/dist/remote-broker/cutover.py +761 -0
- package/dist/remote-broker/requirements.txt +5 -0
- package/dist/remote-broker/resources.json +6 -0
- package/dist/remote-broker/resources.write-staging.example.json +18 -0
- package/dist/remote-broker/retained.py +686 -0
- package/dist/remote-broker/retained_cleanup.py +550 -0
- package/dist/remote-broker/server.py +648 -0
- package/dist/remote-broker/worker.py +1007 -0
- package/dist/router_auth.d.ts +31 -0
- package/dist/router_auth.js +155 -9
- package/dist/runbooks/catalog.d.ts +3 -0
- package/dist/runbooks/catalog.js +26 -0
- package/dist/runbooks/library.d.ts +11 -0
- package/dist/runbooks/library.js +95 -0
- package/dist/runbooks/package.d.ts +43 -0
- package/dist/runbooks/package.js +102 -0
- package/dist/session.d.ts +65 -13
- package/dist/session.js +548 -43
- package/dist/session_handoff.d.ts +25 -0
- package/dist/session_handoff.js +322 -0
- package/dist/session_handoff_contract.d.ts +89 -0
- package/dist/session_handoff_contract.js +179 -0
- package/dist/session_media.d.ts +41 -0
- package/dist/session_media.js +357 -0
- package/dist/session_ownership.d.ts +16 -0
- package/dist/session_ownership.js +221 -0
- package/dist/session_public.d.ts +10 -2
- package/dist/session_public.js +6 -1
- package/dist/simulation/dynamic_history.d.ts +42 -0
- package/dist/simulation/dynamic_history.js +173 -0
- package/dist/simulation/engines/swarm/pane_manager.js +7 -6
- package/dist/simulation/inference.d.ts +3 -1
- package/dist/simulation/inference.js +19 -1
- package/dist/simulation/intervention_generation.d.ts +27 -0
- package/dist/simulation/intervention_generation.js +246 -0
- package/dist/simulation/intervention_material.d.ts +4 -0
- package/dist/simulation/intervention_material.js +81 -0
- package/dist/simulation/living/context.d.ts +3130 -0
- package/dist/simulation/living/context.js +944 -0
- package/dist/simulation/living/definer.d.ts +12 -0
- package/dist/simulation/living/definer.js +223 -0
- package/dist/simulation/living/definition.d.ts +257 -0
- package/dist/simulation/living/definition.js +341 -0
- package/dist/simulation/living/frame.d.ts +259 -0
- package/dist/simulation/living/frame.js +223 -0
- package/dist/simulation/living/identifiers.d.ts +2 -0
- package/dist/simulation/living/identifiers.js +12 -0
- package/dist/simulation/living/index.d.ts +15 -0
- package/dist/simulation/living/index.js +15 -0
- package/dist/simulation/living/inference.d.ts +219 -0
- package/dist/simulation/living/inference.js +576 -0
- package/dist/simulation/living/mechanics.d.ts +54 -0
- package/dist/simulation/living/mechanics.js +588 -0
- package/dist/simulation/living/model-input.d.ts +25 -0
- package/dist/simulation/living/model-input.js +215 -0
- package/dist/simulation/living/open-world.d.ts +257 -0
- package/dist/simulation/living/open-world.js +102 -0
- package/dist/simulation/living/projection.d.ts +7 -0
- package/dist/simulation/living/projection.js +31 -0
- package/dist/simulation/living/recall.d.ts +17 -0
- package/dist/simulation/living/recall.js +25 -0
- package/dist/simulation/living/reduce.d.ts +15 -0
- package/dist/simulation/living/reduce.js +1853 -0
- package/dist/simulation/living/replay.d.ts +6 -0
- package/dist/simulation/living/replay.js +22 -0
- package/dist/simulation/living/runtime.d.ts +140 -0
- package/dist/simulation/living/runtime.js +3020 -0
- package/dist/simulation/living/shared-kinds.d.ts +24 -0
- package/dist/simulation/living/shared-kinds.js +96 -0
- package/dist/simulation/living/types.d.ts +2785 -0
- package/dist/simulation/living/types.js +669 -0
- package/dist/simulation/llm.d.ts +3 -1
- package/dist/simulation/llm.js +90 -30
- package/dist/simulation/memory/embedding.d.ts +1 -1
- package/dist/simulation/memory/embedding.js +9 -3
- package/dist/simulation/memory/filesystem.d.ts +2 -0
- package/dist/simulation/memory/filesystem.js +5 -1
- package/dist/simulation/memory/filesystem_file_tree.d.ts +4 -1
- package/dist/simulation/memory/filesystem_file_tree.js +41 -10
- package/dist/simulation/memory/filesystem_invariants.js +1 -2
- package/dist/simulation/memory/filesystem_types.d.ts +1 -0
- package/dist/simulation/memory/memory.d.ts +2 -0
- package/dist/simulation/memory/memory.js +5 -1
- package/dist/simulation/orchestration/intent.js +0 -2
- package/dist/simulation/orchestration/lifecycle.js +0 -2
- package/dist/simulation/plugin_host.d.ts +25 -1
- package/dist/simulation/plugin_host.js +334 -17
- package/dist/simulation/session_documents.d.ts +20 -0
- package/dist/simulation/session_documents.js +62 -0
- package/dist/simulation/session_input_validation.d.ts +43 -0
- package/dist/simulation/session_input_validation.js +364 -0
- package/dist/simulation/session_inputs.d.ts +32 -0
- package/dist/simulation/session_inputs.js +154 -0
- package/dist/simulation/telemetry.d.ts +11 -14
- package/dist/simulation/telemetry.js +177 -54
- package/dist/simulation/toolkit/bounded_turns.d.ts +13 -0
- package/dist/simulation/toolkit/bounded_turns.js +80 -0
- package/dist/simulation/toolkit/dynamic_agents.d.ts +25 -0
- package/dist/simulation/toolkit/dynamic_agents.js +218 -0
- package/dist/simulation/toolkit/dynamic_digest.d.ts +1 -0
- package/dist/simulation/toolkit/dynamic_digest.js +8 -0
- package/dist/simulation/toolkit/dynamic_documents.d.ts +7 -0
- package/dist/simulation/toolkit/dynamic_documents.js +71 -0
- package/dist/simulation/toolkit/dynamic_driver.d.ts +8 -0
- package/dist/simulation/toolkit/dynamic_driver.js +269 -0
- package/dist/simulation/toolkit/dynamic_game.d.ts +53 -0
- package/dist/simulation/toolkit/dynamic_game.js +805 -0
- package/dist/simulation/toolkit/dynamic_knowledge.d.ts +15 -0
- package/dist/simulation/toolkit/dynamic_knowledge.js +91 -0
- package/dist/simulation/toolkit/dynamic_mechanics.d.ts +22 -0
- package/dist/simulation/toolkit/dynamic_mechanics.js +910 -0
- package/dist/simulation/toolkit/dynamic_meetings.d.ts +239 -0
- package/dist/simulation/toolkit/dynamic_meetings.js +350 -0
- package/dist/simulation/toolkit/dynamic_progress.d.ts +131 -0
- package/dist/simulation/toolkit/dynamic_progress.js +214 -0
- package/dist/simulation/toolkit/dynamic_projection.d.ts +9 -0
- package/dist/simulation/toolkit/dynamic_projection.js +219 -0
- package/dist/simulation/toolkit/dynamic_runtime.d.ts +412 -0
- package/dist/simulation/toolkit/dynamic_runtime.js +6536 -0
- package/dist/simulation/toolkit/dynamic_state.d.ts +111 -0
- package/dist/simulation/toolkit/dynamic_state.js +1967 -0
- package/dist/simulation/toolkit/dynamic_story_review.d.ts +508 -0
- package/dist/simulation/toolkit/dynamic_story_review.js +333 -0
- package/dist/simulation/toolkit/dynamic_storyteller.d.ts +77 -0
- package/dist/simulation/toolkit/dynamic_storyteller.js +114 -0
- package/dist/simulation/toolkit/dynamic_storyteller_runtime.d.ts +26 -0
- package/dist/simulation/toolkit/dynamic_storyteller_runtime.js +126 -0
- package/dist/simulation/toolkit/dynamic_world.d.ts +22 -0
- package/dist/simulation/toolkit/dynamic_world.js +377 -0
- package/dist/simulation/toolkit/structured_output.d.ts +5 -0
- package/dist/simulation/toolkit/structured_output.js +321 -0
- package/dist/simulation/types/world.d.ts +2 -0
- package/dist/simulation/world/artifact_store.js +1 -1
- package/dist/simulation/world/memory_manager.d.ts +5 -0
- package/dist/simulation/world/memory_manager.js +17 -2
- package/dist/simulation/world/state.d.ts +3 -2
- package/dist/simulation/world/state.js +15 -5
- package/dist/skills/bundled/measurement-validation/SKILL.md +144 -0
- package/dist/terminal_attach.d.ts +1 -0
- package/dist/terminal_attach.js +113 -0
- package/dist/tool_progress.d.ts +3 -0
- package/dist/tool_progress.js +27 -0
- package/dist/types.d.ts +145 -10
- package/dist/types.js +3 -0
- package/dist/ui/core/input.d.ts +3 -0
- package/dist/ui/core/input_segments.d.ts +18 -1
- package/dist/ui/core/input_segments.js +131 -47
- package/dist/ui/core/input_state.d.ts +12 -0
- package/dist/ui/core/layout_yoga.d.ts +1 -1
- package/dist/ui/core/turn_metadata.d.ts +5 -0
- package/dist/ui/core/turn_metadata.js +22 -0
- package/dist/ui/ink/render.d.ts +2 -1
- package/dist/ui/ink/render.js +6 -5
- package/dist/ui/ratatui/headless_pane_manager.d.ts +14 -1
- package/dist/ui/ratatui/headless_pane_manager.js +293 -104
- package/dist/ui/ratatui/index.d.ts +1 -0
- package/dist/ui/ratatui/input.d.ts +4 -0
- package/dist/ui/ratatui/input.js +15 -0
- package/dist/ui/ratatui/ipc_path.d.ts +2 -0
- package/dist/ui/ratatui/ipc_path.js +29 -1
- package/dist/ui/ratatui/mux_persistence.d.ts +3 -1
- package/dist/ui/ratatui/mux_persistence.js +139 -1
- package/dist/ui/ratatui/pane_relay.d.ts +1 -1
- package/dist/ui/ratatui/pane_relay.js +55 -27
- package/dist/ui/ratatui/process.d.ts +17 -1
- package/dist/ui/ratatui/process.js +80 -7
- package/dist/ui/ratatui/protocol.d.ts +236 -1
- package/dist/ui/ratatui/protocol.js +90 -0
- package/dist/ui/scrollback.d.ts +2 -1
- package/dist/ui/scrollback.js +9 -4
- package/dist/ui/stream_typewriter.d.ts +8 -3
- package/dist/ui/stream_typewriter.js +30 -1
- package/dist/ui/streaming_scrollback.d.ts +1 -0
- package/dist/ui/streaming_scrollback.js +1 -1
- package/dist/ui/transcript_render_cache.js +10 -0
- package/dist/ui/views/activity_line.d.ts +5 -3
- package/dist/ui/views/activity_line.js +65 -7
- package/dist/ui/views/app.d.ts +6 -3
- package/dist/ui/views/app.js +17 -6
- package/dist/ui/views/completion_menu.js +10 -5
- package/dist/ui/views/input_area.d.ts +7 -3
- package/dist/ui/views/input_area.js +15 -9
- package/dist/ui/views/message_list.d.ts +4 -1
- package/dist/ui/views/message_list.js +89 -19
- package/dist/ui/views/queue_panel.d.ts +3 -1
- package/dist/ui/views/queue_panel.js +9 -2
- package/dist/ui/views/status_bar.d.ts +4 -2
- package/dist/ui/views/status_bar.js +28 -13
- package/dist/ui/views/theme.d.ts +2 -0
- package/dist/ui/views/theme.js +2 -0
- package/dist/ui/widgets/color_panel.js +18 -0
- package/dist/ui/widgets/form.js +8 -0
- package/dist/ui/widgets/log_view.js +7 -0
- package/dist/ui/widgets/navigator_panel.js +8 -0
- package/dist/ui/widgets/prompt_viewer.js +2 -0
- package/dist/ui/widgets/questionnaire.d.ts +2 -0
- package/dist/ui/widgets/questionnaire.js +79 -16
- package/dist/ui/widgets/resume_panel.d.ts +2 -0
- package/dist/ui/widgets/resume_panel.js +26 -15
- package/dist/ui/widgets/rewind_panel.d.ts +2 -2
- package/dist/ui/widgets/rewind_panel.js +4 -2
- package/dist/ui/widgets/select.js +16 -27
- package/dist/ui/widgets/settings_panel.js +16 -0
- package/dist/ui/widgets/status_line_panel.d.ts +1 -0
- package/dist/ui/widgets/status_line_panel.js +30 -4
- package/dist/ui/widgets/text_input.d.ts +2 -0
- package/dist/ui/widgets/text_input.js +15 -1
- package/dist/ui/widgets/types.d.ts +2 -0
- package/dist/usage_projection.d.ts +24 -0
- package/dist/usage_projection.js +130 -0
- package/dist/usage_snapshot.d.ts +36 -0
- package/dist/usage_snapshot.js +131 -0
- package/dist/utils/assistant_message.d.ts +2 -0
- package/dist/utils/assistant_message.js +5 -0
- package/dist/utils/atomic_write.d.ts +6 -2
- package/dist/utils/atomic_write.js +29 -4
- package/dist/utils/checked_shell.d.ts +25 -0
- package/dist/utils/checked_shell.js +107 -0
- package/dist/utils/compaction.d.ts +4 -1
- package/dist/utils/compaction.js +31 -7
- package/dist/utils/context.d.ts +4 -4
- package/dist/utils/context.js +14 -12
- package/dist/utils/deepseek.d.ts +2 -2
- package/dist/utils/deepseek.js +13 -5
- package/dist/utils/ephemeral.js +4 -0
- package/dist/utils/ephemeral_tools.d.ts +18 -0
- package/dist/utils/ephemeral_tools.js +97 -0
- package/dist/utils/jev_client.d.ts +4 -0
- package/dist/utils/jev_client.js +15 -0
- package/dist/utils/model_cache.d.ts +15 -0
- package/dist/utils/model_cache.js +143 -0
- package/dist/utils/models.d.ts +4 -2
- package/dist/utils/models.js +12 -16
- package/dist/utils/reasoning.d.ts +8 -1
- package/dist/utils/reasoning.js +31 -11
- package/dist/utils/router_catalog.d.ts +49 -0
- package/dist/utils/router_catalog.js +271 -0
- package/dist/utils/sandbox.d.ts +9 -0
- package/dist/utils/sandbox.js +118 -15
- package/dist/utils/shell.d.ts +7 -0
- package/dist/utils/shell.js +98 -34
- package/dist/utils/subprocess_env.d.ts +3 -0
- package/dist/utils/subprocess_env.js +27 -1
- package/dist/web/accounts_mirror.d.ts +3 -1
- package/dist/web/accounts_mirror.js +13 -9
- package/dist/web/child_publication.d.ts +17 -0
- package/dist/web/child_publication.js +45 -0
- package/dist/web/mirror_authenticator.d.ts +31 -11
- package/dist/web/mirror_authenticator.js +67 -17
- package/dist/web/mirror_client.d.ts +31 -4
- package/dist/web/mirror_client.js +189 -23
- package/dist/web/mirror_stream.d.ts +84 -22
- package/dist/web/mirror_stream.js +676 -95
- package/dist/web/observer_query_redaction.d.ts +6 -0
- package/dist/web/observer_query_redaction.js +58 -0
- package/dist/web/pane_registry.d.ts +2 -0
- package/dist/web/pane_registry.js +86 -22
- package/dist/web/tool_image.d.ts +6 -0
- package/dist/web/tool_image.js +17 -0
- package/dist/web/view_plugin.d.ts +14 -0
- package/dist/web/view_plugin.js +23 -1
- package/dist/worker/executor.js +31 -12
- package/dist/worker/index.d.ts +1 -0
- package/dist/worker/toolsets.js +3 -1
- package/dist/worker/types.d.ts +12 -1
- package/dist/workflows/registry.js +1 -0
- package/dist/workflows/registry_handle.d.ts +2 -0
- package/dist/workflows/runtime.d.ts +2 -0
- package/dist/workflows/runtime.js +1 -0
- package/dist/workspace_agent.d.ts +7 -0
- package/dist/workspace_agent.js +36 -0
- package/dist/workspace_collision.d.ts +76 -0
- package/dist/workspace_collision.js +688 -0
- package/dist/workspace_layout.d.ts +18 -0
- package/dist/workspace_layout.js +121 -0
- package/dist/workspace_runtime.d.ts +44 -0
- package/dist/workspace_runtime.js +82 -0
- package/dist/workspace_session_context.d.ts +62 -0
- package/dist/workspace_session_context.js +160 -0
- package/dist/zlm-embed.mjs +32518 -9615
- package/dist/zlm.js +1 -0
- package/node_modules/@zerg/model-contracts/LICENSE.md +375 -0
- package/node_modules/@zerg/model-contracts/README.md +90 -0
- package/node_modules/@zerg/model-contracts/index.d.ts +20 -0
- package/node_modules/@zerg/model-contracts/index.js +223 -0
- package/node_modules/@zerg/model-contracts/package.json +8 -0
- package/node_modules/glob/node_modules/brace-expansion/index.js +78 -22
- package/node_modules/glob/node_modules/brace-expansion/package.json +1 -1
- package/node_modules/readdir-glob/node_modules/brace-expansion/index.js +78 -22
- package/node_modules/readdir-glob/node_modules/brace-expansion/package.json +1 -1
- package/package.json +45 -24
- package/packages/model-contracts/LICENSE.md +375 -0
- package/packages/model-contracts/README.md +90 -0
- package/packages/model-contracts/index.d.ts +20 -0
- package/packages/model-contracts/index.js +223 -0
- package/packages/model-contracts/package.json +8 -0
- package/remote-broker/Dockerfile +10 -0
- package/remote-broker/README.md +308 -0
- package/remote-broker/archive.py +1227 -0
- package/remote-broker/cutover.py +761 -0
- package/remote-broker/requirements.txt +5 -0
- package/remote-broker/resources.example.json +6 -0
- package/remote-broker/resources.write-staging.example.json +18 -0
- package/remote-broker/retained.py +686 -0
- package/remote-broker/retained_cleanup.py +550 -0
- package/remote-broker/server.py +648 -0
- package/scripts/build-remote-broker.mjs +13 -0
- package/scripts/ensure-local-packages.mjs +6 -0
- package/scripts/node-pty-helpers.mjs +103 -0
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
"""Offline operator cutover. Archives evidence; never imports lease authority."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import contextlib
|
|
5
|
+
import fcntl
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import stat
|
|
12
|
+
import tempfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Iterator, Literal
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
17
|
+
|
|
18
|
+
L = logging.getLogger(__name__)
|
|
19
|
+
MAX_FILE = 8 * 1024 * 1024
|
|
20
|
+
MAX_TOTAL = 64 * 1024 * 1024
|
|
21
|
+
UUID = re.compile(r"^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$")
|
|
22
|
+
FENCE = "cutover-fence.json"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Endpoint(BaseModel):
|
|
26
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
27
|
+
host: str = Field(min_length=1, max_length=253)
|
|
28
|
+
user: str = Field(min_length=1, max_length=100)
|
|
29
|
+
port: int = Field(ge=1, le=65535)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Resource(BaseModel):
|
|
33
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
34
|
+
id: str = Field(pattern=r"^[a-zA-Z0-9._:-]{1,100}$")
|
|
35
|
+
configured_root: str
|
|
36
|
+
root_directory: str
|
|
37
|
+
endpoint: Endpoint
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class StagingView(BaseModel):
|
|
41
|
+
model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
|
|
42
|
+
configured_path: str = Field(min_length=1, max_length=4096)
|
|
43
|
+
directory: str = Field(min_length=1, max_length=4096)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PolicyResource(BaseModel):
|
|
47
|
+
model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
|
|
48
|
+
id: str = Field(pattern=r"^[a-zA-Z0-9._:-]{1,100}$")
|
|
49
|
+
root: str = Field(min_length=1, max_length=4096)
|
|
50
|
+
endpoint: Endpoint
|
|
51
|
+
write_staging_directory: str | None = Field(default=None, max_length=4096)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ResourcePolicy(BaseModel):
|
|
55
|
+
# Unknown policy fields fail closed rather than becoming omitted locations.
|
|
56
|
+
model_config = ConfigDict(extra="forbid", strict=True, frozen=True)
|
|
57
|
+
version: Literal[1]
|
|
58
|
+
resources: list[PolicyResource] = Field(min_length=1, max_length=100)
|
|
59
|
+
receipt_archive_index: bool = False
|
|
60
|
+
legacy_write_staging_directories: list[str] = Field(
|
|
61
|
+
default_factory=list, max_length=100
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Authority(BaseModel):
|
|
66
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
67
|
+
state_directory: str
|
|
68
|
+
staging_directory: str
|
|
69
|
+
worker_file: str
|
|
70
|
+
resources: list[Resource] = Field(min_length=1, max_length=100)
|
|
71
|
+
resource_policy_file: str | None = Field(default=None, max_length=4096)
|
|
72
|
+
legacy_default_staging_only: bool = False
|
|
73
|
+
staging_views: list[StagingView] = Field(default_factory=list, max_length=201)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Mapping(BaseModel):
|
|
77
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
78
|
+
legacy: str
|
|
79
|
+
receiver: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Manifest(BaseModel):
|
|
83
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
84
|
+
version: Literal[1]
|
|
85
|
+
clients_stopped: bool
|
|
86
|
+
legacy: Authority
|
|
87
|
+
receiver: Authority
|
|
88
|
+
client_journals: list[str] = Field(min_length=1, max_length=100)
|
|
89
|
+
identity_mapping: list[Mapping] = Field(min_length=1, max_length=100)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Lease(BaseModel):
|
|
93
|
+
model_config = ConfigDict(extra="allow", strict=True, allow_inf_nan=False)
|
|
94
|
+
root: str
|
|
95
|
+
identity: list[int] = Field(min_length=2, max_length=2)
|
|
96
|
+
ancestors: list[list[int]]
|
|
97
|
+
owner_id: str | None
|
|
98
|
+
generation: int = Field(ge=0)
|
|
99
|
+
expires_at: float | int
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Receipt(BaseModel):
|
|
103
|
+
model_config = ConfigDict(extra="allow", strict=True)
|
|
104
|
+
version: Literal[1]
|
|
105
|
+
id: str
|
|
106
|
+
operation: Literal["write", "delete"]
|
|
107
|
+
status: Literal[
|
|
108
|
+
"pending", "verified", "failed", "unverified", "dry_run", "cancelled"
|
|
109
|
+
]
|
|
110
|
+
admission_resolved: bool = False
|
|
111
|
+
owner_id: str
|
|
112
|
+
resource_id: str
|
|
113
|
+
endpoint: Endpoint
|
|
114
|
+
configured_root: str
|
|
115
|
+
root_identity: list[int] = Field(min_length=2, max_length=2)
|
|
116
|
+
requested_path: str
|
|
117
|
+
tool_call_id: str | None
|
|
118
|
+
target: dict[str, Any]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ClientRecord(BaseModel):
|
|
122
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
123
|
+
version: Literal[1]
|
|
124
|
+
id: str
|
|
125
|
+
status: Literal["pending", "resolved", "not_dispatched"]
|
|
126
|
+
operation: Literal["write", "delete"]
|
|
127
|
+
owner_id: str
|
|
128
|
+
resource_id: str
|
|
129
|
+
endpoint: Endpoint
|
|
130
|
+
target: str
|
|
131
|
+
requested_path: str
|
|
132
|
+
tool_call_id: str | None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def canonical(data: Any) -> bytes:
|
|
136
|
+
return json.dumps(
|
|
137
|
+
data, sort_keys=True, separators=(",", ":"), allow_nan=False
|
|
138
|
+
).encode()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def digest(data: bytes) -> str:
|
|
142
|
+
return hashlib.sha256(data).hexdigest()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def normalized_manifest(manifest: Manifest) -> dict[str, Any]:
|
|
146
|
+
value: dict[str, Any] = manifest.model_dump()
|
|
147
|
+
value["identity_mapping"] = sorted(
|
|
148
|
+
value["identity_mapping"], key=lambda m: m["legacy"]
|
|
149
|
+
)
|
|
150
|
+
value["client_journals"] = sorted(value["client_journals"])
|
|
151
|
+
for side in ["legacy", "receiver"]:
|
|
152
|
+
value[side]["resources"] = sorted(
|
|
153
|
+
value[side]["resources"], key=lambda r: r["id"]
|
|
154
|
+
)
|
|
155
|
+
value[side]["staging_views"] = sorted(
|
|
156
|
+
value[side]["staging_views"], key=lambda view: view["configured_path"]
|
|
157
|
+
)
|
|
158
|
+
return value
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def directory(value: str) -> Path:
|
|
162
|
+
path = Path(value)
|
|
163
|
+
if not path.is_absolute() or str(path.resolve(strict=True)) != value:
|
|
164
|
+
raise ValueError(
|
|
165
|
+
"Evidence directories must be existing canonical absolute paths"
|
|
166
|
+
)
|
|
167
|
+
if not stat.S_ISDIR(path.lstat().st_mode):
|
|
168
|
+
raise ValueError("Evidence path is not a directory")
|
|
169
|
+
return path
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def read_file(path: Path) -> bytes:
|
|
173
|
+
descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
|
174
|
+
try:
|
|
175
|
+
if not stat.S_ISREG(os.fstat(descriptor).st_mode):
|
|
176
|
+
raise ValueError("Evidence must be a regular file")
|
|
177
|
+
with os.fdopen(descriptor, "rb", closefd=False) as stream:
|
|
178
|
+
data = stream.read(MAX_FILE + 1)
|
|
179
|
+
if len(data) > MAX_FILE:
|
|
180
|
+
raise ValueError("Evidence file exceeds the byte bound")
|
|
181
|
+
return data
|
|
182
|
+
finally:
|
|
183
|
+
os.close(descriptor)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def json_data(data: bytes) -> Any:
|
|
187
|
+
try:
|
|
188
|
+
return json.loads(data)
|
|
189
|
+
except (ValueError, UnicodeError) as error:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
"Invalid JSON evidence; restore or reconcile it first"
|
|
192
|
+
) from error
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def entries(path: Path) -> list[Path]:
|
|
196
|
+
result = sorted(path.iterdir())
|
|
197
|
+
if len(result) > 10000:
|
|
198
|
+
raise ValueError(
|
|
199
|
+
"Journal exceeds the entry bound; reviewed maintenance required"
|
|
200
|
+
)
|
|
201
|
+
for item in result:
|
|
202
|
+
if item.suffix != ".json" or UUID.fullmatch(item.stem) is None:
|
|
203
|
+
raise ValueError("Invalid journal evidence entry")
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def sync_directory(path: Path) -> None:
|
|
208
|
+
descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
209
|
+
try:
|
|
210
|
+
os.fsync(descriptor)
|
|
211
|
+
finally:
|
|
212
|
+
os.close(descriptor)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def publish(path: Path, data: bytes) -> None:
|
|
216
|
+
"""No-overwrite, durable, resumable publication of exact immutable bytes."""
|
|
217
|
+
if os.path.lexists(path):
|
|
218
|
+
if read_file(path) != data:
|
|
219
|
+
raise ValueError(
|
|
220
|
+
"Existing cutover evidence conflicts; preserve it for recovery"
|
|
221
|
+
)
|
|
222
|
+
sync_directory(path.parent)
|
|
223
|
+
return
|
|
224
|
+
descriptor, temporary = tempfile.mkstemp(prefix=".cutover-", dir=path.parent)
|
|
225
|
+
try:
|
|
226
|
+
with os.fdopen(descriptor, "wb") as stream:
|
|
227
|
+
stream.write(data)
|
|
228
|
+
stream.flush()
|
|
229
|
+
os.fsync(stream.fileno())
|
|
230
|
+
os.link(temporary, path)
|
|
231
|
+
sync_directory(path.parent)
|
|
232
|
+
finally:
|
|
233
|
+
os.unlink(temporary)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def make_directory(path: Path) -> None:
|
|
237
|
+
if not path.exists():
|
|
238
|
+
make_directory(path.parent)
|
|
239
|
+
path.mkdir(mode=0o700)
|
|
240
|
+
sync_directory(path.parent)
|
|
241
|
+
directory(str(path))
|
|
242
|
+
sync_directory(path)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@contextlib.contextmanager
|
|
246
|
+
def locked(manifest: Manifest) -> Iterator[None]:
|
|
247
|
+
"""Use the same lock as every managed worker, in canonical path order."""
|
|
248
|
+
states = sorted(
|
|
249
|
+
{manifest.legacy.state_directory, manifest.receiver.state_directory}
|
|
250
|
+
)
|
|
251
|
+
if len(states) != 2 or any(
|
|
252
|
+
Path(a).is_relative_to(b) for a in states for b in states if a != b
|
|
253
|
+
):
|
|
254
|
+
raise ValueError(
|
|
255
|
+
"Legacy and receiver state directories must be distinct and disjoint"
|
|
256
|
+
)
|
|
257
|
+
with contextlib.ExitStack() as stack:
|
|
258
|
+
for value in states:
|
|
259
|
+
state = directory(value)
|
|
260
|
+
descriptor = os.open(
|
|
261
|
+
state / "lock", os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK
|
|
262
|
+
)
|
|
263
|
+
stack.callback(os.close, descriptor)
|
|
264
|
+
if not stat.S_ISREG(os.fstat(descriptor).st_mode):
|
|
265
|
+
raise ValueError("Missing regular broker lock evidence")
|
|
266
|
+
fcntl.flock(descriptor, fcntl.LOCK_EX)
|
|
267
|
+
yield
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class Survey:
|
|
271
|
+
def __init__(self, manifest: Manifest, reference: Path) -> None:
|
|
272
|
+
self.manifest = manifest
|
|
273
|
+
self.files: dict[str, bytes] = {}
|
|
274
|
+
self.blockers: list[str] = []
|
|
275
|
+
self.inventory: list[dict[str, Any]] = []
|
|
276
|
+
self.bindings: list[dict[str, Any]] = []
|
|
277
|
+
self.receipts: list[Receipt] = []
|
|
278
|
+
self.staging_bindings: list[dict[str, Any]] = []
|
|
279
|
+
self.reference = read_file(reference)
|
|
280
|
+
if (
|
|
281
|
+
b"os.path.lexists(os.path.join(control, 'cutover-fence.json'))"
|
|
282
|
+
not in self.reference
|
|
283
|
+
):
|
|
284
|
+
raise ValueError("Reference worker lacks the cutover fence protocol")
|
|
285
|
+
|
|
286
|
+
def capture(self, path: Path, name: str) -> bytes:
|
|
287
|
+
data = read_file(path)
|
|
288
|
+
self.files[name] = data
|
|
289
|
+
if sum(map(len, self.files.values())) > MAX_TOTAL:
|
|
290
|
+
raise ValueError("Cutover evidence exceeds the aggregate byte bound")
|
|
291
|
+
return data
|
|
292
|
+
|
|
293
|
+
def inspect_staging(
|
|
294
|
+
self, side: str, authority: Authority, *, check_contents: bool = True
|
|
295
|
+
) -> None:
|
|
296
|
+
default = directory(authority.staging_directory)
|
|
297
|
+
if authority.resource_policy_file is None:
|
|
298
|
+
if (
|
|
299
|
+
side == "receiver"
|
|
300
|
+
or not authority.legacy_default_staging_only
|
|
301
|
+
or authority.staging_views
|
|
302
|
+
):
|
|
303
|
+
self.blockers.append(
|
|
304
|
+
side + ": complete resource policy and staging inventory required"
|
|
305
|
+
)
|
|
306
|
+
info = default.stat()
|
|
307
|
+
self.staging_bindings.append(
|
|
308
|
+
{
|
|
309
|
+
"side": side,
|
|
310
|
+
"configured_path": "legacy-default",
|
|
311
|
+
"directory": str(default),
|
|
312
|
+
"identity": [info.st_dev, info.st_ino],
|
|
313
|
+
"owner_uid": info.st_uid,
|
|
314
|
+
"mode": stat.S_IMODE(info.st_mode),
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
if check_contents and any(default.iterdir()):
|
|
318
|
+
self.blockers.append(
|
|
319
|
+
side + ": retained staging objects require confirmed recovery"
|
|
320
|
+
)
|
|
321
|
+
return
|
|
322
|
+
if authority.legacy_default_staging_only:
|
|
323
|
+
raise ValueError(
|
|
324
|
+
"Legacy default-only attestation conflicts with complete policy inventory"
|
|
325
|
+
)
|
|
326
|
+
policy_path = Path(authority.resource_policy_file)
|
|
327
|
+
if not policy_path.is_absolute() or str(
|
|
328
|
+
policy_path.resolve(strict=True)
|
|
329
|
+
) != str(policy_path):
|
|
330
|
+
raise ValueError(
|
|
331
|
+
"Resource policy must have an existing canonical absolute path"
|
|
332
|
+
)
|
|
333
|
+
policy = ResourcePolicy.model_validate_json(
|
|
334
|
+
self.capture(policy_path, side + "/resource-policy.json")
|
|
335
|
+
)
|
|
336
|
+
resources = {resource.id: resource for resource in authority.resources}
|
|
337
|
+
if len(resources) != len(authority.resources) or len(
|
|
338
|
+
{resource.id for resource in policy.resources}
|
|
339
|
+
) != len(policy.resources):
|
|
340
|
+
raise ValueError(
|
|
341
|
+
"Resource policy and inventory require unique resource IDs"
|
|
342
|
+
)
|
|
343
|
+
if set(resources) != {resource.id for resource in policy.resources}:
|
|
344
|
+
raise ValueError("Resource policy inventory is incomplete")
|
|
345
|
+
expected = {
|
|
346
|
+
"/data/.ztc-delete-quarantine",
|
|
347
|
+
*policy.legacy_write_staging_directories,
|
|
348
|
+
}
|
|
349
|
+
for resource in policy.resources:
|
|
350
|
+
mapped = resources[resource.id]
|
|
351
|
+
if (
|
|
352
|
+
resource.root != mapped.configured_root
|
|
353
|
+
or resource.endpoint != mapped.endpoint
|
|
354
|
+
):
|
|
355
|
+
raise ValueError(
|
|
356
|
+
"Resource policy lacks its original reviewed root/endpoint binding"
|
|
357
|
+
)
|
|
358
|
+
if resource.write_staging_directory is not None:
|
|
359
|
+
expected.add(resource.write_staging_directory)
|
|
360
|
+
views = {view.configured_path: view for view in authority.staging_views}
|
|
361
|
+
if len(views) != len(authority.staging_views) or set(views) != expected:
|
|
362
|
+
raise ValueError(
|
|
363
|
+
"Complete current/legacy staging view inventory is required"
|
|
364
|
+
)
|
|
365
|
+
if views["/data/.ztc-delete-quarantine"].directory != str(default):
|
|
366
|
+
raise ValueError(
|
|
367
|
+
"Default staging view conflicts with the authority inventory"
|
|
368
|
+
)
|
|
369
|
+
roots = {
|
|
370
|
+
resource.id: directory(resource.root_directory)
|
|
371
|
+
for resource in authority.resources
|
|
372
|
+
}
|
|
373
|
+
for configured, view in sorted(views.items()):
|
|
374
|
+
if (
|
|
375
|
+
not os.path.isabs(configured)
|
|
376
|
+
or os.path.normpath(configured) != configured
|
|
377
|
+
or "\x00" in configured
|
|
378
|
+
):
|
|
379
|
+
raise ValueError(
|
|
380
|
+
"Configured staging paths must be canonical absolute paths"
|
|
381
|
+
)
|
|
382
|
+
location = directory(view.directory)
|
|
383
|
+
info = location.stat()
|
|
384
|
+
if info.st_mode & 0o077:
|
|
385
|
+
raise ValueError("Staging views must have private permissions")
|
|
386
|
+
if any(
|
|
387
|
+
Path(configured).is_relative_to(resource.root)
|
|
388
|
+
or Path(resource.root).is_relative_to(configured)
|
|
389
|
+
for resource in policy.resources
|
|
390
|
+
):
|
|
391
|
+
raise ValueError(
|
|
392
|
+
"Private staging overlaps an original resource policy path"
|
|
393
|
+
)
|
|
394
|
+
if any(
|
|
395
|
+
location.is_relative_to(root)
|
|
396
|
+
or root.is_relative_to(location)
|
|
397
|
+
or (info.st_dev, info.st_ino)
|
|
398
|
+
== (root.stat().st_dev, root.stat().st_ino)
|
|
399
|
+
for root in roots.values()
|
|
400
|
+
):
|
|
401
|
+
raise ValueError("Private staging views overlap a mapped resource")
|
|
402
|
+
for resource in policy.resources:
|
|
403
|
+
if (
|
|
404
|
+
resource.write_staging_directory == configured
|
|
405
|
+
and roots[resource.id].stat().st_dev != info.st_dev
|
|
406
|
+
):
|
|
407
|
+
raise ValueError(
|
|
408
|
+
"Configured write staging view must share the resource filesystem"
|
|
409
|
+
)
|
|
410
|
+
self.staging_bindings.append(
|
|
411
|
+
{
|
|
412
|
+
"side": side,
|
|
413
|
+
"configured_path": configured,
|
|
414
|
+
"directory": str(location),
|
|
415
|
+
"identity": [info.st_dev, info.st_ino],
|
|
416
|
+
"owner_uid": info.st_uid,
|
|
417
|
+
"mode": stat.S_IMODE(info.st_mode),
|
|
418
|
+
}
|
|
419
|
+
)
|
|
420
|
+
if check_contents and any(location.iterdir()):
|
|
421
|
+
self.blockers.append(
|
|
422
|
+
side
|
|
423
|
+
+ ": retained staging objects require confirmed recovery: "
|
|
424
|
+
+ configured
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
def inspect_authority(self, side: str, authority: Authority) -> None:
|
|
428
|
+
state = directory(authority.state_directory)
|
|
429
|
+
self.inspect_staging(side, authority)
|
|
430
|
+
if read_file(Path(authority.worker_file)) != self.reference:
|
|
431
|
+
raise ValueError(
|
|
432
|
+
"Deployed worker differs from the reviewed fence-capable worker"
|
|
433
|
+
)
|
|
434
|
+
resources: dict[str, Resource] = {}
|
|
435
|
+
identities: dict[str, list[int]] = {}
|
|
436
|
+
for resource in authority.resources:
|
|
437
|
+
root = directory(resource.root_directory)
|
|
438
|
+
if (
|
|
439
|
+
not os.path.isabs(resource.configured_root)
|
|
440
|
+
or os.path.normpath(resource.configured_root)
|
|
441
|
+
!= resource.configured_root
|
|
442
|
+
):
|
|
443
|
+
raise ValueError("Configured roots must be canonical absolute paths")
|
|
444
|
+
if resource.id in resources or resource.configured_root in identities:
|
|
445
|
+
raise ValueError("Authority resource IDs and roots must be unique")
|
|
446
|
+
info = root.stat()
|
|
447
|
+
identity = [info.st_dev, info.st_ino]
|
|
448
|
+
resources[resource.id] = resource
|
|
449
|
+
identities[resource.configured_root] = identity
|
|
450
|
+
self.bindings.append(
|
|
451
|
+
{"side": side, **resource.model_dump(), "root_identity": identity}
|
|
452
|
+
)
|
|
453
|
+
lease_data = json_data(
|
|
454
|
+
self.capture(state / "leases.json", side + "/leases.json")
|
|
455
|
+
)
|
|
456
|
+
if not isinstance(lease_data, dict):
|
|
457
|
+
raise ValueError("Invalid lease registry evidence")
|
|
458
|
+
for key, value in lease_data.items():
|
|
459
|
+
lease = Lease.model_validate(value)
|
|
460
|
+
if (
|
|
461
|
+
key != digest(lease.root.encode())
|
|
462
|
+
or identities.get(lease.root) != lease.identity
|
|
463
|
+
):
|
|
464
|
+
raise ValueError(
|
|
465
|
+
"Lease root or physical identity lacks reviewed mapping"
|
|
466
|
+
)
|
|
467
|
+
# Explicit stop is required even for expired owners; expiry is not quiescence.
|
|
468
|
+
if lease.owner_id is not None:
|
|
469
|
+
self.blockers.append(
|
|
470
|
+
side + ": stop every recorded lease before cutover"
|
|
471
|
+
)
|
|
472
|
+
owners = json_data(self.capture(state / "owners.json", side + "/owners.json"))
|
|
473
|
+
if not isinstance(owners, dict) or any(
|
|
474
|
+
type(v) not in [int, float] for v in owners.values()
|
|
475
|
+
):
|
|
476
|
+
raise ValueError("Invalid owner-presence evidence")
|
|
477
|
+
canonical(owners) # Reject nonfinite presence timestamps.
|
|
478
|
+
paths = entries(directory(str(state / "receipts")))
|
|
479
|
+
for path in paths:
|
|
480
|
+
receipt = Receipt.model_validate_json(
|
|
481
|
+
self.capture(path, side + "/receipts/" + path.name)
|
|
482
|
+
)
|
|
483
|
+
matched_resource = resources.get(receipt.resource_id)
|
|
484
|
+
if (
|
|
485
|
+
receipt.id != path.stem
|
|
486
|
+
or matched_resource is None
|
|
487
|
+
or receipt.endpoint != matched_resource.endpoint
|
|
488
|
+
or receipt.configured_root != matched_resource.configured_root
|
|
489
|
+
or receipt.root_identity != identities[matched_resource.configured_root]
|
|
490
|
+
):
|
|
491
|
+
raise ValueError("Receipt identity lacks its original reviewed binding")
|
|
492
|
+
expected_target = os.path.normpath(
|
|
493
|
+
os.path.join(matched_resource.configured_root, receipt.requested_path)
|
|
494
|
+
)
|
|
495
|
+
if (
|
|
496
|
+
os.path.isabs(receipt.requested_path)
|
|
497
|
+
or not receipt.requested_path
|
|
498
|
+
or "\x00" in receipt.requested_path
|
|
499
|
+
or not expected_target.startswith(
|
|
500
|
+
matched_resource.configured_root.rstrip("/") + "/"
|
|
501
|
+
)
|
|
502
|
+
or receipt.target.get("path") != expected_target
|
|
503
|
+
):
|
|
504
|
+
raise ValueError("Receipt target lacks its original reviewed binding")
|
|
505
|
+
if (
|
|
506
|
+
receipt.status in ["pending", "unverified"]
|
|
507
|
+
or not receipt.admission_resolved
|
|
508
|
+
):
|
|
509
|
+
self.blockers.append(side + ": unresolved receipt " + receipt.id)
|
|
510
|
+
self.receipts.append(receipt)
|
|
511
|
+
self.inventory.append(
|
|
512
|
+
{"side": side, "receipt_count": len(paths), "lease_count": len(lease_data)}
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
def inspect(self) -> dict[str, Any]:
|
|
516
|
+
if not self.manifest.clients_stopped:
|
|
517
|
+
self.blockers.append(
|
|
518
|
+
"Operator must stop all clients and confirm the complete client inventory"
|
|
519
|
+
)
|
|
520
|
+
for side in ["legacy", "receiver"]:
|
|
521
|
+
authority = (
|
|
522
|
+
self.manifest.legacy if side == "legacy" else self.manifest.receiver
|
|
523
|
+
)
|
|
524
|
+
self.inspect_authority(side, authority)
|
|
525
|
+
mappings = sorted(
|
|
526
|
+
[m.model_dump() for m in self.manifest.identity_mapping],
|
|
527
|
+
key=lambda m: m["legacy"],
|
|
528
|
+
)
|
|
529
|
+
for side in ["legacy", "receiver"]:
|
|
530
|
+
expected = {
|
|
531
|
+
r.id
|
|
532
|
+
for r in (
|
|
533
|
+
self.manifest.legacy if side == "legacy" else self.manifest.receiver
|
|
534
|
+
).resources
|
|
535
|
+
}
|
|
536
|
+
actual = [m[side] for m in mappings]
|
|
537
|
+
if len(set(actual)) != len(actual) or set(actual) != expected:
|
|
538
|
+
raise ValueError(
|
|
539
|
+
"Identity mapping must cover both authorities exactly once"
|
|
540
|
+
)
|
|
541
|
+
count = 0
|
|
542
|
+
journals = sorted(self.manifest.client_journals)
|
|
543
|
+
if len(set(journals)) != len(journals):
|
|
544
|
+
raise ValueError("Duplicate client journal in reviewed inventory")
|
|
545
|
+
for index, value in enumerate(journals):
|
|
546
|
+
journal = directory(value)
|
|
547
|
+
for path in entries(journal):
|
|
548
|
+
record = ClientRecord.model_validate_json(
|
|
549
|
+
self.capture(path, f"clients/{index}/" + path.name)
|
|
550
|
+
)
|
|
551
|
+
if record.id != path.stem:
|
|
552
|
+
raise ValueError("Invalid client operation identity")
|
|
553
|
+
resources = [
|
|
554
|
+
r
|
|
555
|
+
for a in [self.manifest.legacy, self.manifest.receiver]
|
|
556
|
+
for r in a.resources
|
|
557
|
+
if r.id == record.resource_id and r.endpoint == record.endpoint
|
|
558
|
+
]
|
|
559
|
+
if len(resources) != 1:
|
|
560
|
+
raise ValueError(
|
|
561
|
+
"Client endpoint lacks its original reviewed mapping"
|
|
562
|
+
)
|
|
563
|
+
expected_target = os.path.normpath(
|
|
564
|
+
os.path.join(resources[0].configured_root, record.requested_path)
|
|
565
|
+
)
|
|
566
|
+
if (
|
|
567
|
+
os.path.isabs(record.requested_path)
|
|
568
|
+
or not record.requested_path
|
|
569
|
+
or "\x00" in record.requested_path
|
|
570
|
+
or not expected_target.startswith(
|
|
571
|
+
resources[0].configured_root.rstrip("/") + "/"
|
|
572
|
+
)
|
|
573
|
+
or record.target != expected_target
|
|
574
|
+
):
|
|
575
|
+
raise ValueError(
|
|
576
|
+
"Client target lacks its original reviewed mapping"
|
|
577
|
+
)
|
|
578
|
+
count += 1
|
|
579
|
+
if record.status == "pending":
|
|
580
|
+
self.blockers.append("pending client operation " + record.id)
|
|
581
|
+
matches = [
|
|
582
|
+
r
|
|
583
|
+
for r in self.receipts
|
|
584
|
+
if r.id == record.id
|
|
585
|
+
and r.owner_id == record.owner_id
|
|
586
|
+
and r.resource_id == record.resource_id
|
|
587
|
+
and r.endpoint == record.endpoint
|
|
588
|
+
and r.operation == record.operation
|
|
589
|
+
and r.tool_call_id == record.tool_call_id
|
|
590
|
+
and r.requested_path == record.requested_path
|
|
591
|
+
and r.target.get("path") == record.target
|
|
592
|
+
]
|
|
593
|
+
if record.status == "resolved" and len(matches) != 1:
|
|
594
|
+
self.blockers.append(
|
|
595
|
+
"Original receipt missing or mismatched for client operation "
|
|
596
|
+
+ record.id
|
|
597
|
+
)
|
|
598
|
+
if record.status == "not_dispatched" and any(
|
|
599
|
+
r.id == record.id for r in self.receipts
|
|
600
|
+
):
|
|
601
|
+
self.blockers.append(
|
|
602
|
+
"No-dispatch client evidence conflicts with a recorded receipt "
|
|
603
|
+
+ record.id
|
|
604
|
+
)
|
|
605
|
+
self.inventory.append(
|
|
606
|
+
{"side": "clients", "client_count": count, "journal_count": len(journals)}
|
|
607
|
+
)
|
|
608
|
+
# Canonical mapping order is not an ownership grant. Raw journal bytes are retained.
|
|
609
|
+
plan: dict[str, Any] = {
|
|
610
|
+
"version": 1,
|
|
611
|
+
"manifest": normalized_manifest(self.manifest),
|
|
612
|
+
"bindings": sorted(self.bindings, key=lambda b: (b["side"], b["id"])),
|
|
613
|
+
"staging_bindings": sorted(
|
|
614
|
+
self.staging_bindings,
|
|
615
|
+
key=lambda view: (view["side"], view["configured_path"]),
|
|
616
|
+
),
|
|
617
|
+
"worker_sha256": digest(self.reference),
|
|
618
|
+
"files": {n: digest(d) for n, d in sorted(self.files.items())},
|
|
619
|
+
}
|
|
620
|
+
plan["manifest"]["identity_mapping"] = mappings
|
|
621
|
+
plan["manifest"]["client_journals"] = journals
|
|
622
|
+
self.files["plan.json"] = canonical(plan)
|
|
623
|
+
return {
|
|
624
|
+
"ready": not self.blockers,
|
|
625
|
+
"digest": digest(self.files["plan.json"]),
|
|
626
|
+
"blockers": self.blockers,
|
|
627
|
+
"identity_mapping": mappings,
|
|
628
|
+
"bindings": self.bindings,
|
|
629
|
+
"inventory": self.inventory,
|
|
630
|
+
"staging_bindings": sorted(
|
|
631
|
+
self.staging_bindings,
|
|
632
|
+
key=lambda view: (view["side"], view["configured_path"]),
|
|
633
|
+
),
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def execute(
|
|
638
|
+
operation: str, manifest: Manifest, reference: Path, confirmation: str | None
|
|
639
|
+
) -> dict[str, Any]:
|
|
640
|
+
with locked(manifest):
|
|
641
|
+
if operation != "preview" and (
|
|
642
|
+
confirmation is None or re.fullmatch(r"[a-f0-9]{64}", confirmation) is None
|
|
643
|
+
):
|
|
644
|
+
raise ValueError("Explicit preview digest confirmation is required")
|
|
645
|
+
legacy = Path(manifest.legacy.state_directory)
|
|
646
|
+
receiver = Path(manifest.receiver.state_directory)
|
|
647
|
+
fence = canonical({"version": 1, "digest": confirmation})
|
|
648
|
+
archive = receiver / "cutovers" / str(confirmation)
|
|
649
|
+
# Lost activation acknowledgement: do not freeze an already active receiver again.
|
|
650
|
+
if operation == "activate" and not os.path.lexists(receiver / FENCE):
|
|
651
|
+
# Settle a visible unlink left by a lost response before acknowledging it.
|
|
652
|
+
sync_directory(receiver)
|
|
653
|
+
completed = json_data(read_file(archive / "completed.json"))
|
|
654
|
+
plan = json_data(read_file(archive / "plan.json"))
|
|
655
|
+
if (
|
|
656
|
+
completed
|
|
657
|
+
!= {"version": 1, "digest": confirmation, "state": "activated"}
|
|
658
|
+
or read_file(legacy / FENCE) != fence
|
|
659
|
+
or digest(read_file(archive / "plan.json")) != confirmation
|
|
660
|
+
or plan["manifest"] != normalized_manifest(manifest)
|
|
661
|
+
):
|
|
662
|
+
raise ValueError(
|
|
663
|
+
"Activation recovery evidence does not match the confirmed mapping"
|
|
664
|
+
)
|
|
665
|
+
for name, expected in plan["files"].items():
|
|
666
|
+
if digest(read_file(archive / name)) != expected:
|
|
667
|
+
raise ValueError(
|
|
668
|
+
"Archived evidence damaged; preserve it for operator recovery"
|
|
669
|
+
)
|
|
670
|
+
current = Survey(manifest, reference)
|
|
671
|
+
for side, authority in [
|
|
672
|
+
("legacy", manifest.legacy),
|
|
673
|
+
("receiver", manifest.receiver),
|
|
674
|
+
]:
|
|
675
|
+
current.inspect_staging(side, authority, check_contents=False)
|
|
676
|
+
if current.blockers or sorted(
|
|
677
|
+
current.staging_bindings,
|
|
678
|
+
key=lambda view: (view["side"], view["configured_path"]),
|
|
679
|
+
) != plan.get("staging_bindings"):
|
|
680
|
+
raise ValueError(
|
|
681
|
+
"Activation recovery staging identity or policy inventory changed"
|
|
682
|
+
)
|
|
683
|
+
for name, data in current.files.items():
|
|
684
|
+
if read_file(archive / name) != data:
|
|
685
|
+
raise ValueError("Activation recovery resource policy changed")
|
|
686
|
+
return {"state": "activated", "archive": str(archive)}
|
|
687
|
+
survey = Survey(manifest, reference)
|
|
688
|
+
preview = survey.inspect()
|
|
689
|
+
if operation == "preview":
|
|
690
|
+
return {"preview": preview}
|
|
691
|
+
if confirmation != preview["digest"]:
|
|
692
|
+
raise ValueError(
|
|
693
|
+
"Preview confirmation changed; review current evidence and mapping"
|
|
694
|
+
)
|
|
695
|
+
if not preview["ready"]:
|
|
696
|
+
raise ValueError("Cutover blocked: " + "; ".join(preview["blockers"]))
|
|
697
|
+
if operation == "apply":
|
|
698
|
+
# Freeze the destination first. Every interruption leaves at least
|
|
699
|
+
# that authority closed; neither operation copies or resets leases.
|
|
700
|
+
for state in [receiver, legacy]:
|
|
701
|
+
publish(state / FENCE, fence)
|
|
702
|
+
make_directory(archive)
|
|
703
|
+
for name, data in sorted(survey.files.items()):
|
|
704
|
+
target = archive / name
|
|
705
|
+
make_directory(target.parent)
|
|
706
|
+
publish(target, data)
|
|
707
|
+
publish(
|
|
708
|
+
archive / "prepared.json",
|
|
709
|
+
canonical({"version": 1, "digest": confirmation, "state": "frozen"}),
|
|
710
|
+
)
|
|
711
|
+
return {"state": "frozen", "archive": str(archive)}
|
|
712
|
+
for state in [legacy, receiver]:
|
|
713
|
+
if read_file(state / FENCE) != fence:
|
|
714
|
+
raise ValueError(
|
|
715
|
+
"Cutover fence conflicts; explicit operator recovery required"
|
|
716
|
+
)
|
|
717
|
+
if json_data(read_file(archive / "prepared.json")) != {
|
|
718
|
+
"version": 1,
|
|
719
|
+
"digest": confirmation,
|
|
720
|
+
"state": "frozen",
|
|
721
|
+
}:
|
|
722
|
+
raise ValueError("Cutover archive has no complete preparation receipt")
|
|
723
|
+
for name, data in survey.files.items():
|
|
724
|
+
if read_file(archive / name) != data:
|
|
725
|
+
raise ValueError(
|
|
726
|
+
"Cutover archive differs from original evidence; keep both authorities frozen"
|
|
727
|
+
)
|
|
728
|
+
publish(
|
|
729
|
+
archive / "completed.json",
|
|
730
|
+
canonical({"version": 1, "digest": confirmation, "state": "activated"}),
|
|
731
|
+
)
|
|
732
|
+
os.unlink(receiver / FENCE)
|
|
733
|
+
sync_directory(receiver)
|
|
734
|
+
return {"state": "activated", "archive": str(archive)}
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def main() -> None:
|
|
738
|
+
parser = argparse.ArgumentParser(
|
|
739
|
+
description="Preview and confirm remote journal cutover"
|
|
740
|
+
)
|
|
741
|
+
parser.add_argument("operation", choices=["preview", "apply", "activate"])
|
|
742
|
+
parser.add_argument("--manifest", required=True)
|
|
743
|
+
parser.add_argument("--confirm")
|
|
744
|
+
parser.add_argument("--worker-file")
|
|
745
|
+
args = parser.parse_args()
|
|
746
|
+
try:
|
|
747
|
+
manifest = Manifest.model_validate_json(read_file(Path(args.manifest)))
|
|
748
|
+
reference = (
|
|
749
|
+
Path(args.worker_file)
|
|
750
|
+
if args.worker_file
|
|
751
|
+
else Path(__file__).with_name("worker.py")
|
|
752
|
+
)
|
|
753
|
+
result = execute(args.operation, manifest, reference, args.confirm)
|
|
754
|
+
print(json.dumps({"ok": True, **result}, allow_nan=False))
|
|
755
|
+
except (ValueError, OSError) as error:
|
|
756
|
+
print(json.dumps({"ok": False, "error": str(error)}))
|
|
757
|
+
raise SystemExit(2) from error
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
if __name__ == "__main__":
|
|
761
|
+
main()
|