iCode-TUI 0.29.1__py3-none-any.whl
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.
- chrys/__init__.py +16 -0
- chrys/app/__init__.py +3 -0
- chrys/app/acp/__init__.py +3 -0
- chrys/app/acp/bridge.py +664 -0
- chrys/app/acp/content.py +132 -0
- chrys/app/acp/doc/frontend-api.md +425 -0
- chrys/app/acp/history.py +585 -0
- chrys/app/acp/server.py +1796 -0
- chrys/app/acp/session_manager.py +1815 -0
- chrys/app/acp/tool_status.py +45 -0
- chrys/app/cli/__init__.py +3 -0
- chrys/app/cli/acp.py +178 -0
- chrys/app/cli/app.py +165 -0
- chrys/app/cli/approvals.py +101 -0
- chrys/app/cli/headless.py +223 -0
- chrys/app/cli/launch_cwd.py +42 -0
- chrys/app/cli/profiles.py +298 -0
- chrys/app/cli/progress.py +857 -0
- chrys/app/cli/run.py +425 -0
- chrys/app/cli/serve.py +1593 -0
- chrys/app/cli/tool_summary.py +225 -0
- chrys/app/cli/trajectory.py +201 -0
- chrys/app/cli/workflow.py +601 -0
- chrys/app/features/__init__.py +3 -0
- chrys/app/features/buddy/__init__.py +3 -0
- chrys/app/features/buddy/actions.py +68 -0
- chrys/app/features/buddy/animation.py +76 -0
- chrys/app/features/buddy/commands.py +196 -0
- chrys/app/features/buddy/docs/design.md +187 -0
- chrys/app/features/buddy/hatchery.py +80 -0
- chrys/app/features/buddy/lifecycle.py +41 -0
- chrys/app/features/buddy/model.py +265 -0
- chrys/app/features/buddy/pixel_renderer.py +113 -0
- chrys/app/features/buddy/pixel_sprites.py +279 -0
- chrys/app/features/buddy/portrait.py +76 -0
- chrys/app/features/buddy/progression.py +58 -0
- chrys/app/features/buddy/replies.py +191 -0
- chrys/app/features/buddy/sprites/alien.toml +70 -0
- chrys/app/features/buddy/sprites/axolotl.toml +71 -0
- chrys/app/features/buddy/sprites/bat.toml +69 -0
- chrys/app/features/buddy/sprites/bee.toml +69 -0
- chrys/app/features/buddy/sprites/cactus.toml +73 -0
- chrys/app/features/buddy/sprites/capybara.toml +70 -0
- chrys/app/features/buddy/sprites/cat.toml +69 -0
- chrys/app/features/buddy/sprites/crab.toml +70 -0
- chrys/app/features/buddy/sprites/dragon.toml +71 -0
- chrys/app/features/buddy/sprites/duck.toml +69 -0
- chrys/app/features/buddy/sprites/fox.toml +69 -0
- chrys/app/features/buddy/sprites/frog.toml +70 -0
- chrys/app/features/buddy/sprites/ghost.toml +70 -0
- chrys/app/features/buddy/sprites/goose.toml +69 -0
- chrys/app/features/buddy/sprites/jelly.toml +70 -0
- chrys/app/features/buddy/sprites/llama.toml +70 -0
- chrys/app/features/buddy/sprites/mushroom.toml +69 -0
- chrys/app/features/buddy/sprites/octopus.toml +70 -0
- chrys/app/features/buddy/sprites/owl.toml +70 -0
- chrys/app/features/buddy/sprites/panda.toml +71 -0
- chrys/app/features/buddy/sprites/penguin.toml +69 -0
- chrys/app/features/buddy/sprites/rabbit.toml +70 -0
- chrys/app/features/buddy/sprites/robot.toml +72 -0
- chrys/app/features/buddy/sprites/shark.toml +70 -0
- chrys/app/features/buddy/sprites/snail.toml +69 -0
- chrys/app/features/buddy/sprites/snake.toml +69 -0
- chrys/app/features/buddy/sprites/tabby.toml +69 -0
- chrys/app/features/buddy/sprites/turtle.toml +69 -0
- chrys/app/features/buddy/store.py +143 -0
- chrys/app/features/session_title/__init__.py +7 -0
- chrys/app/features/session_title/updater.py +582 -0
- chrys/app/installer.py +651 -0
- chrys/app/parsing.py +17 -0
- chrys/app/tui/__init__.py +3 -0
- chrys/app/tui/app.py +1287 -0
- chrys/app/tui/behaviors/__init__.py +3 -0
- chrys/app/tui/behaviors/click_outside_dismiss.py +90 -0
- chrys/app/tui/behaviors/insert_clipboard.py +73 -0
- chrys/app/tui/behaviors/right_click_copy.py +146 -0
- chrys/app/tui/binding_display.py +139 -0
- chrys/app/tui/buddy_messages.py +17 -0
- chrys/app/tui/buddy_reply.py +86 -0
- chrys/app/tui/chrys.tcss +549 -0
- chrys/app/tui/clipboard.py +109 -0
- chrys/app/tui/copy_messages.py +10 -0
- chrys/app/tui/i18n.py +230 -0
- chrys/app/tui/language.py +58 -0
- chrys/app/tui/notifications/__init__.py +12 -0
- chrys/app/tui/notifications/drivers.py +180 -0
- chrys/app/tui/notifications/service.py +126 -0
- chrys/app/tui/notifications/settings.py +141 -0
- chrys/app/tui/screens/__init__.py +3 -0
- chrys/app/tui/screens/agents/__init__.py +8 -0
- chrys/app/tui/screens/agents/agent_draft_store.py +937 -0
- chrys/app/tui/screens/agents/config.py +2142 -0
- chrys/app/tui/screens/agents/config.tcss +169 -0
- chrys/app/tui/screens/agents/panels/__init__.py +3 -0
- chrys/app/tui/screens/agents/panels/acp.py +1176 -0
- chrys/app/tui/screens/agents/panels/basic.py +497 -0
- chrys/app/tui/screens/agents/panels/compaction.py +231 -0
- chrys/app/tui/screens/agents/panels/config_card.py +218 -0
- chrys/app/tui/screens/agents/panels/instructions.py +91 -0
- chrys/app/tui/screens/agents/panels/markdown_report.py +76 -0
- chrys/app/tui/screens/agents/panels/mcp.py +2262 -0
- chrys/app/tui/screens/agents/panels/memory.py +684 -0
- chrys/app/tui/screens/agents/panels/path_entry.py +227 -0
- chrys/app/tui/screens/agents/panels/skills.py +806 -0
- chrys/app/tui/screens/agents/panels/subagents.py +652 -0
- chrys/app/tui/screens/agents/panels/tools.py +205 -0
- chrys/app/tui/screens/agents/picker.py +103 -0
- chrys/app/tui/screens/agents/skill_paths.py +38 -0
- chrys/app/tui/screens/agents/validation_messages.py +452 -0
- chrys/app/tui/screens/dialogs/__init__.py +35 -0
- chrys/app/tui/screens/dialogs/agent_load.py +834 -0
- chrys/app/tui/screens/dialogs/agent_load.tcss +78 -0
- chrys/app/tui/screens/dialogs/app_mode.py +55 -0
- chrys/app/tui/screens/dialogs/approval/__init__.py +15 -0
- chrys/app/tui/screens/dialogs/approval/approval.tcss +311 -0
- chrys/app/tui/screens/dialogs/approval/bodies/__init__.py +3 -0
- chrys/app/tui/screens/dialogs/approval/bodies/file_edit.py +210 -0
- chrys/app/tui/screens/dialogs/approval/bodies/sub_agent.py +82 -0
- chrys/app/tui/screens/dialogs/approval/body.py +110 -0
- chrys/app/tui/screens/dialogs/approval/dialog.py +565 -0
- chrys/app/tui/screens/dialogs/approval/mode.py +105 -0
- chrys/app/tui/screens/dialogs/ask_user.py +148 -0
- chrys/app/tui/screens/dialogs/ask_user.tcss +62 -0
- chrys/app/tui/screens/dialogs/base.py +84 -0
- chrys/app/tui/screens/dialogs/confirm.py +184 -0
- chrys/app/tui/screens/dialogs/confirm.tcss +46 -0
- chrys/app/tui/screens/dialogs/connection_test.py +183 -0
- chrys/app/tui/screens/dialogs/connection_test.tcss +92 -0
- chrys/app/tui/screens/dialogs/diagram.py +102 -0
- chrys/app/tui/screens/dialogs/diagram.tcss +24 -0
- chrys/app/tui/screens/dialogs/editor.py +378 -0
- chrys/app/tui/screens/dialogs/editor.tcss +132 -0
- chrys/app/tui/screens/dialogs/file_picker.py +582 -0
- chrys/app/tui/screens/dialogs/file_picker.tcss +78 -0
- chrys/app/tui/screens/dialogs/fork_session.py +219 -0
- chrys/app/tui/screens/dialogs/fork_session.tcss +75 -0
- chrys/app/tui/screens/dialogs/image_compression.py +102 -0
- chrys/app/tui/screens/dialogs/image_compression.tcss +28 -0
- chrys/app/tui/screens/dialogs/man_page.py +172 -0
- chrys/app/tui/screens/dialogs/man_page.tcss +39 -0
- chrys/app/tui/screens/dialogs/new_folder.py +277 -0
- chrys/app/tui/screens/dialogs/new_folder.tcss +51 -0
- chrys/app/tui/screens/dialogs/notice.tcss +14 -0
- chrys/app/tui/screens/dialogs/prompt_cache.py +144 -0
- chrys/app/tui/screens/dialogs/prompt_cache.tcss +38 -0
- chrys/app/tui/screens/dialogs/runtime_details.py +371 -0
- chrys/app/tui/screens/dialogs/runtime_details.tcss +49 -0
- chrys/app/tui/screens/dialogs/session_title.py +110 -0
- chrys/app/tui/screens/dialogs/session_title.tcss +43 -0
- chrys/app/tui/screens/dialogs/tool_view.py +207 -0
- chrys/app/tui/screens/dialogs/tool_view.tcss +43 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.py +122 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.tcss +35 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.py +91 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.tcss +17 -0
- chrys/app/tui/screens/dialogs/workflow_input.py +258 -0
- chrys/app/tui/screens/dialogs/workflow_input.tcss +73 -0
- chrys/app/tui/screens/dialogs/workflow_node.py +462 -0
- chrys/app/tui/screens/dialogs/workflow_node.tcss +41 -0
- chrys/app/tui/screens/dialogs/workflow_picker.py +46 -0
- chrys/app/tui/screens/dialogs/workflow_result.py +136 -0
- chrys/app/tui/screens/dialogs/workflow_result.tcss +14 -0
- chrys/app/tui/screens/diff/__init__.py +22 -0
- chrys/app/tui/screens/diff/rollback_modal.py +1381 -0
- chrys/app/tui/screens/diff/rollback_modal.tcss +248 -0
- chrys/app/tui/screens/diff/screen.py +1532 -0
- chrys/app/tui/screens/diff/screen.tcss +104 -0
- chrys/app/tui/screens/guides/__init__.py +7 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/en/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/en/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/en/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/en/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/en/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/en/reference/workflows.md +1006 -0
- chrys/app/tui/screens/guides/_docs/en/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/en/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/_docs/index.yaml +100 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/workflows.md +1005 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/index.py +215 -0
- chrys/app/tui/screens/guides/reader.py +173 -0
- chrys/app/tui/screens/guides/screen.py +442 -0
- chrys/app/tui/screens/guides/screen.tcss +52 -0
- chrys/app/tui/screens/logs/__init__.py +7 -0
- chrys/app/tui/screens/logs/screen.py +504 -0
- chrys/app/tui/screens/logs/screen.tcss +45 -0
- chrys/app/tui/screens/main/__init__.py +7 -0
- chrys/app/tui/screens/main/action_availability.py +30 -0
- chrys/app/tui/screens/main/buddy_command.py +143 -0
- chrys/app/tui/screens/main/commands.py +1073 -0
- chrys/app/tui/screens/main/config_actions.py +464 -0
- chrys/app/tui/screens/main/copy_actions.py +117 -0
- chrys/app/tui/screens/main/dialog_controllers.py +826 -0
- chrys/app/tui/screens/main/dialog_gateway.py +166 -0
- chrys/app/tui/screens/main/diff_controller.py +478 -0
- chrys/app/tui/screens/main/engine_read_model.py +184 -0
- chrys/app/tui/screens/main/event_handlers.py +2014 -0
- chrys/app/tui/screens/main/input_flow.py +241 -0
- chrys/app/tui/screens/main/live_diff.py +244 -0
- chrys/app/tui/screens/main/main_screen_presenter.py +295 -0
- chrys/app/tui/screens/main/model_indicator.py +217 -0
- chrys/app/tui/screens/main/navigation.py +343 -0
- chrys/app/tui/screens/main/ports.py +571 -0
- chrys/app/tui/screens/main/recent_dirs.py +123 -0
- chrys/app/tui/screens/main/rollback_controller.py +586 -0
- chrys/app/tui/screens/main/runtime_info.py +71 -0
- chrys/app/tui/screens/main/screen.py +2265 -0
- chrys/app/tui/screens/main/screen.tcss +21 -0
- chrys/app/tui/screens/main/session_handlers.py +1127 -0
- chrys/app/tui/screens/main/session_title.py +239 -0
- chrys/app/tui/screens/main/settings_coordinator.py +365 -0
- chrys/app/tui/screens/main/settings_persistence.py +184 -0
- chrys/app/tui/screens/main/shell_mode.py +112 -0
- chrys/app/tui/screens/main/state.py +289 -0
- chrys/app/tui/screens/main/submit_state.py +11 -0
- chrys/app/tui/screens/main/subscriptions.py +177 -0
- chrys/app/tui/screens/main/suggestions.py +982 -0
- chrys/app/tui/screens/main/tool_action_bridge.py +75 -0
- chrys/app/tui/screens/main/view_adapter.py +1567 -0
- chrys/app/tui/screens/main/workflow_browser.py +747 -0
- chrys/app/tui/screens/main/workflow_content.py +248 -0
- chrys/app/tui/screens/main/workflow_controller.py +240 -0
- chrys/app/tui/screens/main/workflow_feedback.py +137 -0
- chrys/app/tui/screens/main/workflow_flow.py +49 -0
- chrys/app/tui/screens/main/workflow_run_control.py +456 -0
- chrys/app/tui/screens/main/workflow_session_view.py +316 -0
- chrys/app/tui/screens/main/workspace_actions.py +272 -0
- chrys/app/tui/screens/main/workspace_branch.py +194 -0
- chrys/app/tui/screens/menus/__init__.py +8 -0
- chrys/app/tui/screens/menus/languages.py +114 -0
- chrys/app/tui/screens/models/__init__.py +7 -0
- chrys/app/tui/screens/models/picker.py +147 -0
- chrys/app/tui/screens/models/screen.py +2043 -0
- chrys/app/tui/screens/models/screen.tcss +255 -0
- chrys/app/tui/screens/sessions/__init__.py +8 -0
- chrys/app/tui/screens/sessions/presenter.py +586 -0
- chrys/app/tui/screens/sessions/screen.py +779 -0
- chrys/app/tui/screens/sessions/screen.tcss +127 -0
- chrys/app/tui/screens/settings/__init__.py +29 -0
- chrys/app/tui/screens/settings/danger.py +26 -0
- chrys/app/tui/screens/settings/dialog.py +281 -0
- chrys/app/tui/screens/settings/layout.py +519 -0
- chrys/app/tui/screens/settings/migrate_dialog.py +253 -0
- chrys/app/tui/screens/settings/panes/__init__.py +3 -0
- chrys/app/tui/screens/settings/panes/notifications.py +248 -0
- chrys/app/tui/screens/settings/panes/sessions.py +148 -0
- chrys/app/tui/screens/settings/ports.py +55 -0
- chrys/app/tui/screens/settings/provenance.py +102 -0
- chrys/app/tui/screens/settings/rows.py +546 -0
- chrys/app/tui/screens/settings/settings.tcss +406 -0
- chrys/app/tui/screens/themes/__init__.py +8 -0
- chrys/app/tui/screens/themes/dialogs.py +330 -0
- chrys/app/tui/screens/themes/editor.py +632 -0
- chrys/app/tui/screens/themes/editor.tcss +268 -0
- chrys/app/tui/screens/themes/messages.py +95 -0
- chrys/app/tui/screens/themes/palette.py +694 -0
- chrys/app/tui/screens/themes/panel.py +113 -0
- chrys/app/tui/screens/themes/picker.py +203 -0
- chrys/app/tui/screens/themes/save.py +191 -0
- chrys/app/tui/support/__init__.py +3 -0
- chrys/app/tui/support/file_manager.py +64 -0
- chrys/app/tui/support/gc_freeze.py +1114 -0
- chrys/app/tui/support/path_shortcuts.py +50 -0
- chrys/app/tui/support/prompt_history.py +298 -0
- chrys/app/tui/support/workspace_mru.py +476 -0
- chrys/app/tui/terminal/__init__.py +6 -0
- chrys/app/tui/terminal/_pty_backend.py +114 -0
- chrys/app/tui/terminal/emulator/__init__.py +24 -0
- chrys/app/tui/terminal/emulator/buffer.py +456 -0
- chrys/app/tui/terminal/emulator/charsets.py +113 -0
- chrys/app/tui/terminal/emulator/core.py +963 -0
- chrys/app/tui/terminal/emulator/input.py +342 -0
- chrys/app/tui/terminal/emulator/parser.py +227 -0
- chrys/app/tui/terminal/emulator/pen.py +158 -0
- chrys/app/tui/terminal/launcher.py +455 -0
- chrys/app/tui/terminal/leaked_reports.py +44 -0
- chrys/app/tui/terminal/panel.py +323 -0
- chrys/app/tui/terminal/pty_process.py +405 -0
- chrys/app/tui/terminal/rendering.py +151 -0
- chrys/app/tui/terminal/shell.py +273 -0
- chrys/app/tui/terminal/shell_integration.py +199 -0
- chrys/app/tui/terminal/title.py +224 -0
- chrys/app/tui/terminal/widget.py +654 -0
- chrys/app/tui/theme.py +422 -0
- chrys/app/tui/theme_loader.py +201 -0
- chrys/app/tui/theme_stylesheet.py +49 -0
- chrys/app/tui/themes/__init__.py +3 -0
- chrys/app/tui/themes/document.py +120 -0
- chrys/app/tui/themes/preview.py +170 -0
- chrys/app/tui/themes/store.py +176 -0
- chrys/app/tui/toasts.py +69 -0
- chrys/app/tui/util/__init__.py +3 -0
- chrys/app/tui/util/context_pressure.py +146 -0
- chrys/app/tui/util/debug_log.py +292 -0
- chrys/app/tui/util/diff_entries.py +64 -0
- chrys/app/tui/util/formatting.py +53 -0
- chrys/app/tui/util/git_branch.py +326 -0
- chrys/app/tui/util/invocation_progress.py +57 -0
- chrys/app/tui/util/logo.py +32 -0
- chrys/app/tui/util/message_gate.py +35 -0
- chrys/app/tui/util/mutation_sources.py +14 -0
- chrys/app/tui/util/picker_text.py +27 -0
- chrys/app/tui/util/removal.py +47 -0
- chrys/app/tui/util/rich_style.py +36 -0
- chrys/app/tui/util/source_text.py +12 -0
- chrys/app/tui/util/static_update.py +88 -0
- chrys/app/tui/util/todo_format.py +51 -0
- chrys/app/tui/util/visibility.py +214 -0
- chrys/app/tui/widgets/__init__.py +60 -0
- chrys/app/tui/widgets/ask_user_controls.py +652 -0
- chrys/app/tui/widgets/ask_user_markdown.py +17 -0
- chrys/app/tui/widgets/ask_user_prompt.py +606 -0
- chrys/app/tui/widgets/buttons.py +52 -0
- chrys/app/tui/widgets/chat/__init__.py +3 -0
- chrys/app/tui/widgets/chat/agent_transcript_surface.py +818 -0
- chrys/app/tui/widgets/chat/chrome.py +296 -0
- chrys/app/tui/widgets/chat/compaction_card.py +353 -0
- chrys/app/tui/widgets/chat/context_fold.py +77 -0
- chrys/app/tui/widgets/chat/file_snapshot.py +91 -0
- chrys/app/tui/widgets/chat/image_preview.py +214 -0
- chrys/app/tui/widgets/chat/live_renderer.py +383 -0
- chrys/app/tui/widgets/chat/messages.py +1039 -0
- chrys/app/tui/widgets/chat/panel.py +1769 -0
- chrys/app/tui/widgets/chat/ports.py +245 -0
- chrys/app/tui/widgets/chat/renderers/__init__.py +7 -0
- chrys/app/tui/widgets/chat/renderers/ask_user.py +747 -0
- chrys/app/tui/widgets/chat/renderers/execute.py +431 -0
- chrys/app/tui/widgets/chat/renderers/file_edit.py +925 -0
- chrys/app/tui/widgets/chat/renderers/hosted_code.py +83 -0
- chrys/app/tui/widgets/chat/renderers/hosted_generic.py +330 -0
- chrys/app/tui/widgets/chat/renderers/hosted_image.py +107 -0
- chrys/app/tui/widgets/chat/renderers/hosted_mcp.py +43 -0
- chrys/app/tui/widgets/chat/renderers/hosted_search.py +167 -0
- chrys/app/tui/widgets/chat/renderers/hosted_shell.py +132 -0
- chrys/app/tui/widgets/chat/renderers/read_file.py +399 -0
- chrys/app/tui/widgets/chat/renderers/search.py +348 -0
- chrys/app/tui/widgets/chat/renderers/skill.py +564 -0
- chrys/app/tui/widgets/chat/renderers/sleep.py +287 -0
- chrys/app/tui/widgets/chat/renderers/sub_agent.py +1385 -0
- chrys/app/tui/widgets/chat/renderers/todo.py +114 -0
- chrys/app/tui/widgets/chat/renderers/web.py +69 -0
- chrys/app/tui/widgets/chat/replay.py +1605 -0
- chrys/app/tui/widgets/chat/replay_mount.py +322 -0
- chrys/app/tui/widgets/chat/scroll_controller.py +579 -0
- chrys/app/tui/widgets/chat/selection_controller.py +435 -0
- chrys/app/tui/widgets/chat/session_json.py +508 -0
- chrys/app/tui/widgets/chat/toc_model.py +156 -0
- chrys/app/tui/widgets/chat/tool_call.py +2478 -0
- chrys/app/tui/widgets/chat/tool_registry.py +753 -0
- chrys/app/tui/widgets/chat/tool_renderers.py +247 -0
- chrys/app/tui/widgets/chat/tool_view_builders.py +260 -0
- chrys/app/tui/widgets/chat/transcript_reader.py +57 -0
- chrys/app/tui/widgets/checkbox.py +24 -0
- chrys/app/tui/widgets/chrome/__init__.py +3 -0
- chrys/app/tui/widgets/chrome/app_header.py +283 -0
- chrys/app/tui/widgets/chrome/commands.py +95 -0
- chrys/app/tui/widgets/chrome/file_index.py +68 -0
- chrys/app/tui/widgets/chrome/file_scanner.py +670 -0
- chrys/app/tui/widgets/chrome/footer.py +284 -0
- chrys/app/tui/widgets/chrome/image_paste.py +351 -0
- chrys/app/tui/widgets/chrome/input_bar.py +1299 -0
- chrys/app/tui/widgets/chrome/status_bar.py +911 -0
- chrys/app/tui/widgets/chrome/suggestion_list.py +652 -0
- chrys/app/tui/widgets/click_affordance.py +24 -0
- chrys/app/tui/widgets/color_picker/__init__.py +254 -0
- chrys/app/tui/widgets/color_picker/controls.py +171 -0
- chrys/app/tui/widgets/color_picker/model.py +89 -0
- chrys/app/tui/widgets/dialog_buttons.py +58 -0
- chrys/app/tui/widgets/dialog_scroll.py +24 -0
- chrys/app/tui/widgets/diff_view/__init__.py +20 -0
- chrys/app/tui/widgets/diff_view/cells.py +59 -0
- chrys/app/tui/widgets/diff_view/code.py +280 -0
- chrys/app/tui/widgets/diff_view/compute.py +286 -0
- chrys/app/tui/widgets/diff_view/gutter.py +161 -0
- chrys/app/tui/widgets/diff_view/inline.py +77 -0
- chrys/app/tui/widgets/diff_view/palette.py +87 -0
- chrys/app/tui/widgets/diff_view/protocols.py +24 -0
- chrys/app/tui/widgets/diff_view/rows.py +138 -0
- chrys/app/tui/widgets/diff_view/unified.py +267 -0
- chrys/app/tui/widgets/diff_view/widget.py +209 -0
- chrys/app/tui/widgets/editor/__init__.py +79 -0
- chrys/app/tui/widgets/editor/coordinates.py +30 -0
- chrys/app/tui/widgets/editor/highlighter.py +139 -0
- chrys/app/tui/widgets/editor/keymaps.py +357 -0
- chrys/app/tui/widgets/editor/types.py +216 -0
- chrys/app/tui/widgets/editor/vim.py +1195 -0
- chrys/app/tui/widgets/editor/widget.py +401 -0
- chrys/app/tui/widgets/file_preview.py +58 -0
- chrys/app/tui/widgets/hatch.py +138 -0
- chrys/app/tui/widgets/input.py +66 -0
- chrys/app/tui/widgets/loading.py +125 -0
- chrys/app/tui/widgets/markdown/__init__.py +25 -0
- chrys/app/tui/widgets/markdown/_utils.py +21 -0
- chrys/app/tui/widgets/markdown/anchors.py +71 -0
- chrys/app/tui/widgets/markdown/blocks.py +115 -0
- chrys/app/tui/widgets/markdown/copyable.py +163 -0
- chrys/app/tui/widgets/markdown/diagram/__init__.py +35 -0
- chrys/app/tui/widgets/markdown/diagram/canvas.py +409 -0
- chrys/app/tui/widgets/markdown/diagram/charts.py +52 -0
- chrys/app/tui/widgets/markdown/diagram/diagnostics.py +101 -0
- chrys/app/tui/widgets/markdown/diagram/geometry.py +46 -0
- chrys/app/tui/widgets/markdown/diagram/layout.py +494 -0
- chrys/app/tui/widgets/markdown/diagram/messages.py +18 -0
- chrys/app/tui/widgets/markdown/diagram/model.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/parser.py +173 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/charts.py +349 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/common.py +403 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/graphs.py +1072 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/planning.py +317 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schedule.py +298 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schemas.py +241 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/structure.py +221 -0
- chrys/app/tui/widgets/markdown/diagram/presentation.py +32 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/charts.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/common.py +73 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/planning.py +157 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/schedule.py +202 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/structure.py +77 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/xy.py +385 -0
- chrys/app/tui/widgets/markdown/diagram/router.py +651 -0
- chrys/app/tui/widgets/markdown/diagram/specs/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/specs/charts.py +94 -0
- chrys/app/tui/widgets/markdown/diagram/specs/planning.py +68 -0
- chrys/app/tui/widgets/markdown/diagram/specs/schedule.py +65 -0
- chrys/app/tui/widgets/markdown/diagram/specs/structure.py +23 -0
- chrys/app/tui/widgets/markdown/diagram/viewer.py +92 -0
- chrys/app/tui/widgets/markdown/links.py +47 -0
- chrys/app/tui/widgets/markdown/math/__init__.py +36 -0
- chrys/app/tui/widgets/markdown/math/layout.py +739 -0
- chrys/app/tui/widgets/markdown/math/markdown.py +291 -0
- chrys/app/tui/widgets/markdown/math/parser.py +325 -0
- chrys/app/tui/widgets/markdown/math/symbols.py +325 -0
- chrys/app/tui/widgets/markdown/navigator.py +80 -0
- chrys/app/tui/widgets/markdown/parser.py +1057 -0
- chrys/app/tui/widgets/markdown/stream.py +73 -0
- chrys/app/tui/widgets/markdown/toc.py +98 -0
- chrys/app/tui/widgets/markdown/viewer.py +146 -0
- chrys/app/tui/widgets/markdown/widget.py +2046 -0
- chrys/app/tui/widgets/marquee.py +101 -0
- chrys/app/tui/widgets/option_menu.py +82 -0
- chrys/app/tui/widgets/page_navigator.py +118 -0
- chrys/app/tui/widgets/select.py +196 -0
- chrys/app/tui/widgets/selection.py +59 -0
- chrys/app/tui/widgets/sidebar/__init__.py +3 -0
- chrys/app/tui/widgets/sidebar/buddy.py +367 -0
- chrys/app/tui/widgets/sidebar/context.py +482 -0
- chrys/app/tui/widgets/sidebar/debug.py +195 -0
- chrys/app/tui/widgets/sidebar/empty_state.py +11 -0
- chrys/app/tui/widgets/sidebar/panel.py +198 -0
- chrys/app/tui/widgets/sidebar/tasks.py +198 -0
- chrys/app/tui/widgets/sidebar/toc.py +166 -0
- chrys/app/tui/widgets/sidebar/toc_types.py +30 -0
- chrys/app/tui/widgets/syntax_theme.py +85 -0
- chrys/app/tui/widgets/text_area.py +258 -0
- chrys/app/tui/widgets/trajectory/__init__.py +8 -0
- chrys/app/tui/widgets/trajectory/chartkit.py +264 -0
- chrys/app/tui/widgets/trajectory/insights.py +1266 -0
- chrys/app/tui/widgets/trajectory/overview.py +1101 -0
- chrys/app/tui/widgets/trajectory/panel.py +983 -0
- chrys/app/tui/widgets/trajectory/presentation.py +311 -0
- chrys/app/tui/widgets/trajectory/session_info.py +124 -0
- chrys/app/tui/widgets/trajectory/text_view.py +124 -0
- chrys/app/tui/widgets/trajectory/timeline.py +380 -0
- chrys/app/tui/widgets/welcome.py +126 -0
- chrys/app/tui/widgets/workflow/__init__.py +5 -0
- chrys/app/tui/widgets/workflow/edges.py +89 -0
- chrys/app/tui/widgets/workflow/graph.py +869 -0
- chrys/app/tui/widgets/workflow/info.py +310 -0
- chrys/app/tui/widgets/workflow/node_view.py +27 -0
- chrys/app/tui/widgets/workflow/output.py +225 -0
- chrys/app/tui/widgets/workflow/panel.py +658 -0
- chrys/app/tui/widgets/workflow/projector.py +324 -0
- chrys/app/tui/widgets/workflow/records.py +69 -0
- chrys/app/tui/widgets/workflow/scrollbar.py +82 -0
- chrys/app/tui/widgets/workflow/selection.py +151 -0
- chrys/app/tui/widgets/workflow/source.py +65 -0
- chrys/app/tui/widgets/workflow/text.py +257 -0
- chrys/app/tui/widgets/workflow/values.py +317 -0
- chrys/foundation/__init__.py +0 -0
- chrys/foundation/branding.py +15 -0
- chrys/foundation/config/__init__.py +7 -0
- chrys/foundation/config/coercion.py +374 -0
- chrys/foundation/config/context.py +34 -0
- chrys/foundation/config/env_file.py +141 -0
- chrys/foundation/config/env_layers.py +370 -0
- chrys/foundation/config/migrations.py +521 -0
- chrys/foundation/config/notification_events.py +22 -0
- chrys/foundation/config/process_settings.py +332 -0
- chrys/foundation/config/runtime_pointer.py +133 -0
- chrys/foundation/config/settings.py +1952 -0
- chrys/foundation/config/settings_store.py +996 -0
- chrys/foundation/config/spec.py +396 -0
- chrys/foundation/config/user_settings.py +140 -0
- chrys/foundation/config/warnings.py +262 -0
- chrys/foundation/config/web_values.py +135 -0
- chrys/foundation/config/yaml_store.py +487 -0
- chrys/foundation/errors/__init__.py +68 -0
- chrys/foundation/errors/_legacy.py +117 -0
- chrys/foundation/errors/_text.py +13 -0
- chrys/foundation/errors/_walk.py +85 -0
- chrys/foundation/errors/classify.py +278 -0
- chrys/foundation/errors/display.py +274 -0
- chrys/foundation/errors/formatting.py +283 -0
- chrys/foundation/errors/kinds.py +67 -0
- chrys/foundation/errors/network.py +497 -0
- chrys/foundation/errors/provider.py +445 -0
- chrys/foundation/errors/route.py +85 -0
- chrys/foundation/events/__init__.py +8 -0
- chrys/foundation/events/bus.py +143 -0
- chrys/foundation/events/types.py +1629 -0
- chrys/foundation/events/workflow.py +38 -0
- chrys/foundation/hosted_tools.py +142 -0
- chrys/foundation/i18n/__init__.py +32 -0
- chrys/foundation/i18n/_catalogs/zh-Hans/LC_MESSAGES/chrys.mo +0 -0
- chrys/foundation/i18n/catalogs.py +42 -0
- chrys/foundation/i18n/formatting.py +202 -0
- chrys/foundation/i18n/locale.py +92 -0
- chrys/foundation/i18n/localizer.py +187 -0
- chrys/foundation/i18n/messages.py +268 -0
- chrys/foundation/io/__init__.py +3 -0
- chrys/foundation/io/result_content.py +237 -0
- chrys/foundation/models/__init__.py +3 -0
- chrys/foundation/models/approval_reuse.py +23 -0
- chrys/foundation/models/ask_user.py +468 -0
- chrys/foundation/models/execution.py +18 -0
- chrys/foundation/models/history_markers.py +115 -0
- chrys/foundation/models/invocations.py +63 -0
- chrys/foundation/models/mutation_scope.py +28 -0
- chrys/foundation/models/session_env.py +87 -0
- chrys/foundation/models/session_surface.py +29 -0
- chrys/foundation/models/todos.py +74 -0
- chrys/foundation/models/turns.py +125 -0
- chrys/foundation/models/workflow_session.py +89 -0
- chrys/foundation/models/workspace.py +59 -0
- chrys/foundation/net/__init__.py +3 -0
- chrys/foundation/net/address_class.py +57 -0
- chrys/foundation/net/route_probe.py +94 -0
- chrys/foundation/net/url.py +107 -0
- chrys/foundation/observability/__init__.py +3 -0
- chrys/foundation/observability/exporters.py +174 -0
- chrys/foundation/observability/gate.py +31 -0
- chrys/foundation/observability/setup.py +497 -0
- chrys/foundation/observability/sink.py +228 -0
- chrys/foundation/patches/__init__.py +19 -0
- chrys/foundation/patches/mdurl_cache.py +59 -0
- chrys/foundation/patches/patcher.py +287 -0
- chrys/foundation/patches/staged_members.py +205 -0
- chrys/foundation/patches/textual_block_border.py +105 -0
- chrys/foundation/patches/textual_button_ansi.py +65 -0
- chrys/foundation/patches/textual_button_ansi_old.txt +68 -0
- chrys/foundation/patches/textual_callback_cache.py +248 -0
- chrys/foundation/patches/textual_callback_dispatch.py +109 -0
- chrys/foundation/patches/textual_compositor_cjk.py +566 -0
- chrys/foundation/patches/textual_dispatch_cache.py +150 -0
- chrys/foundation/patches/textual_ime_cursor_anchor.py +92 -0
- chrys/foundation/patches/textual_kitty_keyboard.py +268 -0
- chrys/foundation/patches/textual_lru_acyclic.py +241 -0
- chrys/foundation/patches/textual_message_pump.py +136 -0
- chrys/foundation/patches/textual_node_diet.py +875 -0
- chrys/foundation/patches/textual_one_shot_timer.py +48 -0
- chrys/foundation/patches/textual_option_list.py +90 -0
- chrys/foundation/patches/textual_precompose.py +89 -0
- chrys/foundation/patches/textual_pruned_tabs.py +69 -0
- chrys/foundation/patches/textual_reflow_reuse.py +1753 -0
- chrys/foundation/patches/textual_removed_node_caches.py +86 -0
- chrys/foundation/patches/textual_removed_screen_callbacks.py +55 -0
- chrys/foundation/patches/textual_selection_extract.py +48 -0
- chrys/foundation/patches/textual_strip_cycles.py +119 -0
- chrys/foundation/patches/textual_tab_selection.py +1611 -0
- chrys/foundation/patches/textual_timer_skip.py +70 -0
- chrys/foundation/patches/textual_utf8_decoder.py +106 -0
- chrys/foundation/patches/textual_win_sleep.py +167 -0
- chrys/foundation/patches/watchdog_fsevents.py +70 -0
- chrys/foundation/patches/watchdog_windows.py +52 -0
- chrys/foundation/platform/__init__.py +556 -0
- chrys/foundation/platform/c_api.py +32 -0
- chrys/foundation/platform/child_reap.py +107 -0
- chrys/foundation/platform/command_line.py +95 -0
- chrys/foundation/platform/files.py +1470 -0
- chrys/foundation/platform/output_capture.py +113 -0
- chrys/foundation/platform/paths.py +193 -0
- chrys/foundation/platform/process.py +1468 -0
- chrys/foundation/platform/pty_output.py +19 -0
- chrys/foundation/platform/runtime_paths.py +281 -0
- chrys/foundation/platform/win_console.py +129 -0
- chrys/foundation/reasoning_origin.py +67 -0
- chrys/foundation/recovery.py +16 -0
- chrys/foundation/retry.py +278 -0
- chrys/foundation/text/__init__.py +3 -0
- chrys/foundation/text/encoding.py +603 -0
- chrys/foundation/text/images.py +304 -0
- chrys/foundation/text/lines.py +24 -0
- chrys/foundation/text/mentions.py +90 -0
- chrys/foundation/text/model_json.py +25 -0
- chrys/foundation/text/reading_order.py +20 -0
- chrys/foundation/text/tokenizer.py +25 -0
- chrys/foundation/text/tool_output.py +287 -0
- chrys/foundation/text/yaml_io.py +62 -0
- chrys/foundation/tool_call_context.py +202 -0
- chrys/foundation/tool_execution_stamp.py +184 -0
- chrys/foundation/tool_invocation_order.py +48 -0
- chrys/foundation/tool_kinds.py +102 -0
- chrys/foundation/tool_result_metadata.py +287 -0
- chrys/foundation/trajectory/__init__.py +10 -0
- chrys/foundation/trajectory/context.py +481 -0
- chrys/foundation/trajectory/envelope.py +571 -0
- chrys/foundation/trajectory/event_types.py +294 -0
- chrys/foundation/trajectory/finalizers.py +46 -0
- chrys/foundation/trajectory/fingerprint.py +62 -0
- chrys/foundation/trajectory/ids.py +155 -0
- chrys/foundation/trajectory/keys.py +99 -0
- chrys/foundation/trajectory/lease.py +173 -0
- chrys/foundation/trajectory/metadata.py +72 -0
- chrys/foundation/trajectory/reader.py +271 -0
- chrys/foundation/trajectory/recovery.py +225 -0
- chrys/foundation/trajectory/revisions.py +152 -0
- chrys/foundation/trajectory/segments.py +225 -0
- chrys/foundation/trajectory/tools.py +117 -0
- chrys/foundation/trajectory/usage.py +151 -0
- chrys/foundation/trajectory/writer.py +972 -0
- chrys/foundation/trajectory_timing.py +97 -0
- chrys/foundation/util/__init__.py +3 -0
- chrys/foundation/util/async_tasks.py +29 -0
- chrys/foundation/util/chrys_headers.py +23 -0
- chrys/foundation/util/env_templates.py +104 -0
- chrys/foundation/util/filenames.py +96 -0
- chrys/foundation/util/header_charset.py +138 -0
- chrys/foundation/util/httpx_helpers.py +12 -0
- chrys/foundation/util/lock.py +236 -0
- chrys/foundation/util/once_close.py +46 -0
- chrys/foundation/util/session_ids.py +29 -0
- chrys/foundation/util/shell_tokens.py +88 -0
- chrys/foundation/util/sub_agent_context.py +38 -0
- chrys/foundation/util/time.py +35 -0
- chrys/foundation/util/unicode_scalars.py +36 -0
- chrys/foundation/vendor/__init__.py +43 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-unknown-linux-gnu +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-unknown-linux-musl +0 -0
- chrys/kernel/__init__.py +256 -0
- chrys/kernel/_content.py +1866 -0
- chrys/kernel/_loop_recorder.py +631 -0
- chrys/kernel/_null_overlay.py +1026 -0
- chrys/kernel/_result_ceiling.py +216 -0
- chrys/kernel/_serialization.py +431 -0
- chrys/kernel/_tool_arg_errors.py +1629 -0
- chrys/kernel/_tool_execution.py +1172 -0
- chrys/kernel/_tool_expansion.py +26 -0
- chrys/kernel/_types.py +2598 -0
- chrys/kernel/agent.py +994 -0
- chrys/kernel/client.py +1084 -0
- chrys/kernel/compaction.py +1315 -0
- chrys/kernel/exceptions.py +211 -0
- chrys/kernel/exchanges.py +683 -0
- chrys/kernel/identity.py +95 -0
- chrys/kernel/images.py +36 -0
- chrys/kernel/instrumentation.py +1618 -0
- chrys/kernel/loop.py +2308 -0
- chrys/kernel/middleware.py +616 -0
- chrys/kernel/sessions.py +604 -0
- chrys/kernel/tools.py +1061 -0
- chrys/kernel/types.py +126 -0
- chrys/orchestration/__init__.py +3 -0
- chrys/orchestration/engine/__init__.py +3 -0
- chrys/orchestration/engine/assembly.py +236 -0
- chrys/orchestration/engine/build/__init__.py +3 -0
- chrys/orchestration/engine/build/builder.py +1061 -0
- chrys/orchestration/engine/build/construction.py +408 -0
- chrys/orchestration/engine/build/loaded.py +139 -0
- chrys/orchestration/engine/engine.py +789 -0
- chrys/orchestration/engine/execution.py +771 -0
- chrys/orchestration/engine/loader.py +865 -0
- chrys/orchestration/engine/rollback.py +918 -0
- chrys/orchestration/engine/run/__init__.py +3 -0
- chrys/orchestration/engine/run/active_injection.py +419 -0
- chrys/orchestration/engine/run/attachments.py +519 -0
- chrys/orchestration/engine/run/bindings.py +1095 -0
- chrys/orchestration/engine/run/coordinator.py +1062 -0
- chrys/orchestration/engine/run/finalizer.py +417 -0
- chrys/orchestration/engine/run/input_refs.py +60 -0
- chrys/orchestration/engine/run/prompt_content.py +457 -0
- chrys/orchestration/engine/run/resume.py +299 -0
- chrys/orchestration/engine/run/retry.py +948 -0
- chrys/orchestration/engine/run/runner.py +625 -0
- chrys/orchestration/engine/run/runtime_skills.py +146 -0
- chrys/orchestration/engine/run/sub_agent_coordination.py +126 -0
- chrys/orchestration/engine/run/turn_hooks.py +258 -0
- chrys/orchestration/engine/run/turn_state.py +149 -0
- chrys/orchestration/engine/run/working_dir.py +50 -0
- chrys/orchestration/engine/session_lifecycle.py +2013 -0
- chrys/orchestration/engine/state/__init__.py +3 -0
- chrys/orchestration/engine/state/active_session.py +183 -0
- chrys/orchestration/engine/state/controls.py +639 -0
- chrys/orchestration/engine/state/current_agent.py +26 -0
- chrys/orchestration/engine/state/lifecycle_permits.py +395 -0
- chrys/orchestration/engine/state/machine.py +193 -0
- chrys/orchestration/engine/state/session_writer.py +434 -0
- chrys/orchestration/engine/trajectory.py +714 -0
- chrys/orchestration/engine/usage.py +118 -0
- chrys/orchestration/invoker/__init__.py +3 -0
- chrys/orchestration/invoker/acp.py +727 -0
- chrys/orchestration/invoker/acp_protocol.py +1115 -0
- chrys/orchestration/invoker/acp_spec.py +79 -0
- chrys/orchestration/invoker/attempts.py +994 -0
- chrys/orchestration/invoker/child_compaction.py +131 -0
- chrys/orchestration/invoker/child_history.py +99 -0
- chrys/orchestration/invoker/contracts.py +253 -0
- chrys/orchestration/invoker/evidence.py +94 -0
- chrys/orchestration/invoker/kernel.py +343 -0
- chrys/orchestration/invoker/origin.py +79 -0
- chrys/orchestration/invoker/resources.py +343 -0
- chrys/orchestration/invoker/runtime.py +299 -0
- chrys/orchestration/invoker/usage.py +80 -0
- chrys/orchestration/session_hooks.py +117 -0
- chrys/orchestration/session_host.py +1023 -0
- chrys/orchestration/session_resources.py +95 -0
- chrys/orchestration/session_usage.py +219 -0
- chrys/orchestration/startup.py +255 -0
- chrys/orchestration/sub_agents/__init__.py +3 -0
- chrys/orchestration/sub_agents/acp_policy.py +269 -0
- chrys/orchestration/sub_agents/kernel_policy.py +848 -0
- chrys/orchestration/sub_agents/registration.py +44 -0
- chrys/orchestration/sub_agents/shell.py +337 -0
- chrys/orchestration/sub_agents/tools.py +2228 -0
- chrys/orchestration/workflows/__init__.py +3 -0
- chrys/orchestration/workflows/agent_archive.py +156 -0
- chrys/orchestration/workflows/agent_node.py +894 -0
- chrys/orchestration/workflows/agent_node_build.py +687 -0
- chrys/orchestration/workflows/catalog.py +332 -0
- chrys/orchestration/workflows/coordinator.py +771 -0
- chrys/orchestration/workflows/hooks.py +176 -0
- chrys/orchestration/workflows/preview.py +322 -0
- chrys/orchestration/workflows/rollback.py +151 -0
- chrys/orchestration/workflows/runner.py +986 -0
- chrys/orchestration/workflows/session.py +390 -0
- chrys/orchestration/workflows/settings.py +97 -0
- chrys/orchestration/workflows/validation.py +674 -0
- chrys/orchestration/workflows/worker_client.py +1002 -0
- chrys/service/__init__.py +3 -0
- chrys/service/acp_client/__init__.py +65 -0
- chrys/service/acp_client/_transport.py +922 -0
- chrys/service/acp_client/client.py +1311 -0
- chrys/service/acp_client/errors.py +224 -0
- chrys/service/acp_client/protocol.py +220 -0
- chrys/service/acp_client/spawn.py +239 -0
- chrys/service/acp_client/spec.py +100 -0
- chrys/service/agent_middleware/__init__.py +60 -0
- chrys/service/agent_middleware/_metadata_keys.py +52 -0
- chrys/service/agent_middleware/control/__init__.py +3 -0
- chrys/service/agent_middleware/control/approval.py +769 -0
- chrys/service/agent_middleware/control/approval_hooks.py +62 -0
- chrys/service/agent_middleware/control/ask_user.py +207 -0
- chrys/service/agent_middleware/control/interrupt.py +50 -0
- chrys/service/agent_middleware/control/sleep.py +118 -0
- chrys/service/agent_middleware/control/todo.py +117 -0
- chrys/service/agent_middleware/events/__init__.py +3 -0
- chrys/service/agent_middleware/events/hook_dispatch.py +250 -0
- chrys/service/agent_middleware/events/hosted_tools.py +1762 -0
- chrys/service/agent_middleware/events/intermediate_text.py +128 -0
- chrys/service/agent_middleware/events/rejection_metadata.py +50 -0
- chrys/service/agent_middleware/events/result_persistence.py +98 -0
- chrys/service/agent_middleware/events/sub_agent_events.py +951 -0
- chrys/service/agent_middleware/events/tool_error_text.py +22 -0
- chrys/service/agent_middleware/events/tool_events.py +779 -0
- chrys/service/agent_middleware/injection.py +510 -0
- chrys/service/agent_middleware/reminders/__init__.py +91 -0
- chrys/service/agent_middleware/reminders/archive_pointer.py +154 -0
- chrys/service/agent_middleware/reminders/base.py +25 -0
- chrys/service/agent_middleware/reminders/context_usage.py +74 -0
- chrys/service/agent_middleware/reminders/file_change.py +63 -0
- chrys/service/agent_middleware/reminders/mcp.py +39 -0
- chrys/service/agent_middleware/reminders/profile_switch.py +106 -0
- chrys/service/agent_middleware/reminders/runtime_env.py +190 -0
- chrys/service/agent_middleware/reminders/skills.py +44 -0
- chrys/service/agent_middleware/reminders/sub_agents.py +35 -0
- chrys/service/agent_middleware/reminders/todo.py +35 -0
- chrys/service/agent_middleware/reminders/turn_line.py +48 -0
- chrys/service/agent_middleware/response_validation.py +1380 -0
- chrys/service/agent_middleware/system_reminder.py +1107 -0
- chrys/service/agent_middleware/validators.py +396 -0
- chrys/service/analytics/__init__.py +124 -0
- chrys/service/analytics/_actions.py +896 -0
- chrys/service/analytics/_context_evidence.py +357 -0
- chrys/service/analytics/_critical_path.py +179 -0
- chrys/service/analytics/_facts.py +919 -0
- chrys/service/analytics/_insights.py +769 -0
- chrys/service/analytics/_metric_ops.py +83 -0
- chrys/service/analytics/_session_projection.py +460 -0
- chrys/service/analytics/_timeline.py +434 -0
- chrys/service/analytics/_turn_graph.py +1237 -0
- chrys/service/analytics/_turns.py +1377 -0
- chrys/service/analytics/_workflows.py +171 -0
- chrys/service/analytics/aggregation.py +1474 -0
- chrys/service/analytics/classification.py +128 -0
- chrys/service/analytics/export.py +803 -0
- chrys/service/analytics/findings.py +282 -0
- chrys/service/analytics/math.py +79 -0
- chrys/service/analytics/model.py +886 -0
- chrys/service/analytics/reader.py +321 -0
- chrys/service/approval/__init__.py +3 -0
- chrys/service/approval/arbitration.py +102 -0
- chrys/service/approval/command_identity.py +109 -0
- chrys/service/approval/correlation.py +57 -0
- chrys/service/approval/grant_store.py +108 -0
- chrys/service/approval/judge.py +589 -0
- chrys/service/approval/level.py +15 -0
- chrys/service/approval/policy.py +209 -0
- chrys/service/approval/reuse.py +223 -0
- chrys/service/approval/reuse_binding.py +133 -0
- chrys/service/approval/safety_classifier.py +536 -0
- chrys/service/approval/turn_context.py +49 -0
- chrys/service/context/__init__.py +3 -0
- chrys/service/context/compaction/__init__.py +38 -0
- chrys/service/context/compaction/breaker.py +156 -0
- chrys/service/context/compaction/budgets.py +93 -0
- chrys/service/context/compaction/calibration.py +77 -0
- chrys/service/context/compaction/compression.py +690 -0
- chrys/service/context/compaction/current_turn_drop.py +458 -0
- chrys/service/context/compaction/events.py +201 -0
- chrys/service/context/compaction/exclusions.py +261 -0
- chrys/service/context/compaction/groups.py +361 -0
- chrys/service/context/compaction/last_words.py +1996 -0
- chrys/service/context/compaction/last_words_state.py +752 -0
- chrys/service/context/compaction/scoped.py +472 -0
- chrys/service/context/compaction/spill.py +1947 -0
- chrys/service/context/compaction/strategy.py +1116 -0
- chrys/service/context/compaction/summaries.py +319 -0
- chrys/service/context/compaction/turns.py +375 -0
- chrys/service/context/manager.py +174 -0
- chrys/service/context/memory_loader.py +792 -0
- chrys/service/context/middleware/__init__.py +3 -0
- chrys/service/context/middleware/usage.py +332 -0
- chrys/service/context/providers/__init__.py +3 -0
- chrys/service/context/providers/context_management.py +432 -0
- chrys/service/context/providers/history.py +647 -0
- chrys/service/context/providers/memory.py +50 -0
- chrys/service/context/tool_content.py +22 -0
- chrys/service/hooks/__init__.py +37 -0
- chrys/service/hooks/detached_worker.py +118 -0
- chrys/service/hooks/docs/README.md +14 -0
- chrys/service/hooks/docs/authoring.md +684 -0
- chrys/service/hooks/docs/configuration.md +499 -0
- chrys/service/hooks/docs/design.md +359 -0
- chrys/service/hooks/docs/project-hooks.md +348 -0
- chrys/service/hooks/events.py +157 -0
- chrys/service/hooks/loader.py +665 -0
- chrys/service/hooks/manager.py +807 -0
- chrys/service/hooks/matcher.py +111 -0
- chrys/service/hooks/outbox.py +294 -0
- chrys/service/hooks/runner.py +572 -0
- chrys/service/hooks/schema.py +407 -0
- chrys/service/llm/__init__.py +7 -0
- chrys/service/llm/_structured_outputs.py +192 -0
- chrys/service/llm/anthropic_messages/__init__.py +9 -0
- chrys/service/llm/anthropic_messages/client.py +254 -0
- chrys/service/llm/anthropic_messages/decode.py +331 -0
- chrys/service/llm/anthropic_messages/history.py +279 -0
- chrys/service/llm/anthropic_messages/request.py +323 -0
- chrys/service/llm/anthropic_messages/server_tools.py +319 -0
- chrys/service/llm/anthropic_messages/stream.py +297 -0
- chrys/service/llm/anthropic_messages/thinking_binding.py +156 -0
- chrys/service/llm/chat_completions/__init__.py +9 -0
- chrys/service/llm/chat_completions/client.py +369 -0
- chrys/service/llm/chat_completions/decode.py +338 -0
- chrys/service/llm/chat_completions/history.py +657 -0
- chrys/service/llm/chat_completions/reasoning.py +244 -0
- chrys/service/llm/chat_completions/request.py +192 -0
- chrys/service/llm/chat_completions/stream.py +546 -0
- chrys/service/llm/chat_completions/validation.py +308 -0
- chrys/service/llm/clients.py +613 -0
- chrys/service/llm/images.py +65 -0
- chrys/service/llm/mock.py +413 -0
- chrys/service/llm/observer.py +446 -0
- chrys/service/llm/one_shot.py +106 -0
- chrys/service/llm/openai_exceptions.py +93 -0
- chrys/service/llm/openai_responses/__init__.py +10 -0
- chrys/service/llm/openai_responses/client.py +323 -0
- chrys/service/llm/openai_responses/decode.py +432 -0
- chrys/service/llm/openai_responses/history.py +519 -0
- chrys/service/llm/openai_responses/hosted.py +625 -0
- chrys/service/llm/openai_responses/replay.py +629 -0
- chrys/service/llm/openai_responses/request.py +342 -0
- chrys/service/llm/openai_responses/stream.py +847 -0
- chrys/service/llm/openai_timestamps.py +57 -0
- chrys/service/llm/providers.py +107 -0
- chrys/service/llm/proxy_route.py +54 -0
- chrys/service/llm/raw_http_log.py +309 -0
- chrys/service/llm/route_facts.py +63 -0
- chrys/service/llm/route_sessions.py +49 -0
- chrys/service/llm/sdk_anthropic.py +18 -0
- chrys/service/llm/sdk_headers.py +47 -0
- chrys/service/llm/sdk_openai.py +18 -0
- chrys/service/llm/sdk_retry.py +59 -0
- chrys/service/llm/wire_client.py +246 -0
- chrys/service/mcp/__init__.py +3 -0
- chrys/service/mcp/_connection.py +235 -0
- chrys/service/mcp/_http_transport.py +350 -0
- chrys/service/mcp/_progressive.py +459 -0
- chrys/service/mcp/_stdio_transport.py +614 -0
- chrys/service/mcp/_tool_mixins.py +96 -0
- chrys/service/mcp/adapter.py +1031 -0
- chrys/service/mcp/cache.py +476 -0
- chrys/service/mcp/content.py +32 -0
- chrys/service/mcp/errors.py +169 -0
- chrys/service/mcp/owned.py +1330 -0
- chrys/service/mcp/result_limits.py +168 -0
- chrys/service/mcp/thinking_warning.py +95 -0
- chrys/service/mcp/validation.py +74 -0
- chrys/service/mutations/__init__.py +84 -0
- chrys/service/mutations/coordination.py +894 -0
- chrys/service/mutations/detector.py +458 -0
- chrys/service/mutations/git_calibrator.py +397 -0
- chrys/service/mutations/git_state.py +647 -0
- chrys/service/mutations/pipeline.py +779 -0
- chrys/service/mutations/scanner.py +466 -0
- chrys/service/mutations/snapshot_files.py +233 -0
- chrys/service/mutations/store.py +625 -0
- chrys/service/mutations/tool_names.py +15 -0
- chrys/service/mutations/trace.py +246 -0
- chrys/service/mutations/tracker.py +1289 -0
- chrys/service/mutations/types.py +559 -0
- chrys/service/mutations/workspace_changes.py +1583 -0
- chrys/service/profiles/__init__.py +2 -0
- chrys/service/profiles/agents/__init__.py +3 -0
- chrys/service/profiles/agents/builtins/Code.yaml +199 -0
- chrys/service/profiles/agents/builtins/Explore.yaml +62 -0
- chrys/service/profiles/agents/builtins/General.yaml +114 -0
- chrys/service/profiles/agents/builtins/QA.yaml +128 -0
- chrys/service/profiles/agents/loader.py +808 -0
- chrys/service/profiles/agents/registry.py +727 -0
- chrys/service/profiles/agents/schema.py +371 -0
- chrys/service/profiles/agents/serializer.py +196 -0
- chrys/service/profiles/coercion.py +22 -0
- chrys/service/profiles/models/__init__.py +3 -0
- chrys/service/profiles/models/env_bridge.py +157 -0
- chrys/service/profiles/models/loader.py +282 -0
- chrys/service/profiles/models/options.py +438 -0
- chrys/service/profiles/models/registry.py +104 -0
- chrys/service/profiles/models/resolver.py +286 -0
- chrys/service/profiles/models/schema.py +91 -0
- chrys/service/profiles/models/serializer.py +76 -0
- chrys/service/session/__init__.py +3 -0
- chrys/service/session/checkpoint.py +156 -0
- chrys/service/session/history.py +1616 -0
- chrys/service/session/message_metadata.py +151 -0
- chrys/service/session/persistence.py +435 -0
- chrys/service/session/runtime_metadata.py +210 -0
- chrys/service/session/sub_agent_logs.py +1069 -0
- chrys/service/session/sub_agent_transcript.py +654 -0
- chrys/service/skills/__init__.py +3 -0
- chrys/service/skills/adapter.py +408 -0
- chrys/service/skills/constants.py +8 -0
- chrys/service/skills/loader.py +575 -0
- chrys/service/skills/model.py +130 -0
- chrys/service/skills/provider.py +841 -0
- chrys/service/skills/runner.py +390 -0
- chrys/service/state/__init__.py +3 -0
- chrys/service/state/_fork.py +710 -0
- chrys/service/state/_session_files.py +175 -0
- chrys/service/state/_session_meta.py +552 -0
- chrys/service/state/locks.py +154 -0
- chrys/service/state/serializers.py +156 -0
- chrys/service/state/session_catalog.py +361 -0
- chrys/service/state/session_listing.py +84 -0
- chrys/service/state/session_migration.py +446 -0
- chrys/service/state/session_mru.py +315 -0
- chrys/service/state/store.py +2132 -0
- chrys/service/state/workflow.py +158 -0
- chrys/service/text_blocks.py +47 -0
- chrys/service/todos/__init__.py +3 -0
- chrys/service/todos/reminder.py +31 -0
- chrys/service/todos/tracker.py +58 -0
- chrys/service/tools/__init__.py +3 -0
- chrys/service/tools/approval_targets.py +92 -0
- chrys/service/tools/builtins/__init__.py +3 -0
- chrys/service/tools/builtins/ask_user.py +326 -0
- chrys/service/tools/builtins/doc_converter/__init__.py +494 -0
- chrys/service/tools/builtins/doc_converter/artifacts.py +392 -0
- chrys/service/tools/builtins/doc_converter/parsers/__init__.py +3 -0
- chrys/service/tools/builtins/doc_converter/parsers/_table.py +34 -0
- chrys/service/tools/builtins/doc_converter/parsers/base.py +101 -0
- chrys/service/tools/builtins/doc_converter/parsers/docx.py +88 -0
- chrys/service/tools/builtins/doc_converter/parsers/pdf.py +144 -0
- chrys/service/tools/builtins/doc_converter/parsers/pptx.py +103 -0
- chrys/service/tools/builtins/doc_converter/parsers/xls.py +32 -0
- chrys/service/tools/builtins/doc_converter/parsers/xlsx.py +51 -0
- chrys/service/tools/builtins/doc_converter/registry.py +55 -0
- chrys/service/tools/builtins/filesystem.py +1071 -0
- chrys/service/tools/builtins/search.py +1265 -0
- chrys/service/tools/builtins/shell.py +967 -0
- chrys/service/tools/builtins/shell_command_filtering/__init__.py +5 -0
- chrys/service/tools/builtins/shell_command_filtering/policy.py +60 -0
- chrys/service/tools/builtins/shell_command_filtering/presets.py +152 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/__init__.py +358 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/commands.py +416 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/git.py +408 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/syntax.py +87 -0
- chrys/service/tools/builtins/shell_command_filtering/tokenizer.py +214 -0
- chrys/service/tools/builtins/shell_filter.py +24 -0
- chrys/service/tools/builtins/sleep.py +91 -0
- chrys/service/tools/builtins/todo.py +64 -0
- chrys/service/tools/builtins/web/__init__.py +6 -0
- chrys/service/tools/builtins/web/build.py +268 -0
- chrys/service/tools/builtins/web/config.py +338 -0
- chrys/service/tools/builtins/web/credentials.py +37 -0
- chrys/service/tools/builtins/web/fetch/__init__.py +3 -0
- chrys/service/tools/builtins/web/fetch/cache.py +68 -0
- chrys/service/tools/builtins/web/fetch/convert.py +56 -0
- chrys/service/tools/builtins/web/fetch/tool.py +223 -0
- chrys/service/tools/builtins/web/hosted.py +61 -0
- chrys/service/tools/builtins/web/http.py +330 -0
- chrys/service/tools/builtins/web/progress.py +18 -0
- chrys/service/tools/builtins/web/search/__init__.py +3 -0
- chrys/service/tools/builtins/web/search/exa_mcp.py +146 -0
- chrys/service/tools/builtins/web/search/html.py +152 -0
- chrys/service/tools/builtins/web/search/json_api.py +227 -0
- chrys/service/tools/builtins/web/search/providers.py +77 -0
- chrys/service/tools/builtins/web/search/tool.py +218 -0
- chrys/service/tools/builtins/web/search/types.py +28 -0
- chrys/service/tools/builtins/web/tls_identity.py +66 -0
- chrys/service/tools/kinds.py +115 -0
- chrys/service/tools/names.py +47 -0
- chrys/service/tools/registry.py +242 -0
- chrys/service/tools/result_metadata.py +103 -0
- chrys/service/tools/session_artifacts.py +196 -0
- chrys/service/tools/spill.py +218 -0
- chrys/service/tools/workspace_paths.py +81 -0
- chrys/service/trajectory/__init__.py +11 -0
- chrys/service/trajectory/approvals.py +201 -0
- chrys/service/trajectory/compaction.py +420 -0
- chrys/service/trajectory/fork.py +75 -0
- chrys/service/trajectory/hooks.py +217 -0
- chrys/service/trajectory/items.py +41 -0
- chrys/service/trajectory/preparation.py +374 -0
- chrys/service/trajectory/retries.py +95 -0
- chrys/service/trajectory/revisions.py +129 -0
- chrys/service/trajectory/segmented.py +163 -0
- chrys/service/trajectory/session.py +762 -0
- chrys/service/trajectory/state.py +83 -0
- chrys/service/trajectory/sub_agents.py +169 -0
- chrys/service/trajectory/tombstone.py +262 -0
- chrys/service/trajectory/tools.py +323 -0
- chrys/service/trajectory/validation.py +130 -0
- chrys/service/trajectory/waits.py +161 -0
- chrys/service/trajectory/workflow.py +148 -0
- chrys/service/vision.py +128 -0
- chrys/service/workflows/__init__.py +3 -0
- chrys/service/workflows/admission.py +151 -0
- chrys/service/workflows/artifacts.py +109 -0
- chrys/service/workflows/asks.py +130 -0
- chrys/service/workflows/builtins/__init__.py +12 -0
- chrys/service/workflows/builtins/demo-workflow.manifest.json +635 -0
- chrys/service/workflows/builtins/demo-workflow.py +756 -0
- chrys/service/workflows/discovery.py +554 -0
- chrys/service/workflows/environment.py +284 -0
- chrys/service/workflows/graph.py +430 -0
- chrys/service/workflows/history.py +82 -0
- chrys/service/workflows/interpreter.py +107 -0
- chrys/service/workflows/journal.py +334 -0
- chrys/service/workflows/layout.py +56 -0
- chrys/service/workflows/ledger.py +149 -0
- chrys/service/workflows/model_selection.py +18 -0
- chrys/service/workflows/orphans.py +209 -0
- chrys/service/workflows/outcomes.py +46 -0
- chrys/service/workflows/protocol.py +210 -0
- chrys/service/workflows/records.py +142 -0
- chrys/service/workflows/scheduler.py +770 -0
- chrys/service/workflows/sdk/__init__.py +51 -0
- chrys/service/workflows/sdk/_ask.py +167 -0
- chrys/service/workflows/sdk/_builder.py +781 -0
- chrys/service/workflows/sdk/_values.py +92 -0
- chrys/service/workflows/sdk_artifact.py +74 -0
- chrys/service/workflows/store.py +494 -0
- chrys/service/workflows/transcript.py +102 -0
- chrys/service/workflows/values.py +73 -0
- chrys/service/workflows/worker_host.py +1820 -0
- chrys/workflows.py +38 -0
- icode_tui-0.29.1.dist-info/METADATA +104 -0
- icode_tui-0.29.1.dist-info/RECORD +1138 -0
- icode_tui-0.29.1.dist-info/WHEEL +4 -0
- icode_tui-0.29.1.dist-info/entry_points.txt +2 -0
- icode_tui-0.29.1.dist-info/licenses/LICENSE +202 -0
- icode_tui-0.29.1.dist-info/licenses/NOTICE +1955 -0
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Sub-agent audit log storage."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import hashlib
|
|
9
|
+
import hmac
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import secrets
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from datetime import UTC, datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
from urllib.parse import urlsplit
|
|
20
|
+
|
|
21
|
+
from chrys.foundation.platform import get_platform
|
|
22
|
+
from chrys.foundation.platform.files import (
|
|
23
|
+
SecureFileError,
|
|
24
|
+
atomic_write_owner_only_bytes,
|
|
25
|
+
atomic_write_owner_only_text,
|
|
26
|
+
read_owner_verified_bounded,
|
|
27
|
+
secure_open_owner_only,
|
|
28
|
+
secure_open_owner_only_binary,
|
|
29
|
+
secure_unlink_owner_verified,
|
|
30
|
+
surrogate_safe_text,
|
|
31
|
+
)
|
|
32
|
+
from chrys.foundation.trajectory.metadata import read_analytics_item_id
|
|
33
|
+
from chrys.foundation.util.filenames import bounded_safe_stem, safe_fs_name
|
|
34
|
+
from chrys.foundation.util.lock import FileLock
|
|
35
|
+
from chrys.service.session.message_metadata import TOOL_RESULT_METADATA_KEY
|
|
36
|
+
from chrys.service.state.serializers import serialize_state
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
SUB_AGENT_LOG_PREVIEW_MAX_CHARS = 2000
|
|
41
|
+
SUB_AGENT_TRANSCRIPT_OCCURRENCE_IDENTITY = "analytics_item_id_v1"
|
|
42
|
+
SUB_AGENT_LOG_TMP_PREFIX = "."
|
|
43
|
+
SUB_AGENT_LOG_TMP_SUFFIX = ".tmp"
|
|
44
|
+
SUB_AGENT_PENDING_SOURCE_PATH_KEY = "_chrys_source_path"
|
|
45
|
+
SUB_AGENT_RESTORE_CONSUMED_KEY = "_chrys_restore_consumed"
|
|
46
|
+
_INVOCATION_ID_RE = re.compile(r"^[0-9a-f]{12}$")
|
|
47
|
+
_ACP_HMAC_KEY_FILE = "acp-audit-hmac.key"
|
|
48
|
+
_ACP_HMAC_KEY_BYTES = 32
|
|
49
|
+
_ACP_RING_ITEMS = 500
|
|
50
|
+
_ACP_RING_BYTES = 256 * 1024
|
|
51
|
+
_ACP_ATTEMPT_ITEMS = 50
|
|
52
|
+
# Read ceilings for owner-VERIFIED artifacts. Owner verification proves the
|
|
53
|
+
# file is same-uid, but a same-uid ACP child is UNTRUSTED (it may plant a
|
|
54
|
+
# huge/sparse record), so restore/fork reads must be bounded to stop one
|
|
55
|
+
# small-on-disk file from forcing an unbounded allocation. Pending records
|
|
56
|
+
# are tiny metadata; audit envelopes may embed a kernel sub-agent's full
|
|
57
|
+
# serialized history (including images), so they get far more headroom.
|
|
58
|
+
_MAX_PENDING_RECORD_BYTES = 4 * 1024 * 1024
|
|
59
|
+
MAX_SUB_AGENT_AUDIT_BYTES = 128 * 1024 * 1024
|
|
60
|
+
# A same-uid ACP child is untrusted: individual pending records are bounded to
|
|
61
|
+
# _MAX_PENDING_RECORD_BYTES, but the child can plant hundreds of them, so drain
|
|
62
|
+
# must also cap the AGGREGATE it materializes in memory per restore. Past the
|
|
63
|
+
# cap the remainder is deferred, not dropped — finalize deletes the records
|
|
64
|
+
# consumed this restore, so the leftovers drain over subsequent restores.
|
|
65
|
+
MAX_PENDING_RECORDS = 256
|
|
66
|
+
MAX_PENDING_RECORDS_TOTAL_BYTES = 32 * 1024 * 1024
|
|
67
|
+
# Even before the per-record caps apply, the mere ENUMERATION of a session
|
|
68
|
+
# artifact directory is attacker-influenced: a same-uid child can plant millions
|
|
69
|
+
# of tiny files. `Path.glob`/`Path.iterdir` are EAGER on 3.14 (they list scandir
|
|
70
|
+
# before yielding), so `scan_bounded_json_files` drives `os.scandir` directly and
|
|
71
|
+
# breaks after MAX_SCANNED_* examined entries. A truncated scan defers the
|
|
72
|
+
# remainder; corrupt/consumed files leave the scan over restores so it converges.
|
|
73
|
+
MAX_SCANNED_PENDING_FILES = 4096
|
|
74
|
+
MAX_SCANNED_AUDIT_FILES = 4096
|
|
75
|
+
# Enumeration count is not enough: a bounded set of files can still be huge/
|
|
76
|
+
# sparse, so reading them all (`read_owner_verified_bounded`, up to the per-file
|
|
77
|
+
# ceilings) would itself be an unbounded I/O DoS (e.g. 4096 x 128 MiB audit
|
|
78
|
+
# reads). Bound the CUMULATIVE on-disk size a single scan will accept for
|
|
79
|
+
# reading, using scandir's free st_size to refuse a bomb without reading it.
|
|
80
|
+
MAX_PENDING_SCAN_TOTAL_BYTES = 64 * 1024 * 1024
|
|
81
|
+
MAX_AUDIT_SCAN_TOTAL_BYTES = 512 * 1024 * 1024
|
|
82
|
+
# Marks a paused record whose still-"running" audit could NOT be re-flipped to
|
|
83
|
+
# "orphaned" this restore (transient audit-write failure). finalize keeps such
|
|
84
|
+
# records so the next restore retries the repair; deleting them would strand
|
|
85
|
+
# the audit "running" and backlinked, hence permanently exempt from repair.
|
|
86
|
+
SUB_AGENT_REPAIR_FAILED_KEY = "_chrys_repair_failed"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(slots=True)
|
|
90
|
+
class SubAgentLogStats:
|
|
91
|
+
"""In-memory counters snapshotted into sub-agent audit logs."""
|
|
92
|
+
|
|
93
|
+
tool_call_count: int = 0
|
|
94
|
+
total_tokens: int = 0
|
|
95
|
+
total_usage_tokens: int = 0
|
|
96
|
+
usage_unreported_attempts: int = 0
|
|
97
|
+
|
|
98
|
+
def record_tool_call(self) -> None:
|
|
99
|
+
self.tool_call_count += 1
|
|
100
|
+
|
|
101
|
+
def record_usage(self, total_tokens: int, total_usage_tokens: int) -> None:
|
|
102
|
+
self.total_tokens = total_tokens
|
|
103
|
+
self.total_usage_tokens = total_usage_tokens
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True, slots=True)
|
|
107
|
+
class SubAgentModelProvenance:
|
|
108
|
+
"""Non-sensitive model provenance stored in sub-agent audit metadata."""
|
|
109
|
+
|
|
110
|
+
provider: str = ""
|
|
111
|
+
api_style: str = ""
|
|
112
|
+
model_id: str = ""
|
|
113
|
+
base_url_origin: str = ""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class SubAgentSessionLogWriter:
|
|
117
|
+
"""Cancellation-safe per-invocation writer for one sub-agent log file."""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
*,
|
|
122
|
+
parent_session_dir: Path | None,
|
|
123
|
+
parent_session_id: str | None,
|
|
124
|
+
invocation_id: str,
|
|
125
|
+
tool_name: str,
|
|
126
|
+
agent_profile: str,
|
|
127
|
+
agent_display_name: str,
|
|
128
|
+
prompt: str,
|
|
129
|
+
parent_provider_call_id: str,
|
|
130
|
+
parent_event_call_id: str,
|
|
131
|
+
model: SubAgentModelProvenance | None = None,
|
|
132
|
+
primary_cwd: str = "",
|
|
133
|
+
working_dirs: list[str] | None = None,
|
|
134
|
+
stats: SubAgentLogStats | None = None,
|
|
135
|
+
runner: str = "kernel",
|
|
136
|
+
acp_command: str = "",
|
|
137
|
+
acp_args: list[str] | None = None,
|
|
138
|
+
) -> None:
|
|
139
|
+
self._parent_session_dir = parent_session_dir
|
|
140
|
+
self._parent_session_id = parent_session_id or ""
|
|
141
|
+
self._invocation_id = invocation_id
|
|
142
|
+
self._tool_name = tool_name
|
|
143
|
+
self._agent_profile = agent_profile
|
|
144
|
+
self._agent_display_name = agent_display_name
|
|
145
|
+
self._prompt_preview = preview_text(prompt)
|
|
146
|
+
self._parent_provider_call_id = parent_provider_call_id
|
|
147
|
+
self._parent_event_call_id = parent_event_call_id
|
|
148
|
+
self._model = model or SubAgentModelProvenance()
|
|
149
|
+
self._primary_cwd = primary_cwd
|
|
150
|
+
self._working_dirs = list(working_dirs or ([primary_cwd] if primary_cwd else []))
|
|
151
|
+
self._stats = stats or SubAgentLogStats()
|
|
152
|
+
self._runner = runner
|
|
153
|
+
self._acp_command = acp_command
|
|
154
|
+
self._acp_args = list(acp_args or [])
|
|
155
|
+
self._created_at = _utc_now()
|
|
156
|
+
self._lock = asyncio.Lock()
|
|
157
|
+
self._active = False
|
|
158
|
+
self._log_file_name = make_sub_agent_log_file_name(tool_name, invocation_id)
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def log_file_name(self) -> str:
|
|
162
|
+
return self._log_file_name
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def path(self) -> Path | None:
|
|
166
|
+
if self._parent_session_dir is None:
|
|
167
|
+
return None
|
|
168
|
+
return sessions_dir(self._parent_session_dir) / self._log_file_name
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def active(self) -> bool:
|
|
172
|
+
return self._active
|
|
173
|
+
|
|
174
|
+
def set_acp_launch(self, command: str, args: list[str]) -> None:
|
|
175
|
+
"""Update the resolved launch argv used by the next ACP envelope write."""
|
|
176
|
+
if self._runner != "acp":
|
|
177
|
+
raise RuntimeError("ACP launch provenance is available only for ACP audit writers")
|
|
178
|
+
self._acp_command = command
|
|
179
|
+
self._acp_args = list(args)
|
|
180
|
+
|
|
181
|
+
async def write(
|
|
182
|
+
self,
|
|
183
|
+
*,
|
|
184
|
+
status: str,
|
|
185
|
+
state: dict[str, Any] | None = None,
|
|
186
|
+
result: str = "",
|
|
187
|
+
failure_reason: str = "",
|
|
188
|
+
last_error: str = "",
|
|
189
|
+
ended: bool = False,
|
|
190
|
+
acp_state: dict[str, Any] | None = None,
|
|
191
|
+
) -> bool:
|
|
192
|
+
"""Best-effort write of the current invocation envelope."""
|
|
193
|
+
path = self.path
|
|
194
|
+
if path is None:
|
|
195
|
+
return False
|
|
196
|
+
async with self._lock:
|
|
197
|
+
write_task: asyncio.Task[None] | None = None
|
|
198
|
+
cancelled = False
|
|
199
|
+
try:
|
|
200
|
+
envelope = self._envelope(
|
|
201
|
+
status=status,
|
|
202
|
+
state=state,
|
|
203
|
+
result=result,
|
|
204
|
+
failure_reason=failure_reason,
|
|
205
|
+
last_error=last_error,
|
|
206
|
+
ended=ended,
|
|
207
|
+
acp_state=acp_state,
|
|
208
|
+
)
|
|
209
|
+
write_task = asyncio.create_task(asyncio.to_thread(_atomic_write_json, path, envelope))
|
|
210
|
+
try:
|
|
211
|
+
await asyncio.shield(write_task)
|
|
212
|
+
except asyncio.CancelledError:
|
|
213
|
+
cancelled = True
|
|
214
|
+
while not write_task.done():
|
|
215
|
+
try:
|
|
216
|
+
await asyncio.shield(write_task)
|
|
217
|
+
except asyncio.CancelledError:
|
|
218
|
+
cancelled = True
|
|
219
|
+
write_task.result()
|
|
220
|
+
except Exception as exc:
|
|
221
|
+
logger.warning("Failed to write sub-agent audit log %s: %s", path.name, exc)
|
|
222
|
+
if cancelled:
|
|
223
|
+
raise asyncio.CancelledError from exc
|
|
224
|
+
if self._runner == "acp":
|
|
225
|
+
raise
|
|
226
|
+
return False
|
|
227
|
+
self._active = True
|
|
228
|
+
if cancelled:
|
|
229
|
+
raise asyncio.CancelledError
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
def _envelope(
|
|
233
|
+
self,
|
|
234
|
+
*,
|
|
235
|
+
status: str,
|
|
236
|
+
state: dict[str, Any] | None,
|
|
237
|
+
result: str,
|
|
238
|
+
failure_reason: str,
|
|
239
|
+
last_error: str,
|
|
240
|
+
ended: bool,
|
|
241
|
+
acp_state: dict[str, Any] | None,
|
|
242
|
+
) -> dict[str, Any]:
|
|
243
|
+
serialized_state = serialize_state(state or {}) if self._runner == "kernel" else {}
|
|
244
|
+
now = _utc_now()
|
|
245
|
+
meta = {
|
|
246
|
+
"schema_version": 1,
|
|
247
|
+
"runner": self._runner,
|
|
248
|
+
"record_type": "sub_agent_session",
|
|
249
|
+
"parent_session_id": self._parent_session_id,
|
|
250
|
+
"parent_event_call_id": self._parent_event_call_id,
|
|
251
|
+
"parent_provider_call_id": self._parent_provider_call_id,
|
|
252
|
+
"invocation_id": self._invocation_id,
|
|
253
|
+
"tool_name": self._tool_name,
|
|
254
|
+
"agent_profile": self._agent_profile,
|
|
255
|
+
"agent_display_name": self._agent_display_name,
|
|
256
|
+
"status": status,
|
|
257
|
+
"failure_reason": failure_reason,
|
|
258
|
+
"last_error": last_error,
|
|
259
|
+
"created_at": self._created_at,
|
|
260
|
+
"updated_at": now,
|
|
261
|
+
"ended_at": now if ended else "",
|
|
262
|
+
"prompt_preview": self._prompt_preview,
|
|
263
|
+
"result_preview": preview_text(result),
|
|
264
|
+
"message_count": len(serialized_state.get("messages", [])),
|
|
265
|
+
"tool_call_count": self._stats.tool_call_count,
|
|
266
|
+
"total_tokens": self._stats.total_tokens,
|
|
267
|
+
"total_usage_tokens": self._stats.total_usage_tokens,
|
|
268
|
+
"usage_unreported_attempts": self._stats.usage_unreported_attempts,
|
|
269
|
+
"primary_cwd": self._primary_cwd,
|
|
270
|
+
"working_dirs": list(self._working_dirs),
|
|
271
|
+
"model_provider": self._model.provider,
|
|
272
|
+
"model_api_style": self._model.api_style,
|
|
273
|
+
"model_id": self._model.model_id,
|
|
274
|
+
"model_base_url_origin": self._model.base_url_origin,
|
|
275
|
+
}
|
|
276
|
+
if self._runner == "kernel" and serialized_history_has_occurrence_ids(serialized_state):
|
|
277
|
+
meta["transcript_occurrence_identity"] = SUB_AGENT_TRANSCRIPT_OCCURRENCE_IDENTITY
|
|
278
|
+
if self._runner == "acp":
|
|
279
|
+
normalized = normalize_acp_state(acp_state or {}, parent_session_dir=self._parent_session_dir)
|
|
280
|
+
normalized["usage_unreported_attempts"] = self._stats.usage_unreported_attempts
|
|
281
|
+
normalized["launch"] = acp_launch_provenance(self._acp_command, self._acp_args)
|
|
282
|
+
return {"meta": meta, "acp_state": normalized}
|
|
283
|
+
return {"meta": meta, "state": serialized_state}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def serialized_history_has_occurrence_ids(state: dict[str, Any]) -> bool:
|
|
287
|
+
"""Return whether every persisted kernel message has stable occurrence identity."""
|
|
288
|
+
live_messages = state.get("messages")
|
|
289
|
+
compressed = state.get("compressed_msgs")
|
|
290
|
+
if not isinstance(live_messages, list) or not isinstance(compressed, list):
|
|
291
|
+
return False
|
|
292
|
+
messages: list[Any] = list(live_messages)
|
|
293
|
+
for block in compressed:
|
|
294
|
+
if not isinstance(block, dict):
|
|
295
|
+
return False
|
|
296
|
+
block_messages = block.get("messages")
|
|
297
|
+
if not isinstance(block_messages, list):
|
|
298
|
+
return False
|
|
299
|
+
messages.extend(block_messages)
|
|
300
|
+
return all(
|
|
301
|
+
isinstance(message, dict) and read_analytics_item_id(message.get("additional_properties")) is not None
|
|
302
|
+
for message in messages
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def pending_dir(parent_session_dir: Path) -> Path:
|
|
307
|
+
return parent_session_dir / "sub_agents" / "pending"
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def sessions_dir(parent_session_dir: Path) -> Path:
|
|
311
|
+
return parent_session_dir / "sub_agents" / "sessions"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def make_sub_agent_log_file_name(tool_name: str, invocation_id: str) -> str:
|
|
315
|
+
# The invocation id is appended as the filename suffix. Sanitize it as well
|
|
316
|
+
# as the tool-name stem: a chrys-generated 12-hex id passes through
|
|
317
|
+
# unchanged (``safe_fs_name`` is a length-preserving 1:1 substitution), but
|
|
318
|
+
# an attacker-plantable pause record could carry a traversal payload
|
|
319
|
+
# (``/../../victim``) that would otherwise escape the sessions directory
|
|
320
|
+
# when this name is path-joined. Stripping the separators keeps the result
|
|
321
|
+
# a single in-directory basename regardless of caller trust.
|
|
322
|
+
suffix = f"_{safe_fs_name(invocation_id)}.json"
|
|
323
|
+
stem = bounded_safe_stem(
|
|
324
|
+
tool_name,
|
|
325
|
+
suffix=suffix,
|
|
326
|
+
temp_prefix=SUB_AGENT_LOG_TMP_PREFIX,
|
|
327
|
+
temp_suffix=SUB_AGENT_LOG_TMP_SUFFIX,
|
|
328
|
+
)
|
|
329
|
+
return f"{stem}{suffix}"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def validate_invocation_id(invocation_id: str) -> bool:
|
|
333
|
+
return bool(_INVOCATION_ID_RE.fullmatch(invocation_id))
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def preview_text(text: str, *, max_chars: int = SUB_AGENT_LOG_PREVIEW_MAX_CHARS) -> str:
|
|
337
|
+
"""Return a bounded preview, appending ``...`` only within the cap."""
|
|
338
|
+
if len(text) <= max_chars:
|
|
339
|
+
return text
|
|
340
|
+
if max_chars <= 3:
|
|
341
|
+
return "." * max(0, max_chars)
|
|
342
|
+
return f"{text[: max_chars - 3]}..."
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def sanitize_base_url_origin(raw_url: str) -> str:
|
|
346
|
+
"""Return ``scheme://host[:port]`` for normal absolute URLs, else ``""``."""
|
|
347
|
+
if not raw_url:
|
|
348
|
+
return ""
|
|
349
|
+
try:
|
|
350
|
+
parsed = urlsplit(raw_url)
|
|
351
|
+
except ValueError:
|
|
352
|
+
return ""
|
|
353
|
+
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
|
|
354
|
+
return ""
|
|
355
|
+
try:
|
|
356
|
+
port = parsed.port
|
|
357
|
+
except ValueError:
|
|
358
|
+
return ""
|
|
359
|
+
host = parsed.hostname
|
|
360
|
+
if port is None:
|
|
361
|
+
return f"{parsed.scheme}://{host}"
|
|
362
|
+
return f"{parsed.scheme}://{host}:{port}"
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def redact_acp_argv(args: list[str]) -> list[str]:
|
|
366
|
+
"""Conservatively retain option prefixes while redacting every value."""
|
|
367
|
+
redacted: list[str] = []
|
|
368
|
+
positional_only = False
|
|
369
|
+
for arg in args:
|
|
370
|
+
if positional_only:
|
|
371
|
+
# Everything after ``--`` is positional (doc §10: every attached
|
|
372
|
+
# AND positional value is replaced), even if it starts with ``-``.
|
|
373
|
+
redacted.append("<redacted>")
|
|
374
|
+
continue
|
|
375
|
+
if arg == "--":
|
|
376
|
+
# The end-of-options marker is a fixed token, not a secret; keep
|
|
377
|
+
# it and redact everything that follows.
|
|
378
|
+
positional_only = True
|
|
379
|
+
redacted.append(arg)
|
|
380
|
+
elif arg.startswith("--") and "=" in arg:
|
|
381
|
+
option, _value = arg.split("=", 1)
|
|
382
|
+
redacted.append(f"{option}=<redacted>")
|
|
383
|
+
elif arg.startswith("--") and len(arg) > 2:
|
|
384
|
+
redacted.append(arg)
|
|
385
|
+
elif arg.startswith("-") and len(arg) > 2:
|
|
386
|
+
redacted.append(f"{arg[:2]}<redacted>")
|
|
387
|
+
elif arg.startswith("-") and len(arg) == 2:
|
|
388
|
+
redacted.append(arg)
|
|
389
|
+
else:
|
|
390
|
+
redacted.append("<redacted>")
|
|
391
|
+
return redacted
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _audit_safe_text(value: str) -> str:
|
|
395
|
+
"""Escape any unpaired surrogate so this string is safe to audit.
|
|
396
|
+
|
|
397
|
+
POSIX filesystem paths (and env-expanded launch argv derived from them) can
|
|
398
|
+
carry surrogateescaped bytes — legal str values that are NOT strict-UTF-8
|
|
399
|
+
encodable. Left raw they crash the audit twice: the provenance digest below
|
|
400
|
+
and the ``ensure_ascii=False`` file write, and even a surrogatepass-written
|
|
401
|
+
file would fail the audit's strict-UTF-8 JSON read-back in reconcile/fork.
|
|
402
|
+
``backslashreplace`` turns a lone surrogate into its ``\\udcXX`` escape (a
|
|
403
|
+
no-op on valid text), keeping the stored value total and round-trippable
|
|
404
|
+
while the digest below still binds the exact original bytes.
|
|
405
|
+
|
|
406
|
+
This is per-field defense-in-depth for the provenance digest inputs; the
|
|
407
|
+
whole-envelope boundary sanitize in ``_atomic_write_json`` (and the sibling
|
|
408
|
+
ACP writers) is what guarantees EVERY field — meta paths included — is
|
|
409
|
+
surrogate-free before the strict file write.
|
|
410
|
+
"""
|
|
411
|
+
return surrogate_safe_text(value)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def acp_launch_provenance(command: str, args: list[str]) -> dict[str, Any]:
|
|
415
|
+
"""Return non-secret launch metadata plus a keyed full-argv digest."""
|
|
416
|
+
key = load_or_create_acp_audit_key()
|
|
417
|
+
# surrogatepass keeps the digest total over the EXACT argv bytes even when a
|
|
418
|
+
# value carries a surrogateescaped filesystem byte; the stored command/args
|
|
419
|
+
# are surrogate-escaped so the audit envelope stays strict-UTF-8 round-trippable.
|
|
420
|
+
payload = json.dumps(
|
|
421
|
+
[command, *args],
|
|
422
|
+
ensure_ascii=False,
|
|
423
|
+
separators=(",", ":"),
|
|
424
|
+
allow_nan=False,
|
|
425
|
+
).encode("utf-8", "surrogatepass")
|
|
426
|
+
return {
|
|
427
|
+
"command": _audit_safe_text(command),
|
|
428
|
+
"args": [_audit_safe_text(arg) for arg in redact_acp_argv(args)],
|
|
429
|
+
"arg_count": len(args),
|
|
430
|
+
"argv_hmac_sha256": hmac.new(key, payload, hashlib.sha256).hexdigest(),
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def load_or_create_acp_audit_key(*, config_dir: Path | None = None, timeout: float = 5.0) -> bytes:
|
|
435
|
+
"""Securely validate or no-replace-create the installation ACP HMAC key."""
|
|
436
|
+
root = config_dir or get_platform().config_dir
|
|
437
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
438
|
+
key_path = root / _ACP_HMAC_KEY_FILE
|
|
439
|
+
lock_path = root / f"{_ACP_HMAC_KEY_FILE}.lock"
|
|
440
|
+
with FileLock(lock_path, timeout=timeout):
|
|
441
|
+
if key_path.exists() or key_path.is_symlink():
|
|
442
|
+
return _read_acp_audit_key(key_path)
|
|
443
|
+
|
|
444
|
+
temp_path = root / f".{_ACP_HMAC_KEY_FILE}.{secrets.token_hex(8)}.tmp"
|
|
445
|
+
fd = secure_open_owner_only(temp_path, write=True, create=True)
|
|
446
|
+
try:
|
|
447
|
+
key = secrets.token_bytes(_ACP_HMAC_KEY_BYTES)
|
|
448
|
+
with os.fdopen(fd, "wb") as file:
|
|
449
|
+
fd = -1
|
|
450
|
+
file.write(key)
|
|
451
|
+
file.flush()
|
|
452
|
+
os.fsync(file.fileno())
|
|
453
|
+
try:
|
|
454
|
+
os.link(temp_path, key_path, follow_symlinks=False)
|
|
455
|
+
except FileExistsError:
|
|
456
|
+
# A creator not using this lock is still never allowed to be
|
|
457
|
+
# overwritten; validate what won the no-replace publish race.
|
|
458
|
+
return _read_acp_audit_key(key_path)
|
|
459
|
+
_fsync_parent(root)
|
|
460
|
+
return _read_acp_audit_key(key_path)
|
|
461
|
+
finally:
|
|
462
|
+
if fd >= 0:
|
|
463
|
+
os.close(fd)
|
|
464
|
+
secure_unlink_owner_verified(temp_path)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _read_acp_audit_key(path: Path) -> bytes:
|
|
468
|
+
try:
|
|
469
|
+
with secure_open_owner_only_binary(path) as file:
|
|
470
|
+
key = file.read(_ACP_HMAC_KEY_BYTES + 1)
|
|
471
|
+
except OSError as exc:
|
|
472
|
+
raise SecureFileError("The ACP audit HMAC key failed secure verification.") from exc
|
|
473
|
+
if len(key) != _ACP_HMAC_KEY_BYTES:
|
|
474
|
+
raise SecureFileError("The ACP audit HMAC key has an invalid length.")
|
|
475
|
+
return key
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _fsync_parent(path: Path) -> None:
|
|
479
|
+
if get_platform().is_windows:
|
|
480
|
+
return
|
|
481
|
+
fd = os.open(path, os.O_RDONLY)
|
|
482
|
+
try:
|
|
483
|
+
os.fsync(fd)
|
|
484
|
+
finally:
|
|
485
|
+
os.close(fd)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def normalize_acp_state(state: dict[str, Any], *, parent_session_dir: Path | None) -> dict[str, Any]:
|
|
489
|
+
"""Bound the ACP codec and normalize referenced files session-relatively."""
|
|
490
|
+
normalized = json.loads(json.dumps(state, ensure_ascii=False, default=str, allow_nan=False))
|
|
491
|
+
updates = normalized.get("translated_updates")
|
|
492
|
+
if isinstance(updates, list):
|
|
493
|
+
ring: list[Any] = []
|
|
494
|
+
# Count list delimiters and commas as well as entry payloads.
|
|
495
|
+
size = 2
|
|
496
|
+
for update in reversed(updates[-_ACP_RING_ITEMS:]):
|
|
497
|
+
item_size = len(
|
|
498
|
+
json.dumps(
|
|
499
|
+
update,
|
|
500
|
+
ensure_ascii=False,
|
|
501
|
+
separators=(",", ":"),
|
|
502
|
+
allow_nan=False,
|
|
503
|
+
).encode()
|
|
504
|
+
)
|
|
505
|
+
added = item_size + (1 if ring else 0)
|
|
506
|
+
if size + added > _ACP_RING_BYTES:
|
|
507
|
+
break
|
|
508
|
+
ring.append(update)
|
|
509
|
+
size += added
|
|
510
|
+
normalized["translated_updates"] = list(reversed(ring))
|
|
511
|
+
attempts = normalized.get("attempts")
|
|
512
|
+
if isinstance(attempts, list) and len(attempts) > _ACP_ATTEMPT_ITEMS:
|
|
513
|
+
# Manual retries are unlimited, so the attempt history needs a ring
|
|
514
|
+
# like translated_updates. Newest-wins; per-entry fields are already
|
|
515
|
+
# client-capped (ACP session ids are validated at 4096 chars).
|
|
516
|
+
normalized["attempts"] = attempts[-_ACP_ATTEMPT_ITEMS:]
|
|
517
|
+
for key in ("diagnostic_path", "stderr_log_path"):
|
|
518
|
+
raw = normalized.get(key)
|
|
519
|
+
if not raw:
|
|
520
|
+
continue
|
|
521
|
+
if parent_session_dir is None:
|
|
522
|
+
normalized.pop(key, None)
|
|
523
|
+
continue
|
|
524
|
+
root = parent_session_dir.resolve()
|
|
525
|
+
path = Path(str(raw))
|
|
526
|
+
if not path.is_absolute():
|
|
527
|
+
path = root / path
|
|
528
|
+
try:
|
|
529
|
+
resolved = path.resolve()
|
|
530
|
+
normalized[key] = str(resolved.relative_to(root))
|
|
531
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
532
|
+
raise ValueError(f"ACP state file reference escapes the session directory: {key}") from exc
|
|
533
|
+
return normalized
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def validate_acp_state_file_references(state: dict[str, Any], *, parent_session_dir: Path) -> None:
|
|
537
|
+
"""Reject absolute, parent-traversing, and symlink-escaping ACP references."""
|
|
538
|
+
root = parent_session_dir.resolve()
|
|
539
|
+
for key in ("diagnostic_path", "stderr_log_path"):
|
|
540
|
+
raw = state.get(key)
|
|
541
|
+
if not raw:
|
|
542
|
+
continue
|
|
543
|
+
path = Path(str(raw))
|
|
544
|
+
if path.is_absolute() or ".." in path.parts:
|
|
545
|
+
raise ValueError(f"Invalid ACP state file reference: {key}")
|
|
546
|
+
try:
|
|
547
|
+
(root / path).resolve().relative_to(root)
|
|
548
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
549
|
+
raise ValueError(f"ACP state file reference escapes the session directory: {key}") from exc
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
_PAUSED_RECORD_REQUIRED_STR_FIELDS = ("invocation_id", "tool_name")
|
|
553
|
+
_PAUSED_RECORD_OPTIONAL_STR_FIELDS = (
|
|
554
|
+
"parent_call_id",
|
|
555
|
+
"parent_provider_call_id",
|
|
556
|
+
"sub_agent_log_file",
|
|
557
|
+
"created_at",
|
|
558
|
+
"paused_at",
|
|
559
|
+
"status",
|
|
560
|
+
"failure_reason",
|
|
561
|
+
"last_error",
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _paused_record_is_well_formed(data: dict[str, Any]) -> bool:
|
|
566
|
+
"""Reject records whose field types would crash restore consumers.
|
|
567
|
+
|
|
568
|
+
Required identity fields must be non-empty strings; optional fields are
|
|
569
|
+
string-or-absent (``inject_error_results_for_sub_agents`` uses parent
|
|
570
|
+
call ids as dict keys, and the restore Warning joins tool names).
|
|
571
|
+
"""
|
|
572
|
+
for field in _PAUSED_RECORD_REQUIRED_STR_FIELDS:
|
|
573
|
+
value = data.get(field)
|
|
574
|
+
if not isinstance(value, str) or not value:
|
|
575
|
+
return False
|
|
576
|
+
for field in _PAUSED_RECORD_OPTIONAL_STR_FIELDS:
|
|
577
|
+
value = data.get(field)
|
|
578
|
+
if value is not None and not isinstance(value, str):
|
|
579
|
+
return False
|
|
580
|
+
return True
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def scan_bounded_json_files(
|
|
584
|
+
directory: Path,
|
|
585
|
+
*,
|
|
586
|
+
max_files: int,
|
|
587
|
+
max_total_bytes: int | None = None,
|
|
588
|
+
suffixes: tuple[str, ...] = (".json",),
|
|
589
|
+
) -> tuple[list[Path], bool]:
|
|
590
|
+
"""Lazily enumerate at most *max_files* matching entries via ``os.scandir``.
|
|
591
|
+
|
|
592
|
+
``Path.glob``/``Path.iterdir`` are EAGER on CPython 3.14 — they run
|
|
593
|
+
``list(scandir)`` internally before yielding the first result, so wrapping
|
|
594
|
+
them in ``islice`` does NOT bound enumeration and a same-uid flood of
|
|
595
|
+
planted files still OOMs restore/fork during listing. ``os.scandir`` yields
|
|
596
|
+
lazily; we break after *max_files* EXAMINED entries (not just matched ones,
|
|
597
|
+
so a flood of non-matching names cannot spin the loop unbounded) and, when
|
|
598
|
+
*max_total_bytes* is given, once the cumulative on-disk size of accepted
|
|
599
|
+
files would exceed it — using the free ``st_size`` from ``scandir`` so a
|
|
600
|
+
sparse/huge file is refused WITHOUT being read (bounding the bytes a later
|
|
601
|
+
``read_owner_verified_bounded`` will materialize). Returns the bounded
|
|
602
|
+
subset sorted by path plus a truncated flag; the remainder (the
|
|
603
|
+
filesystem-order tail beyond the caps) is deferred to a later pass.
|
|
604
|
+
"""
|
|
605
|
+
paths: list[Path] = []
|
|
606
|
+
truncated = False
|
|
607
|
+
total = 0
|
|
608
|
+
try:
|
|
609
|
+
with os.scandir(directory) as iterator:
|
|
610
|
+
examined = 0
|
|
611
|
+
for entry in iterator:
|
|
612
|
+
examined += 1
|
|
613
|
+
if examined > max_files or (max_total_bytes is not None and total >= max_total_bytes):
|
|
614
|
+
truncated = True
|
|
615
|
+
break
|
|
616
|
+
if not entry.name.endswith(suffixes):
|
|
617
|
+
continue
|
|
618
|
+
if max_total_bytes is not None:
|
|
619
|
+
try:
|
|
620
|
+
total += min(entry.stat(follow_symlinks=False).st_size, MAX_SUB_AGENT_AUDIT_BYTES + 1)
|
|
621
|
+
except OSError:
|
|
622
|
+
continue
|
|
623
|
+
paths.append(Path(entry.path))
|
|
624
|
+
except FileNotFoundError, NotADirectoryError:
|
|
625
|
+
return [], False
|
|
626
|
+
except OSError:
|
|
627
|
+
logger.warning("Failed to scan sub-agent directory %s", directory.name, exc_info=True)
|
|
628
|
+
return [], False
|
|
629
|
+
return sorted(paths), truncated
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class SubAgentSessionArtifactService:
|
|
633
|
+
"""Profile-independent restore and orphan repair for one session directory."""
|
|
634
|
+
|
|
635
|
+
def __init__(self, session_dir: Path) -> None:
|
|
636
|
+
self._session_dir = session_dir
|
|
637
|
+
|
|
638
|
+
def drain_paused_records(self) -> list[dict[str, Any]]:
|
|
639
|
+
logs = self._log_files_by_invocation()
|
|
640
|
+
records: list[dict[str, Any]] = []
|
|
641
|
+
candidates: list[Path] = []
|
|
642
|
+
for directory in (self._session_dir / "sub_agents", pending_dir(self._session_dir)):
|
|
643
|
+
# Bound enumeration AND cumulative read BEFORE the per-record caps: a
|
|
644
|
+
# same-uid flood of tiny (even corrupt) files must not materialize an
|
|
645
|
+
# unbounded Path list, and a pile of huge/sparse records must not
|
|
646
|
+
# force an unbounded read. Corrupt/consumed files leave the scan over
|
|
647
|
+
# restores.
|
|
648
|
+
paths, truncated = scan_bounded_json_files(
|
|
649
|
+
directory,
|
|
650
|
+
max_files=MAX_SCANNED_PENDING_FILES,
|
|
651
|
+
max_total_bytes=MAX_PENDING_SCAN_TOTAL_BYTES,
|
|
652
|
+
)
|
|
653
|
+
if truncated:
|
|
654
|
+
logger.warning(
|
|
655
|
+
"Paused sub-agent scan of %s truncated; a later restore drains the rest",
|
|
656
|
+
directory.name,
|
|
657
|
+
)
|
|
658
|
+
candidates.extend(paths)
|
|
659
|
+
total_bytes = 0
|
|
660
|
+
for index, path in enumerate(candidates):
|
|
661
|
+
# A same-uid ACP child can plant hundreds of individually-valid
|
|
662
|
+
# (<4 MiB) records; without an aggregate ceiling this loop would
|
|
663
|
+
# hold them all in memory at once and OOM the restore. Stop past the
|
|
664
|
+
# cap and DEFER the rest — candidate sizes are unknown until read, so
|
|
665
|
+
# breaking is safer than scanning on. finalize removes the records
|
|
666
|
+
# consumed this restore, letting the remainder drain later.
|
|
667
|
+
if len(records) >= MAX_PENDING_RECORDS or total_bytes >= MAX_PENDING_RECORDS_TOTAL_BYTES:
|
|
668
|
+
logger.warning(
|
|
669
|
+
"Deferring %d paused sub-agent record(s) past the per-restore cap (%d drained, %d bytes)",
|
|
670
|
+
len(candidates) - index,
|
|
671
|
+
len(records),
|
|
672
|
+
total_bytes,
|
|
673
|
+
)
|
|
674
|
+
break
|
|
675
|
+
try:
|
|
676
|
+
raw = _secure_read_bytes(path)
|
|
677
|
+
except OSError, ValueError:
|
|
678
|
+
# ValueError == over the per-record read ceiling (an over-cap
|
|
679
|
+
# planted record); OSError == I/O failure. _archive drops the
|
|
680
|
+
# over-cap file via its own ceiling and quarantines the rest.
|
|
681
|
+
logger.warning("Failed to read paused sub-agent %s", path.name, exc_info=True)
|
|
682
|
+
self._archive(path, "corrupt")
|
|
683
|
+
continue
|
|
684
|
+
try:
|
|
685
|
+
data = json.loads(raw.decode("utf-8"))
|
|
686
|
+
except UnicodeError, json.JSONDecodeError, ValueError, RecursionError:
|
|
687
|
+
# RecursionError (not a ValueError) covers a deeply nested but
|
|
688
|
+
# byte-bounded record whose json.loads blows the stack; quarantine
|
|
689
|
+
# it like any other unreadable record rather than aborting restore.
|
|
690
|
+
logger.warning("Failed to load paused sub-agent %s", path.name, exc_info=True)
|
|
691
|
+
self._archive(path, "corrupt")
|
|
692
|
+
continue
|
|
693
|
+
if not isinstance(data, dict) or not _paused_record_is_well_formed(data):
|
|
694
|
+
logger.warning("Quarantining malformed paused sub-agent record %s", path.name)
|
|
695
|
+
self._archive(path, "corrupt")
|
|
696
|
+
continue
|
|
697
|
+
total_bytes += len(raw)
|
|
698
|
+
data[SUB_AGENT_PENDING_SOURCE_PATH_KEY] = str(path)
|
|
699
|
+
invocation_id = data.get("invocation_id")
|
|
700
|
+
if not data.get("sub_agent_log_file") and isinstance(invocation_id, str) and invocation_id in logs:
|
|
701
|
+
data["sub_agent_log_file"] = logs[invocation_id]
|
|
702
|
+
records.append(data)
|
|
703
|
+
records.sort(key=lambda record: str(record.get("created_at") or record.get("paused_at") or ""))
|
|
704
|
+
return records
|
|
705
|
+
|
|
706
|
+
def reconcile_orphaned_running_logs(
|
|
707
|
+
self,
|
|
708
|
+
parent_messages: list[Any],
|
|
709
|
+
paused_records: list[dict[str, Any]],
|
|
710
|
+
) -> int:
|
|
711
|
+
pending_by_id: dict[str, dict[str, Any]] = {}
|
|
712
|
+
for record in paused_records:
|
|
713
|
+
value = record.get("invocation_id")
|
|
714
|
+
if isinstance(value, str) and value:
|
|
715
|
+
pending_by_id.setdefault(value, record)
|
|
716
|
+
pending_ids = set(pending_by_id)
|
|
717
|
+
backlink_ids, backlink_files = _sub_agent_backlinks(parent_messages)
|
|
718
|
+
repaired = 0
|
|
719
|
+
directory = sessions_dir(self._session_dir)
|
|
720
|
+
seen: set[Path] = set()
|
|
721
|
+
# Reconcile each pending record's OWN audit by its deterministic path
|
|
722
|
+
# FIRST, independent of the bounded scan below. The critical case — a
|
|
723
|
+
# pending record beside a still-"running" audit (the process died in the
|
|
724
|
+
# window between writing the pause record and stamping the audit
|
|
725
|
+
# "paused") — MUST be repaired THIS restore: injection then deletes the
|
|
726
|
+
# record and backlinks the log, after which the backlink-exemption would
|
|
727
|
+
# shield the stale "running" status from every future repair. A flood
|
|
728
|
+
# (or a legitimately huge session) can push that audit past the scan
|
|
729
|
+
# cap, so scanning alone is not enough — target it directly.
|
|
730
|
+
for invocation_id, record in pending_by_id.items():
|
|
731
|
+
tool_name = record.get("tool_name")
|
|
732
|
+
if not isinstance(tool_name, str) or not tool_name:
|
|
733
|
+
continue
|
|
734
|
+
# Only a chrys-generated 12-hex id can name a real audit; a pending
|
|
735
|
+
# record is same-uid attacker-plantable, so reject anything else
|
|
736
|
+
# before it reaches the path join below (defense in depth with the
|
|
737
|
+
# sanitization inside make_sub_agent_log_file_name).
|
|
738
|
+
if not validate_invocation_id(invocation_id):
|
|
739
|
+
continue
|
|
740
|
+
target = directory / make_sub_agent_log_file_name(tool_name, invocation_id)
|
|
741
|
+
if target in seen:
|
|
742
|
+
continue
|
|
743
|
+
seen.add(target)
|
|
744
|
+
if self._reconcile_running_audit(
|
|
745
|
+
target,
|
|
746
|
+
pending_by_id=pending_by_id,
|
|
747
|
+
pending_ids=pending_ids,
|
|
748
|
+
backlink_ids=backlink_ids,
|
|
749
|
+
backlink_files=backlink_files,
|
|
750
|
+
):
|
|
751
|
+
repaired += 1
|
|
752
|
+
# Bound the audit scan by BOTH count and cumulative on-disk size: a flood
|
|
753
|
+
# of planted audit files (each up to the 128 MiB ceiling) would otherwise
|
|
754
|
+
# force reading+parsing every one before any repair. This best-effort
|
|
755
|
+
# pass catches genuinely-orphaned running audits (no pending record).
|
|
756
|
+
paths, truncated = scan_bounded_json_files(
|
|
757
|
+
directory,
|
|
758
|
+
max_files=MAX_SCANNED_AUDIT_FILES,
|
|
759
|
+
max_total_bytes=MAX_AUDIT_SCAN_TOTAL_BYTES,
|
|
760
|
+
)
|
|
761
|
+
if truncated:
|
|
762
|
+
logger.warning("Sub-agent audit reconcile scan truncated; a later restore repairs the rest")
|
|
763
|
+
for path in paths:
|
|
764
|
+
if path in seen:
|
|
765
|
+
continue
|
|
766
|
+
seen.add(path)
|
|
767
|
+
if self._reconcile_running_audit(
|
|
768
|
+
path,
|
|
769
|
+
pending_by_id=pending_by_id,
|
|
770
|
+
pending_ids=pending_ids,
|
|
771
|
+
backlink_ids=backlink_ids,
|
|
772
|
+
backlink_files=backlink_files,
|
|
773
|
+
):
|
|
774
|
+
repaired += 1
|
|
775
|
+
return repaired
|
|
776
|
+
|
|
777
|
+
def _reconcile_running_audit(
|
|
778
|
+
self,
|
|
779
|
+
path: Path,
|
|
780
|
+
*,
|
|
781
|
+
pending_by_id: dict[str, dict[str, Any]],
|
|
782
|
+
pending_ids: set[str],
|
|
783
|
+
backlink_ids: set[str],
|
|
784
|
+
backlink_files: set[str],
|
|
785
|
+
) -> bool:
|
|
786
|
+
"""Flip one still-"running" audit to "orphaned"; return True if repaired.
|
|
787
|
+
|
|
788
|
+
Shared by the targeted (per pending record) and bounded-scan passes of
|
|
789
|
+
reconcile_orphaned_running_logs.
|
|
790
|
+
"""
|
|
791
|
+
envelope = self._read_audit_envelope_or_drop(path)
|
|
792
|
+
if envelope is None:
|
|
793
|
+
return False
|
|
794
|
+
try:
|
|
795
|
+
meta = envelope.get("meta")
|
|
796
|
+
if isinstance(meta, dict) and meta.get("runner") == "acp":
|
|
797
|
+
acp_state = envelope.get("acp_state")
|
|
798
|
+
if not isinstance(acp_state, dict):
|
|
799
|
+
raise ValueError("ACP sub-agent log is missing ACP state")
|
|
800
|
+
validate_acp_state_file_references(
|
|
801
|
+
acp_state,
|
|
802
|
+
parent_session_dir=self._session_dir,
|
|
803
|
+
)
|
|
804
|
+
if not isinstance(meta, dict) or meta.get("status") != "running":
|
|
805
|
+
return False
|
|
806
|
+
invocation_id = meta.get("invocation_id")
|
|
807
|
+
if not isinstance(invocation_id, str):
|
|
808
|
+
return False
|
|
809
|
+
# A pending record beside a still-"running" audit means the
|
|
810
|
+
# process died between the pause-record and audit-"paused"
|
|
811
|
+
# writes — repair it NOW: the record is discarded during this
|
|
812
|
+
# restore and its injected error result backlinks the log, so
|
|
813
|
+
# both skips below would otherwise shield the stale status
|
|
814
|
+
# forever. Backlinked audits without a pending record keep
|
|
815
|
+
# their accounted-for exemption.
|
|
816
|
+
if invocation_id not in pending_ids and (invocation_id in backlink_ids or path.name in backlink_files):
|
|
817
|
+
return False
|
|
818
|
+
now = _utc_now()
|
|
819
|
+
meta.update(
|
|
820
|
+
{
|
|
821
|
+
"status": "orphaned",
|
|
822
|
+
"failure_reason": "process_terminated",
|
|
823
|
+
"last_error": "process terminated before sub-agent wrote a terminal audit status",
|
|
824
|
+
"updated_at": now,
|
|
825
|
+
"ended_at": now,
|
|
826
|
+
}
|
|
827
|
+
)
|
|
828
|
+
serialized = json.dumps(envelope, indent=2, ensure_ascii=False, allow_nan=False)
|
|
829
|
+
try:
|
|
830
|
+
atomic_write_owner_only_text(path, serialized)
|
|
831
|
+
except OSError, ValueError:
|
|
832
|
+
# The audit could not be flipped to "orphaned" this restore
|
|
833
|
+
# (transient write failure — NOT an encode failure, which
|
|
834
|
+
# stays with the outer handler). Flag its pending record so
|
|
835
|
+
# finalize keeps it: deleting it would leave the still-
|
|
836
|
+
# "running" audit backlinked and thus permanently exempt
|
|
837
|
+
# (the skip above) from repair on every future restore.
|
|
838
|
+
record = pending_by_id.get(invocation_id)
|
|
839
|
+
if record is not None:
|
|
840
|
+
record[SUB_AGENT_REPAIR_FAILED_KEY] = True
|
|
841
|
+
logger.warning(
|
|
842
|
+
"Failed to write reconciled sub-agent audit log %s; keeping its pending record for retry",
|
|
843
|
+
path.name,
|
|
844
|
+
exc_info=True,
|
|
845
|
+
)
|
|
846
|
+
return False
|
|
847
|
+
return True
|
|
848
|
+
except OSError, ValueError:
|
|
849
|
+
logger.warning("Failed to reconcile sub-agent audit log %s", path.name, exc_info=True)
|
|
850
|
+
return False
|
|
851
|
+
|
|
852
|
+
def finalize_restored_paused_records(self, records: list[dict[str, Any]]) -> None:
|
|
853
|
+
for record in records:
|
|
854
|
+
if record.get(SUB_AGENT_REPAIR_FAILED_KEY):
|
|
855
|
+
# reconcile could not repair this record's still-"running"
|
|
856
|
+
# audit; keep the pending record in place so the next restore
|
|
857
|
+
# retries (it stays in pending_ids, bypassing the backlink
|
|
858
|
+
# exemption). Deleting it now would strand the audit forever.
|
|
859
|
+
continue
|
|
860
|
+
source = record.get(SUB_AGENT_PENDING_SOURCE_PATH_KEY)
|
|
861
|
+
if not isinstance(source, str) or not source:
|
|
862
|
+
continue
|
|
863
|
+
path = Path(source)
|
|
864
|
+
if record.get(SUB_AGENT_RESTORE_CONSUMED_KEY):
|
|
865
|
+
secure_unlink_owner_verified(path)
|
|
866
|
+
else:
|
|
867
|
+
self._archive(path, "unmatched")
|
|
868
|
+
|
|
869
|
+
def archive_unconsumed_restored_paused_records(self, records: list[dict[str, Any]]) -> None:
|
|
870
|
+
for record in records:
|
|
871
|
+
if record.get(SUB_AGENT_RESTORE_CONSUMED_KEY) or record.get(SUB_AGENT_REPAIR_FAILED_KEY):
|
|
872
|
+
# Skip repair-failed records for the same reason as finalize:
|
|
873
|
+
# archiving them out of pending would strand the "running" audit.
|
|
874
|
+
continue
|
|
875
|
+
source = record.get(SUB_AGENT_PENDING_SOURCE_PATH_KEY)
|
|
876
|
+
if isinstance(source, str) and source:
|
|
877
|
+
self._archive(Path(source), "unmatched")
|
|
878
|
+
|
|
879
|
+
def _read_audit_envelope_or_drop(self, path: Path) -> dict[str, Any] | None:
|
|
880
|
+
"""Read one audit envelope, dropping it if it exceeds the read ceiling.
|
|
881
|
+
|
|
882
|
+
An over-cap audit envelope can never be parsed, repaired, or forked;
|
|
883
|
+
leaving it in place re-reads/re-allocates the whole file on every
|
|
884
|
+
restore and would strand a stuck-"running" status forever. Over-cap
|
|
885
|
+
files are dropped (mirroring the over-cap pending-record policy);
|
|
886
|
+
malformed-but-bounded files are skipped in place for later inspection.
|
|
887
|
+
"""
|
|
888
|
+
# Keep the bounded READ and the PARSE in separate try blocks: the
|
|
889
|
+
# over-cap ValueError (from read_owner_verified_bounded) means DROP,
|
|
890
|
+
# but a parse-time ValueError must NOT — json.loads raises a *plain*
|
|
891
|
+
# ValueError for a valid-but-oversized integer (>4300 digits) and a
|
|
892
|
+
# RecursionError (not a ValueError at all) for deeply nested input,
|
|
893
|
+
# both of which can occur well under the byte ceiling. Fusing the two
|
|
894
|
+
# would misclassify those as over-cap and delete bounded evidence, or
|
|
895
|
+
# let RecursionError escape and abort hydration entirely.
|
|
896
|
+
try:
|
|
897
|
+
raw = _secure_read_bytes(path, max_bytes=MAX_SUB_AGENT_AUDIT_BYTES)
|
|
898
|
+
except ValueError:
|
|
899
|
+
# read_owner_verified_bounded raises a plain ValueError before any
|
|
900
|
+
# decode/parse when the file is over the ceiling — drop it.
|
|
901
|
+
logger.warning("Dropping over-sized sub-agent audit log %s", path.name)
|
|
902
|
+
secure_unlink_owner_verified(path)
|
|
903
|
+
return None
|
|
904
|
+
except OSError:
|
|
905
|
+
logger.warning("Failed to read sub-agent audit log %s", path.name, exc_info=True)
|
|
906
|
+
return None
|
|
907
|
+
try:
|
|
908
|
+
envelope = json.loads(raw.decode("utf-8"))
|
|
909
|
+
except UnicodeError, ValueError, RecursionError:
|
|
910
|
+
# Malformed / oversized-int / deeply nested but byte-bounded:
|
|
911
|
+
# preserve in place for later inspection (never delete).
|
|
912
|
+
logger.warning("Skipping malformed sub-agent audit log %s", path.name, exc_info=True)
|
|
913
|
+
return None
|
|
914
|
+
return envelope if isinstance(envelope, dict) else None
|
|
915
|
+
|
|
916
|
+
def _log_files_by_invocation(self) -> dict[str, str]:
|
|
917
|
+
directory = sessions_dir(self._session_dir)
|
|
918
|
+
found: dict[str, str] = {}
|
|
919
|
+
duplicate: set[str] = set()
|
|
920
|
+
# Bound the audit scan by BOTH count and cumulative on-disk size: this
|
|
921
|
+
# builds a map by reading+parsing every audit file, so an unbounded
|
|
922
|
+
# listing (or unbounded reads) would OOM/stall restore before drain's
|
|
923
|
+
# per-record caps ever apply.
|
|
924
|
+
paths, truncated = scan_bounded_json_files(
|
|
925
|
+
directory,
|
|
926
|
+
max_files=MAX_SCANNED_AUDIT_FILES,
|
|
927
|
+
max_total_bytes=MAX_AUDIT_SCAN_TOTAL_BYTES,
|
|
928
|
+
)
|
|
929
|
+
if truncated:
|
|
930
|
+
logger.warning("Sub-agent audit log-file scan truncated; some records may lack a log backlink this restore")
|
|
931
|
+
for path in paths:
|
|
932
|
+
envelope = self._read_audit_envelope_or_drop(path)
|
|
933
|
+
if envelope is None:
|
|
934
|
+
continue
|
|
935
|
+
meta = envelope.get("meta")
|
|
936
|
+
if isinstance(meta, dict) and meta.get("runner") == "acp":
|
|
937
|
+
acp_state = envelope.get("acp_state")
|
|
938
|
+
try:
|
|
939
|
+
if not isinstance(acp_state, dict):
|
|
940
|
+
raise ValueError("ACP sub-agent log is missing ACP state")
|
|
941
|
+
validate_acp_state_file_references(
|
|
942
|
+
acp_state,
|
|
943
|
+
parent_session_dir=self._session_dir,
|
|
944
|
+
)
|
|
945
|
+
except ValueError:
|
|
946
|
+
continue
|
|
947
|
+
invocation_id = meta.get("invocation_id") if isinstance(meta, dict) else None
|
|
948
|
+
if not isinstance(invocation_id, str) or not invocation_id:
|
|
949
|
+
continue
|
|
950
|
+
if invocation_id in found:
|
|
951
|
+
duplicate.add(invocation_id)
|
|
952
|
+
else:
|
|
953
|
+
found[invocation_id] = path.name
|
|
954
|
+
for invocation_id in duplicate:
|
|
955
|
+
found.pop(invocation_id, None)
|
|
956
|
+
return found
|
|
957
|
+
|
|
958
|
+
def _archive(self, path: Path, category: str) -> None:
|
|
959
|
+
try:
|
|
960
|
+
payload = _secure_read_bytes(path)
|
|
961
|
+
except FileNotFoundError:
|
|
962
|
+
return
|
|
963
|
+
except ValueError:
|
|
964
|
+
# Over the read ceiling: an over-cap file is exactly why we are
|
|
965
|
+
# quarantining it — never copy it forward, just drop the source.
|
|
966
|
+
logger.warning("Dropping over-sized sub-agent pending record %s", path.name)
|
|
967
|
+
secure_unlink_owner_verified(path)
|
|
968
|
+
return
|
|
969
|
+
except OSError:
|
|
970
|
+
logger.warning("Failed to read sub-agent pending record %s", path, exc_info=True)
|
|
971
|
+
return
|
|
972
|
+
try:
|
|
973
|
+
destination_dir = pending_dir(self._session_dir) / category
|
|
974
|
+
destination = destination_dir / path.name
|
|
975
|
+
for _attempt in range(100):
|
|
976
|
+
if not destination.exists():
|
|
977
|
+
break
|
|
978
|
+
destination = destination_dir / f"{path.stem}-{secrets.token_hex(4)}{path.suffix}"
|
|
979
|
+
atomic_write_owner_only_bytes(destination, payload)
|
|
980
|
+
secure_unlink_owner_verified(path)
|
|
981
|
+
except FileNotFoundError:
|
|
982
|
+
return
|
|
983
|
+
except OSError:
|
|
984
|
+
logger.warning("Failed to archive sub-agent pending record %s", path, exc_info=True)
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def _secure_read_bytes(path: Path, *, max_bytes: int = _MAX_PENDING_RECORD_BYTES) -> bytes:
|
|
988
|
+
# Owner-verified, not owner-only: pre-existing session artifacts predate
|
|
989
|
+
# the owner-only writers and must stay readable after upgrade.
|
|
990
|
+
return read_owner_verified_bounded(path, max_bytes=max_bytes)
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def _sub_agent_backlinks(parent_messages: list[Any]) -> tuple[set[str], set[str]]:
|
|
994
|
+
invocation_ids: set[str] = set()
|
|
995
|
+
log_files: set[str] = set()
|
|
996
|
+
for message in parent_messages:
|
|
997
|
+
for content in getattr(message, "contents", ()):
|
|
998
|
+
properties = getattr(content, "additional_properties", {})
|
|
999
|
+
metadata = properties.get(TOOL_RESULT_METADATA_KEY) if isinstance(properties, dict) else None
|
|
1000
|
+
if not isinstance(metadata, dict):
|
|
1001
|
+
continue
|
|
1002
|
+
invocation_id = metadata.get("sub_agent_invocation_id")
|
|
1003
|
+
if isinstance(invocation_id, str) and invocation_id:
|
|
1004
|
+
invocation_ids.add(invocation_id)
|
|
1005
|
+
log_file = metadata.get("sub_agent_log_file")
|
|
1006
|
+
if isinstance(log_file, str) and log_file:
|
|
1007
|
+
log_files.add(log_file)
|
|
1008
|
+
return invocation_ids, log_files
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
_MAX_REFERENCE_WALK_DEPTH = 200
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def collect_sub_agent_log_file_references(root: Any) -> set[str]:
|
|
1015
|
+
"""Collect every ``sub_agent_log_file`` value anywhere in a parsed envelope.
|
|
1016
|
+
|
|
1017
|
+
Used by the fork copier to guarantee every audit the forked history
|
|
1018
|
+
references is carried forward, so a bounded artifact scan can never strand
|
|
1019
|
+
a dangling reference in a legitimately large session. The walk is
|
|
1020
|
+
schema-agnostic (it keys on the field name, not the message/content
|
|
1021
|
+
nesting, so serialization changes cannot silently drop references),
|
|
1022
|
+
iterative, and depth-bounded so a deeply nested — but ``json``-parseable —
|
|
1023
|
+
envelope cannot exhaust the interpreter stack.
|
|
1024
|
+
"""
|
|
1025
|
+
references: set[str] = set()
|
|
1026
|
+
stack: list[tuple[int, Any]] = [(0, root)]
|
|
1027
|
+
while stack:
|
|
1028
|
+
depth, node = stack.pop()
|
|
1029
|
+
if depth > _MAX_REFERENCE_WALK_DEPTH:
|
|
1030
|
+
continue
|
|
1031
|
+
if isinstance(node, dict):
|
|
1032
|
+
value = node.get("sub_agent_log_file")
|
|
1033
|
+
if isinstance(value, str) and value:
|
|
1034
|
+
references.add(value)
|
|
1035
|
+
stack.extend((depth + 1, child) for child in node.values() if isinstance(child, (dict, list)))
|
|
1036
|
+
elif isinstance(node, list):
|
|
1037
|
+
stack.extend((depth + 1, child) for child in node if isinstance(child, (dict, list)))
|
|
1038
|
+
return references
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def is_safe_sub_agent_log_basename(name: str) -> bool:
|
|
1042
|
+
"""True iff *name* is a bare ``.json`` audit basename safe to join to a dir.
|
|
1043
|
+
|
|
1044
|
+
A ``sub_agent_log_file`` reference is chrys-authored, but the fork joins it
|
|
1045
|
+
to the parent's owner-writable sessions dir, so reject anything that is not
|
|
1046
|
+
a plain in-directory filename (no separators, no ``.``/``..``, no leading
|
|
1047
|
+
dot) before using it as a copy source.
|
|
1048
|
+
"""
|
|
1049
|
+
if not name or not name.endswith(".json"):
|
|
1050
|
+
return False
|
|
1051
|
+
if name.startswith("."):
|
|
1052
|
+
return False
|
|
1053
|
+
if name in (".", ".."):
|
|
1054
|
+
return False
|
|
1055
|
+
return name == os.path.basename(name) and "/" not in name and "\\" not in name
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
def _atomic_write_json(path: Path, envelope: dict[str, Any]) -> None:
|
|
1059
|
+
# A surrogateescaped workspace path (meta primary_cwd/working_dirs) or any
|
|
1060
|
+
# other field is made write-safe at the sink: atomic_write_owner_only_text
|
|
1061
|
+
# encodes with backslashreplace, so this cannot crash on a lone surrogate.
|
|
1062
|
+
atomic_write_owner_only_text(
|
|
1063
|
+
path,
|
|
1064
|
+
json.dumps(envelope, indent=2, ensure_ascii=False, allow_nan=False),
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def _utc_now() -> str:
|
|
1069
|
+
return datetime.now(UTC).isoformat()
|