execweave 0.8.33__tar.gz → 0.8.34__tar.gz
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.
- {execweave-0.8.33 → execweave-0.8.34}/PKG-INFO +4 -1
- {execweave-0.8.33 → execweave-0.8.34}/pyproject.toml +4 -2
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/__init__.py +1 -1
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/_conversation_records_core.py +47 -6
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/_dashboard_shell_base.py +16 -1
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_conversation.py +11 -5
- execweave-0.8.34/src/execweave/content_integrity.py +289 -0
- execweave-0.8.34/src/execweave/controlled_checks.py +561 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_preview_codex.py +1 -1
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_records.py +4 -4
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_records_codex.py +24 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/dashboard_shell.py +16 -7
- execweave-0.8.34/src/execweave/delivery_status.py +94 -0
- execweave-0.8.34/src/execweave/finalization.py +58 -0
- execweave-0.8.34/src/execweave/investigation_index.py +444 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live.py +9 -5
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_core.py +29 -5
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_script_c.py +16 -1
- execweave-0.8.34/src/execweave/redacted_archive.py +906 -0
- execweave-0.8.34/src/execweave/run_assessment.py +188 -0
- execweave-0.8.34/src/execweave/task_artifacts.py +192 -0
- execweave-0.8.34/src/execweave/task_validation.py +317 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_agent_panel.py +14 -6
- execweave-0.8.34/src/execweave/viewer_agent_panel_default.py +79 -0
- execweave-0.8.34/src/execweave/viewer_archive_verifier.py +178 -0
- execweave-0.8.34/src/execweave/viewer_content_browser.py +307 -0
- execweave-0.8.34/src/execweave/viewer_content_health.py +208 -0
- execweave-0.8.34/src/execweave/viewer_controlled_checks.py +258 -0
- execweave-0.8.34/src/execweave/viewer_conversation_sync.py +115 -0
- execweave-0.8.34/src/execweave/viewer_delivery_status.py +155 -0
- execweave-0.8.34/src/execweave/viewer_framework_model_output.py +133 -0
- execweave-0.8.34/src/execweave/viewer_history_browser.py +225 -0
- execweave-0.8.34/src/execweave/viewer_investigation.py +268 -0
- execweave-0.8.34/src/execweave/viewer_message_handoffs.py +114 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_projection.py +19 -4
- execweave-0.8.34/src/execweave/viewer_recorded_sources.py +146 -0
- execweave-0.8.34/src/execweave/viewer_run_assessment.py +120 -0
- execweave-0.8.34/src/execweave/viewer_run_guide.py +175 -0
- execweave-0.8.34/src/execweave/viewer_runtime_evidence.py +258 -0
- execweave-0.8.34/src/execweave/viewer_structure_share.py +132 -0
- execweave-0.8.34/src/execweave/viewer_task_validation.py +241 -0
- execweave-0.8.34/src/execweave/viewer_workflow_mode.py +127 -0
- execweave-0.8.33/src/execweave/finalization.py +0 -44
- execweave-0.8.33/src/execweave/viewer_agent_panel_default.py +0 -36
- {execweave-0.8.33 → execweave-0.8.34}/.gitignore +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/LICENSE +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/README.md +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/__main__.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/_http_proxy_base.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/_http_proxy_bounded.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/_http_proxy_stage.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/acceptance_output.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/agent_bootstrap.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/agent_topology.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/agent_trace.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/agy_preview_sanitize.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/analysis.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/anthropic.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/anthropic_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/anthropic_full_fidelity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_adapter.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_adapter_base.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_full_fidelity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_full_fidelity_base.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_full_fidelity_collaboration_base.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_hook_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_record.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_subagent_linkage.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/antigravity_trace_capability.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/auto_specialized.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/backends.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/benchmark.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/claude_adapter.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/claude_child_transcript.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/claude_delegation.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/claude_full_fidelity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/claude_hook_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/claude_hook_contract.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/claude_model_observer.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/claude_record.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_adapter.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_full_fidelity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_hook_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_hook_entry.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_hook_lifecycle.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_message_diagnostics.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_message_transport_diagnostics.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_record.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_rollout_structures.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_rollout_trace.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/codex_rollout_trace_base.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/collector.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/command.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/content_evidence.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/content_store.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_archive.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_message_identity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_preview.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_preview_antigravity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_preview_claude.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_preview_common.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_preview_generic.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_preview_lines.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_preview_transcript.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_records_antigravity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_records_common.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/conversation_records_ollama.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/correlation.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cursor_adapter.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cursor_delegation.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cursor_delegation_base.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cursor_full_fidelity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cursor_hook_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cursor_hook_contract.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cursor_lifecycle.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/cursor_record.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/entry.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/evidence_availability.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/evidence_grade.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/fidelity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/filesystem.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/focus.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/framework_adapters/__init__.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/framework_adapters/autogen.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/framework_adapters/base.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/framework_adapters/camel.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/framework_adapters/camel_channel.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/framework_adapters/metagpt.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/framework_adapters/registry.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/graph.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/graph_ops.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/hook_input.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/http_proxy.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/http_proxy_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/inference_gateway.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/inference_gateway_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/inference_gateway_full_fidelity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/inference_identity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/inference_identity_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/integrity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/integrity_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/litellm_callback.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/litellm_callback_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_extra_style.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_markup.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_process_layout.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_readability.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_script_a.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_script_b.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_script_d.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/live_view_style.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/model_runtime.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/model_runtime_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/model_runtime_full_fidelity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/openai_compatible.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/openai_compatible_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/openai_compatible_full_fidelity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/opencode_adapter.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/opencode_event_contract.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/opencode_full_fidelity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/opencode_hook_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/opencode_plugin_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/opencode_record.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/opencode_task_linkage.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/overhead_benchmark.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/provider_capability.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/provider_lifecycle.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/provider_record.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/rule_pack.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/rule_pack_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/runtime_endpoint_availability.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/scalability_benchmark.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/schema.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/scope.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/semantic.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/session_summary.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/sink.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/strace_backend.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/stream_assembly.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/theme.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/top.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/top_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/validate.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/vendor/dagre.min.js +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/view_cli.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_agent_panel_antigravity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_agent_panel_claude.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_agent_panel_codex.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_agent_panel_cursor.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_agent_panel_ollama.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_agent_panel_opencode.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_agent_panel_registry.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_background_files.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_content_inspector.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_dashboard_clean.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_dashboard_focus.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_dashboard_hardening.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_dashboard_hardening_v2.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_dashboard_pr70.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_edge_decross.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_execution_flow.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_execution_flow_provider_normalization.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_execution_flow_safety.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_external_endpoints.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_flow_canvas.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_flow_layout.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_fold_state.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_framework_flow.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_gif.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_layout_geometry.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_layout_v2.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_layout_v2_detached.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_limits.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_live_layout.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_projection_base.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_projection_bridges.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_semantic_projection.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_session_flow.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/viewer_tool_mirror_identity.py +0 -0
- {execweave-0.8.33 → execweave-0.8.34}/src/execweave/workflow.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: execweave
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.34
|
|
4
4
|
Summary: Turn AI-agent runtime behavior into a local interactive execution graph.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Irish-kw/ExecWeave
|
|
6
6
|
Project-URL: Repository, https://github.com/Irish-kw/ExecWeave
|
|
@@ -42,6 +42,7 @@ Requires-Python: >=3.10
|
|
|
42
42
|
Requires-Dist: psutil>=5.9
|
|
43
43
|
Requires-Dist: watchdog>=4.0
|
|
44
44
|
Provides-Extra: dev
|
|
45
|
+
Requires-Dist: cryptography>=44; extra == 'dev'
|
|
45
46
|
Requires-Dist: pillow>=10.0; extra == 'dev'
|
|
46
47
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
47
48
|
Requires-Dist: pywinpty<4,>=3.0.5; (platform_system == 'Windows') and extra == 'dev'
|
|
@@ -56,6 +57,8 @@ Provides-Extra: release
|
|
|
56
57
|
Requires-Dist: build>=1.2; extra == 'release'
|
|
57
58
|
Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'release'
|
|
58
59
|
Requires-Dist: twine>=6.0; extra == 'release'
|
|
60
|
+
Provides-Extra: validation
|
|
61
|
+
Requires-Dist: cryptography>=44; extra == 'validation'
|
|
59
62
|
Description-Content-Type: text/markdown
|
|
60
63
|
|
|
61
64
|
# ExecWeave
|
|
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "execweave"
|
|
7
|
-
# ExecWeave v0.8.
|
|
8
|
-
version = "0.8.
|
|
7
|
+
# ExecWeave v0.8.34 release metadata
|
|
8
|
+
version = "0.8.34"
|
|
9
9
|
description = "Turn AI-agent runtime behavior into a local interactive execution graph."
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -42,7 +42,9 @@ dependencies = [
|
|
|
42
42
|
]
|
|
43
43
|
|
|
44
44
|
[project.optional-dependencies]
|
|
45
|
+
validation = ["cryptography>=44"]
|
|
45
46
|
dev = [
|
|
47
|
+
"cryptography>=44",
|
|
46
48
|
"pytest>=8.0",
|
|
47
49
|
"PyYAML>=6.0",
|
|
48
50
|
"pillow>=10.0",
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
|
+
from collections import Counter
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any
|
|
7
8
|
from .content_store import _filesystem_path
|
|
@@ -491,6 +492,7 @@ def conversation_record_entries(
|
|
|
491
492
|
for node in nodes
|
|
492
493
|
if isinstance(node.get("id"), str) and node.get("id")
|
|
493
494
|
}
|
|
495
|
+
source_counts = Counter(node.get("id") for node in nodes if isinstance(node.get("id"), str))
|
|
494
496
|
entries: list[dict[str, Any]] = []
|
|
495
497
|
for edge in edges:
|
|
496
498
|
target_id = edge.get("target")
|
|
@@ -542,6 +544,36 @@ def conversation_record_entries(
|
|
|
542
544
|
derived = preview.pop("derived_agent_previews", None) if isinstance(preview, dict) else None
|
|
543
545
|
if preview is not None:
|
|
544
546
|
entry["conversation_preview"] = preview
|
|
547
|
+
# Keep each framework model response attached to its own content reference.
|
|
548
|
+
# Conversation merging publishes only one aggregate preview and can otherwise
|
|
549
|
+
# leave the actual model bodies behind a later control/status message.
|
|
550
|
+
source_attrs = source.get("attributes") if isinstance(source, dict) else None
|
|
551
|
+
if (
|
|
552
|
+
isinstance(source_attrs, dict)
|
|
553
|
+
and source.get("type") == "agent"
|
|
554
|
+
and source_counts[source_id] == 1
|
|
555
|
+
and source_attrs.get("conversation_scope") == "framework_agent"
|
|
556
|
+
and reference["content_kind"] in {
|
|
557
|
+
"metagpt.model_response", "autogen.model_response", "camel.model_response"
|
|
558
|
+
}
|
|
559
|
+
and edge.get("relation") == "HAS_MODEL_CONTENT"
|
|
560
|
+
and all(
|
|
561
|
+
record.get(flag) is None or record.get(flag) is False
|
|
562
|
+
for record in (edge, edge.get("attributes") if isinstance(edge.get("attributes"), dict) else {})
|
|
563
|
+
for flag in ("inferred", "viewer_only")
|
|
564
|
+
)
|
|
565
|
+
and isinstance(preview, dict)
|
|
566
|
+
):
|
|
567
|
+
agent_path = preview.get("agent_path")
|
|
568
|
+
entry["model_response_preview"] = {
|
|
569
|
+
"schema_version": "1",
|
|
570
|
+
"source_id": source_id,
|
|
571
|
+
"agent_path": agent_path,
|
|
572
|
+
"messages": [dict(message) for message in preview.get("messages", [])
|
|
573
|
+
if isinstance(message, dict) and agent_path
|
|
574
|
+
and message.get("sender") == agent_path],
|
|
575
|
+
"messages_truncated": bool(preview.get("messages_truncated")),
|
|
576
|
+
}
|
|
545
577
|
entries.append(entry)
|
|
546
578
|
if isinstance(derived, list):
|
|
547
579
|
entries.extend(_derived_agent_entries(entry, derived))
|
|
@@ -810,14 +842,19 @@ def _mark_shared_evidence(entries: list[dict[str, Any]]) -> None:
|
|
|
810
842
|
def conversation_index_payload(
|
|
811
843
|
graph: dict[str, Any],
|
|
812
844
|
run_root: str | Path,
|
|
845
|
+
*,
|
|
846
|
+
investigation_cache=None,
|
|
847
|
+
include_investigation: bool = False,
|
|
813
848
|
) -> dict[str, Any]:
|
|
814
|
-
"""Build the conversation
|
|
849
|
+
"""Build the shared conversation projection, with opt-in investigation.
|
|
815
850
|
|
|
816
|
-
The
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
851
|
+
The default is the historical, lightweight conversation contract used by
|
|
852
|
+
ordinary Live polling. Event-stream inspection is requested explicitly by
|
|
853
|
+
the investigation reader and offline exporters. Both modes publish exactly
|
|
854
|
+
the same conversation entries; the extended mode only adds an index.
|
|
820
855
|
"""
|
|
856
|
+
from .investigation_index import build_investigation_index
|
|
857
|
+
|
|
821
858
|
root = Path(run_root).expanduser().resolve()
|
|
822
859
|
entries = conversation_record_entries(graph, root)
|
|
823
860
|
_mark_shared_injected_context(entries)
|
|
@@ -830,6 +867,10 @@ def conversation_index_payload(
|
|
|
830
867
|
return {
|
|
831
868
|
"schema_version": "0.3",
|
|
832
869
|
"scope": "run_local_provider_neutral_conversation_projection",
|
|
870
|
+
"session_id": graph.get("session_id") if isinstance(graph.get("session_id"), str) else None,
|
|
871
|
+
"source_path": graph.get("source_path") if isinstance(graph.get("source_path"), str) else None,
|
|
872
|
+
**({"investigation": build_investigation_index(graph, root, cache=investigation_cache)}
|
|
873
|
+
if include_investigation else {}),
|
|
833
874
|
"entry_count": len(entries),
|
|
834
875
|
"visible_message_count": visible_message_count,
|
|
835
876
|
"external_provider_folder_lookup_required": False,
|
|
@@ -852,7 +893,7 @@ def write_conversation_records(
|
|
|
852
893
|
root = Path(run_root).expanduser().resolve()
|
|
853
894
|
root.mkdir(parents=True, exist_ok=True)
|
|
854
895
|
if payload is None:
|
|
855
|
-
payload = conversation_index_payload(graph, root)
|
|
896
|
+
payload = conversation_index_payload(graph, root, include_investigation=True)
|
|
856
897
|
entries = payload["entries"]
|
|
857
898
|
json_path = root / "conversations.json"
|
|
858
899
|
markdown_path = root / "conversations.md"
|
|
@@ -4,7 +4,13 @@ import json
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from .live_view import LIVE_HTML as _BASE_LIVE_HTML
|
|
7
|
+
from .run_assessment import build_run_assessment
|
|
8
|
+
from .viewer_run_assessment import inject_run_assessment
|
|
9
|
+
from .viewer_investigation import inject_investigation
|
|
10
|
+
from .viewer_runtime_evidence import inject_runtime_evidence
|
|
7
11
|
from .viewer_agent_panel import inject_agent_panel
|
|
12
|
+
from .viewer_framework_model_output import inject_framework_model_output
|
|
13
|
+
from .viewer_content_browser import inject_content_browser
|
|
8
14
|
from .viewer_dashboard_clean import fold_budget_bootstrap, inject_live_dashboard_clean
|
|
9
15
|
from .viewer_dashboard_focus import inject_live_dashboard_focus
|
|
10
16
|
from .viewer_dashboard_pr70 import inject_pr70_dashboard_repairs
|
|
@@ -84,7 +90,9 @@ def _build_dashboard_html() -> str:
|
|
|
84
90
|
html = normalize_provider_execution_flow(
|
|
85
91
|
harden_execution_flow_projection(inject_execution_flow(html))
|
|
86
92
|
)
|
|
87
|
-
|
|
93
|
+
html = _align_agent_panel_topology(inject_agent_panel(_guard_compact_live_snapshot(html)))
|
|
94
|
+
html = inject_framework_model_output(html)
|
|
95
|
+
return inject_runtime_evidence(inject_investigation(inject_run_assessment(inject_content_browser(html))))
|
|
88
96
|
|
|
89
97
|
|
|
90
98
|
DASHBOARD_HTML = _build_dashboard_html()
|
|
@@ -103,14 +111,21 @@ def render_static_dashboard_html(
|
|
|
103
111
|
graph: dict[str, Any],
|
|
104
112
|
*,
|
|
105
113
|
conversation_entries: list[dict[str, Any]] | None = None,
|
|
114
|
+
investigation_index: dict[str, Any] | None = None,
|
|
106
115
|
) -> str:
|
|
107
116
|
"""Render the exact dashboard shell used by live, backed by embedded snapshots."""
|
|
117
|
+
# Derived presentation metadata belongs beside the raw graph, not in it.
|
|
118
|
+
assessment = graph.get("run_assessment")
|
|
119
|
+
if not isinstance(assessment, dict):
|
|
120
|
+
assessment = build_run_assessment(graph)
|
|
108
121
|
bootstrap = (
|
|
109
122
|
"<script>window.__execweaveStaticMode=true;"
|
|
110
123
|
f"{fold_budget_bootstrap()}"
|
|
111
124
|
f"{viewer_limits_bootstrap(resolve_viewer_limits())}"
|
|
125
|
+
f"window.__execweaveStaticRunAssessment={_safe_json(assessment)};"
|
|
112
126
|
f"window.__execweaveStaticGraph={_safe_json(graph)};"
|
|
113
127
|
f"window.__execweaveStaticConversations={_safe_json(conversation_entries or [])};"
|
|
128
|
+
f"window.__execweaveStaticInvestigation={_safe_json(investigation_index)};"
|
|
114
129
|
"</script>\n"
|
|
115
130
|
)
|
|
116
131
|
html = DASHBOARD_HTML.replace("<script>", bootstrap + "<script>", 1)
|
|
@@ -354,7 +354,7 @@ class _DerivedThreads:
|
|
|
354
354
|
return
|
|
355
355
|
self._messages.setdefault(str(agent_path), []).append(message)
|
|
356
356
|
|
|
357
|
-
def previews(self) -> list[dict[str, Any]]:
|
|
357
|
+
def previews(self, *, preserve_history: bool = False) -> list[dict[str, Any]]:
|
|
358
358
|
results: list[dict[str, Any]] = []
|
|
359
359
|
for agent_path in sorted(self._messages):
|
|
360
360
|
messages = sorted(
|
|
@@ -364,7 +364,7 @@ class _DerivedThreads:
|
|
|
364
364
|
str(message.get("timestamp") or ""),
|
|
365
365
|
),
|
|
366
366
|
)
|
|
367
|
-
truncated = len(messages) > _MAX_PREVIEW_MESSAGES
|
|
367
|
+
truncated = not preserve_history and len(messages) > _MAX_PREVIEW_MESSAGES
|
|
368
368
|
if truncated:
|
|
369
369
|
messages = messages[:10] + messages[-(_MAX_PREVIEW_MESSAGES - 10) :]
|
|
370
370
|
thread_id = self._thread_ids.get(agent_path)
|
|
@@ -393,9 +393,15 @@ def codex_rollout_preview(path: str | Path) -> dict[str, Any] | None:
|
|
|
393
393
|
return previews[0] if previews else None
|
|
394
394
|
|
|
395
395
|
|
|
396
|
-
def codex_rollout_previews(
|
|
396
|
+
def codex_rollout_previews(
|
|
397
|
+
path: str | Path, *, preserve_history: bool = False,
|
|
398
|
+
) -> list[dict[str, Any]]:
|
|
397
399
|
"""Extract agent-local conversations a single Codex rollout provides evidence for.
|
|
398
400
|
|
|
401
|
+
``preserve_history`` retains all normalized message records for the Dashboard;
|
|
402
|
+
the default remains the bounded preview used by existing callers. This does
|
|
403
|
+
not change per-message text limits or the provider visibility boundary.
|
|
404
|
+
|
|
399
405
|
The first result is the rollout's own thread. Codex subagent rollouts can
|
|
400
406
|
physically contain inherited parent history: newer paginated rollouts persist
|
|
401
407
|
ordinals on each line, while older compatible rollouts derive them from physical
|
|
@@ -597,7 +603,7 @@ def codex_rollout_previews(path: str | Path) -> list[dict[str, Any]]:
|
|
|
597
603
|
)
|
|
598
604
|
except (OSError, RuntimeError, UnicodeError):
|
|
599
605
|
return []
|
|
600
|
-
truncated = len(messages) > _MAX_PREVIEW_MESSAGES
|
|
606
|
+
truncated = not preserve_history and len(messages) > _MAX_PREVIEW_MESSAGES
|
|
601
607
|
if truncated:
|
|
602
608
|
messages = messages[:10] + messages[-(_MAX_PREVIEW_MESSAGES - 10) :]
|
|
603
609
|
owner = {
|
|
@@ -612,4 +618,4 @@ def codex_rollout_previews(path: str | Path) -> list[dict[str, Any]]:
|
|
|
612
618
|
"messages_truncated": truncated,
|
|
613
619
|
"messages": messages,
|
|
614
620
|
}
|
|
615
|
-
return [owner, *derived.previews()]
|
|
621
|
+
return [owner, *derived.previews(preserve_history=preserve_history)]
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Verify declared archive references without inspecting provider payload bodies.
|
|
2
|
+
|
|
3
|
+
The scope is the exported graph's observed_content nodes and conversation-index
|
|
4
|
+
entries, not provider recall or an adversary-resistant log. Payload JSON is a leaf:
|
|
5
|
+
strings/dictionaries supplied by agents never become additional filesystem paths.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import stat
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Iterator
|
|
17
|
+
|
|
18
|
+
CONTENT_PATH = re.compile(r"content/sha256/([0-9a-f]{64})\.(?:json|txt|bin)")
|
|
19
|
+
INDEXES = {"graph.json": "nodes", "conversations.json": "entries"}
|
|
20
|
+
EXPORTS = (*INDEXES, "viewer.html")
|
|
21
|
+
CHUNK = 1024 * 1024
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ArchiveReadError(Exception):
|
|
25
|
+
"""A bounded, content-free archive diagnostic."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _signature(value: os.stat_result) -> tuple[int, ...]:
|
|
29
|
+
return (value.st_dev, value.st_ino, value.st_size, value.st_mtime_ns, value.st_ctime_ns)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _reject_link(path: Path) -> None:
|
|
33
|
+
info = path.lstat()
|
|
34
|
+
if stat.S_ISLNK(info.st_mode) or getattr(info, "st_file_attributes", 0) & 0x400:
|
|
35
|
+
raise ArchiveReadError("unsafe_path") # Includes Windows reparse points.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _path_descriptor_stat(path: Path) -> os.stat_result:
|
|
39
|
+
"""Compare two open-file snapshots, not Windows stat/fstat timestamps.
|
|
40
|
+
|
|
41
|
+
Path-based stat and descriptor-based fstat need not expose the same ctime
|
|
42
|
+
semantics on supported Windows/Python versions. Reopening the recorded path
|
|
43
|
+
preserves the identity/size/time comparison without rounding timestamps or
|
|
44
|
+
ignoring changes. This handle is metadata-only and never reads payload bytes.
|
|
45
|
+
"""
|
|
46
|
+
_reject_link(path)
|
|
47
|
+
flags = os.O_RDONLY | getattr(os, "O_BINARY", 0)
|
|
48
|
+
flags |= getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
|
|
49
|
+
descriptor = os.open(path, flags)
|
|
50
|
+
try:
|
|
51
|
+
value = os.fstat(descriptor)
|
|
52
|
+
if not stat.S_ISREG(value.st_mode):
|
|
53
|
+
raise ArchiveReadError("not_regular_file")
|
|
54
|
+
return value
|
|
55
|
+
finally:
|
|
56
|
+
os.close(descriptor)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@contextmanager
|
|
60
|
+
def _open_regular(root: Path, relative: str) -> Iterator[Any]:
|
|
61
|
+
if relative not in EXPORTS and CONTENT_PATH.fullmatch(relative) is None:
|
|
62
|
+
raise ArchiveReadError("invalid_reference")
|
|
63
|
+
opened: list[int] = []
|
|
64
|
+
try:
|
|
65
|
+
parts = relative.split("/")
|
|
66
|
+
# Pin every directory on POSIX. O_NOFOLLOW prevents symlink traversal even
|
|
67
|
+
# when a directory entry is replaced between validation and opening.
|
|
68
|
+
if os.open in os.supports_dir_fd and hasattr(os, "O_NOFOLLOW"):
|
|
69
|
+
flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
|
|
70
|
+
parent = os.open(root, flags)
|
|
71
|
+
opened.append(parent)
|
|
72
|
+
for part in parts[:-1]:
|
|
73
|
+
parent = os.open(part, flags, dir_fd=parent)
|
|
74
|
+
opened.append(parent)
|
|
75
|
+
fd = os.open(parts[-1], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent)
|
|
76
|
+
else:
|
|
77
|
+
path = root
|
|
78
|
+
_reject_link(path)
|
|
79
|
+
for part in parts:
|
|
80
|
+
path = path / part
|
|
81
|
+
_reject_link(path)
|
|
82
|
+
# Windows has no Python dir_fd/O_NOFOLLOW equivalent. Reject reparse
|
|
83
|
+
# points before and after reading; do not claim tamper-proof storage.
|
|
84
|
+
fd = os.open(path, os.O_RDONLY | getattr(os, "O_BINARY", 0))
|
|
85
|
+
with os.fdopen(fd, "rb") as handle:
|
|
86
|
+
before = os.fstat(handle.fileno())
|
|
87
|
+
if not stat.S_ISREG(before.st_mode):
|
|
88
|
+
raise ArchiveReadError("not_regular_file")
|
|
89
|
+
yield handle
|
|
90
|
+
after = os.fstat(handle.fileno())
|
|
91
|
+
path = root
|
|
92
|
+
_reject_link(path)
|
|
93
|
+
for part in parts:
|
|
94
|
+
path = path / part
|
|
95
|
+
_reject_link(path)
|
|
96
|
+
if _signature(before) != _signature(after) or _signature(after) != _signature(_path_descriptor_stat(path)):
|
|
97
|
+
raise ArchiveReadError("changed_during_read")
|
|
98
|
+
except FileNotFoundError as exc:
|
|
99
|
+
raise ArchiveReadError("missing_file") from exc
|
|
100
|
+
except OSError as exc:
|
|
101
|
+
# Do not write raw exception messages: paths may contain private data.
|
|
102
|
+
raise ArchiveReadError("unreadable_or_unsafe_path") from exc
|
|
103
|
+
finally:
|
|
104
|
+
for fd in reversed(opened):
|
|
105
|
+
os.close(fd)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _read(root: Path, relative: str, limit: int, *, collect: bool = False) -> tuple[dict[str, Any], bytes]:
|
|
109
|
+
chunks: list[bytes] = []
|
|
110
|
+
digest = hashlib.sha256()
|
|
111
|
+
size = 0
|
|
112
|
+
with _open_regular(root, relative) as handle:
|
|
113
|
+
if os.fstat(handle.fileno()).st_size > limit:
|
|
114
|
+
raise ArchiveReadError("verification_limit")
|
|
115
|
+
while True:
|
|
116
|
+
chunk = handle.read(min(CHUNK, max(1, limit - size + 1)))
|
|
117
|
+
if not chunk:
|
|
118
|
+
break
|
|
119
|
+
size += len(chunk)
|
|
120
|
+
if size > limit:
|
|
121
|
+
raise ArchiveReadError("verification_limit")
|
|
122
|
+
digest.update(chunk)
|
|
123
|
+
if collect:
|
|
124
|
+
chunks.append(chunk)
|
|
125
|
+
return {"sha256": digest.hexdigest(), "size_bytes": size}, b"".join(chunks)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def hash_export(root: Path, name: str) -> dict[str, Any]:
|
|
129
|
+
"""Hash only a named, regular primary export, with bounded reads."""
|
|
130
|
+
if name not in EXPORTS:
|
|
131
|
+
raise ArchiveReadError("invalid_export")
|
|
132
|
+
return _read(root, name, 1024 * 1024 * 1024)[0]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _unique_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
136
|
+
result: dict[str, Any] = {}
|
|
137
|
+
for key, value in pairs:
|
|
138
|
+
if key in result:
|
|
139
|
+
raise ValueError("duplicate JSON key")
|
|
140
|
+
result[key] = value
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _constant(value: str) -> None:
|
|
145
|
+
raise ValueError("non-finite JSON value")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def audit_content_references(
|
|
149
|
+
run_root: str | Path,
|
|
150
|
+
*,
|
|
151
|
+
max_index_bytes: int = 64 * 1024 * 1024,
|
|
152
|
+
max_content_bytes: int = 1024 * 1024 * 1024,
|
|
153
|
+
max_references: int = 100_000,
|
|
154
|
+
) -> dict[str, Any]:
|
|
155
|
+
"""Verify registered references; limits and unknown sources never pass silently.
|
|
156
|
+
|
|
157
|
+
Missing size metadata on legacy references is accepted but not invented. Source
|
|
158
|
+
completeness is orthogonal: an intact blob of partial provider content can pass
|
|
159
|
+
byte-integrity verification without implying complete provider observation.
|
|
160
|
+
"""
|
|
161
|
+
for limit in (max_index_bytes, max_content_bytes, max_references):
|
|
162
|
+
if type(limit) is not int or limit < 0:
|
|
163
|
+
raise ValueError("limits must be nonnegative integers")
|
|
164
|
+
root = Path(run_root).expanduser().absolute()
|
|
165
|
+
report: dict[str, Any] = {
|
|
166
|
+
"schema_version": "0.1",
|
|
167
|
+
"scope": "declared_graph_and_conversation_content",
|
|
168
|
+
"state": "incomplete",
|
|
169
|
+
"reference_count": 0,
|
|
170
|
+
"reference_count_truncated": False,
|
|
171
|
+
"unique_file_count": 0,
|
|
172
|
+
"verified_file_count": 0,
|
|
173
|
+
"error_count": 0,
|
|
174
|
+
"errors": [],
|
|
175
|
+
"errors_truncated": False,
|
|
176
|
+
"index_files": {},
|
|
177
|
+
"verified_files": {},
|
|
178
|
+
"limits": {"index_bytes": max_index_bytes, "content_bytes": max_content_bytes,
|
|
179
|
+
"references": max_references},
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
def fail(code: str, location: str) -> None:
|
|
183
|
+
report["error_count"] += 1
|
|
184
|
+
if len(report["errors"]) < 100:
|
|
185
|
+
report["errors"].append({"code": code, "location": location})
|
|
186
|
+
else:
|
|
187
|
+
report["errors_truncated"] = True
|
|
188
|
+
|
|
189
|
+
# None is an unknown legacy size, not zero. A duplicate declaration must agree
|
|
190
|
+
# with every known size; a later valid reference cannot erase a conflict.
|
|
191
|
+
references: dict[str, tuple[str, int | None, str]] = {}
|
|
192
|
+
conflicted: set[str] = set()
|
|
193
|
+
limit_hit = False
|
|
194
|
+
|
|
195
|
+
def register(value: Any, location: str) -> None:
|
|
196
|
+
nonlocal limit_hit
|
|
197
|
+
if limit_hit:
|
|
198
|
+
return
|
|
199
|
+
report["reference_count"] += 1
|
|
200
|
+
if report["reference_count"] > max_references:
|
|
201
|
+
limit_hit = True
|
|
202
|
+
report["reference_count_truncated"] = True
|
|
203
|
+
fail("reference_limit", location)
|
|
204
|
+
return
|
|
205
|
+
if not isinstance(value, dict):
|
|
206
|
+
fail("invalid_reference", location)
|
|
207
|
+
return
|
|
208
|
+
path, digest, size = value.get("path"), value.get("sha256"), value.get("size_bytes")
|
|
209
|
+
match = CONTENT_PATH.fullmatch(path) if isinstance(path, str) else None
|
|
210
|
+
if (match is None or match[1] != digest or
|
|
211
|
+
("size_bytes" in value and (type(size) is not int or size < 0))):
|
|
212
|
+
fail("invalid_reference", location)
|
|
213
|
+
return
|
|
214
|
+
previous = references.get(path)
|
|
215
|
+
if previous and previous[1] is not None and size is not None and previous[1] != size:
|
|
216
|
+
conflicted.add(path)
|
|
217
|
+
fail("conflicting_reference", location)
|
|
218
|
+
return
|
|
219
|
+
if previous is None or previous[1] is None:
|
|
220
|
+
references[path] = (digest, size, location)
|
|
221
|
+
|
|
222
|
+
sessions: set[str] = set()
|
|
223
|
+
for name, key in INDEXES.items():
|
|
224
|
+
try:
|
|
225
|
+
fingerprint, raw = _read(root, name, max_index_bytes, collect=True)
|
|
226
|
+
report["index_files"][name] = fingerprint
|
|
227
|
+
data = json.loads(raw.decode("utf-8"), object_pairs_hook=_unique_keys, parse_constant=_constant)
|
|
228
|
+
if not isinstance(data, dict) or not isinstance(data.get(key), list):
|
|
229
|
+
raise ValueError("invalid index shape")
|
|
230
|
+
except ArchiveReadError as exc:
|
|
231
|
+
fail(str(exc), name)
|
|
232
|
+
continue
|
|
233
|
+
except (ValueError, RecursionError):
|
|
234
|
+
fail("invalid_index", name)
|
|
235
|
+
continue
|
|
236
|
+
session = data.get("session_id")
|
|
237
|
+
if isinstance(session, str) and session:
|
|
238
|
+
sessions.add(session)
|
|
239
|
+
for index, item in enumerate(data[key]):
|
|
240
|
+
location = f"{name}/{key}/{index}"
|
|
241
|
+
if not isinstance(item, dict):
|
|
242
|
+
fail("invalid_index_entry", location)
|
|
243
|
+
continue
|
|
244
|
+
if key == "nodes":
|
|
245
|
+
if item.get("type") == "observed_content":
|
|
246
|
+
register(item.get("attributes"), location)
|
|
247
|
+
else:
|
|
248
|
+
# Conversation entries are reference records. Do not walk preview
|
|
249
|
+
# messages, request objects, or arbitrary nested provider JSON.
|
|
250
|
+
if "path" in item or "sha256" in item:
|
|
251
|
+
register(item, location)
|
|
252
|
+
else:
|
|
253
|
+
fail("invalid_reference", location)
|
|
254
|
+
# A projected graph may retain hidden raw nodes under expansion.clusters.
|
|
255
|
+
if key == "nodes" and isinstance(data.get("expansion"), dict):
|
|
256
|
+
clusters = data["expansion"].get("clusters", {})
|
|
257
|
+
if not isinstance(clusters, dict):
|
|
258
|
+
fail("invalid_expansion", f"{name}/expansion/clusters")
|
|
259
|
+
continue
|
|
260
|
+
for cluster_index, cluster in enumerate(clusters.values()):
|
|
261
|
+
location = f"{name}/expansion/clusters/{cluster_index}"
|
|
262
|
+
if not isinstance(cluster, dict) or not isinstance(cluster.get("nodes"), list):
|
|
263
|
+
fail("invalid_expansion", location)
|
|
264
|
+
continue
|
|
265
|
+
for index, item in enumerate(cluster["nodes"]):
|
|
266
|
+
if isinstance(item, dict) and item.get("type") == "observed_content":
|
|
267
|
+
register(item.get("attributes"), f"{location}/nodes/{index}")
|
|
268
|
+
if len(sessions) > 1:
|
|
269
|
+
fail("index_session_mismatch", "graph.json/conversations.json")
|
|
270
|
+
report["unique_file_count"] = len(references)
|
|
271
|
+
budget = max_content_bytes
|
|
272
|
+
for path, (digest, size, location) in references.items():
|
|
273
|
+
if path in conflicted:
|
|
274
|
+
continue
|
|
275
|
+
try:
|
|
276
|
+
fingerprint, _ = _read(root, path, budget)
|
|
277
|
+
budget -= fingerprint["size_bytes"]
|
|
278
|
+
if fingerprint["sha256"] != digest:
|
|
279
|
+
raise ArchiveReadError("hash_mismatch")
|
|
280
|
+
if size is not None and fingerprint["size_bytes"] != size:
|
|
281
|
+
raise ArchiveReadError("size_mismatch")
|
|
282
|
+
report["verified_files"][path] = fingerprint
|
|
283
|
+
except ArchiveReadError as exc:
|
|
284
|
+
fail(str(exc), location)
|
|
285
|
+
report["verified_file_count"] = len(report["verified_files"])
|
|
286
|
+
report["completed_read_bytes"] = max_content_bytes - budget
|
|
287
|
+
if report["error_count"] == 0:
|
|
288
|
+
report["state"] = "complete"
|
|
289
|
+
return report
|