@jaguilar87/gaia 5.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +33 -0
- package/.claude-plugin/plugin.json +26 -0
- package/ARCHITECTURE.md +335 -0
- package/CHANGELOG.md +1298 -0
- package/CODE_OF_CONDUCT.md +11 -0
- package/CONTRIBUTING.md +146 -0
- package/INSTALL.md +436 -0
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/SECURITY.md +47 -0
- package/agents/README.md +78 -0
- package/agents/cloud-troubleshooter.md +73 -0
- package/agents/developer.md +65 -0
- package/agents/gaia-operator.md +64 -0
- package/agents/gaia-orchestrator.md +111 -0
- package/agents/gaia-planner.md +53 -0
- package/agents/gaia-system.md +71 -0
- package/agents/gitops-operator.md +61 -0
- package/agents/terraform-architect.md +63 -0
- package/bin/README.md +106 -0
- package/bin/cli/__init__.py +1 -0
- package/bin/cli/approvals.py +740 -0
- package/bin/cli/cleanup.py +562 -0
- package/bin/cli/context.py +283 -0
- package/bin/cli/doctor.py +651 -0
- package/bin/cli/history.py +305 -0
- package/bin/cli/memory.py +483 -0
- package/bin/cli/metrics.py +1068 -0
- package/bin/cli/plans.py +515 -0
- package/bin/cli/status.py +302 -0
- package/bin/cli/update.py +382 -0
- package/bin/gaia +112 -0
- package/bin/gaia-cleanup.js +531 -0
- package/bin/gaia-doctor.js +635 -0
- package/bin/gaia-evidence +126 -0
- package/bin/gaia-history.js +251 -0
- package/bin/gaia-metrics.js +1278 -0
- package/bin/gaia-review.js +269 -0
- package/bin/gaia-scan +44 -0
- package/bin/gaia-scan.py +589 -0
- package/bin/gaia-skills-diagnose.js +929 -0
- package/bin/gaia-status.js +278 -0
- package/bin/gaia-uninstall.js +111 -0
- package/bin/gaia-update.js +919 -0
- package/bin/pre-publish-validate.js +610 -0
- package/bin/python-detect.js +60 -0
- package/bin/validate-sandbox.sh +601 -0
- package/commands/README.md +64 -0
- package/commands/gaia.md +37 -0
- package/commands/scan-project.md +67 -0
- package/config/README.md +71 -0
- package/config/cloud/aws.json +134 -0
- package/config/cloud/gcp.json +139 -0
- package/config/context-contracts.json +158 -0
- package/config/crons-schema.md +81 -0
- package/config/git_standards.json +72 -0
- package/config/surface-routing.json +417 -0
- package/config/universal-rules.json +102 -0
- package/dist/gaia-ops/.claude-plugin/plugin.json +24 -0
- package/dist/gaia-ops/README.md +80 -0
- package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
- package/dist/gaia-ops/agents/developer.md +65 -0
- package/dist/gaia-ops/agents/gaia-operator.md +64 -0
- package/dist/gaia-ops/agents/gaia-orchestrator.md +111 -0
- package/dist/gaia-ops/agents/gaia-planner.md +53 -0
- package/dist/gaia-ops/agents/gaia-system.md +71 -0
- package/dist/gaia-ops/agents/gitops-operator.md +61 -0
- package/dist/gaia-ops/agents/terraform-architect.md +63 -0
- package/dist/gaia-ops/commands/gaia.md +37 -0
- package/dist/gaia-ops/config/README.md +71 -0
- package/dist/gaia-ops/config/cloud/aws.json +134 -0
- package/dist/gaia-ops/config/cloud/gcp.json +139 -0
- package/dist/gaia-ops/config/context-contracts.json +158 -0
- package/dist/gaia-ops/config/crons-schema.md +81 -0
- package/dist/gaia-ops/config/git_standards.json +72 -0
- package/dist/gaia-ops/config/surface-routing.json +417 -0
- package/dist/gaia-ops/config/universal-rules.json +102 -0
- package/dist/gaia-ops/hooks/adapters/__init__.py +52 -0
- package/dist/gaia-ops/hooks/adapters/base.py +219 -0
- package/dist/gaia-ops/hooks/adapters/channel.py +17 -0
- package/dist/gaia-ops/hooks/adapters/claude_code.py +1890 -0
- package/dist/gaia-ops/hooks/adapters/types.py +194 -0
- package/dist/gaia-ops/hooks/adapters/utils.py +25 -0
- package/dist/gaia-ops/hooks/hooks.json +192 -0
- package/dist/gaia-ops/hooks/modules/__init__.py +15 -0
- package/dist/gaia-ops/hooks/modules/agents/__init__.py +29 -0
- package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +647 -0
- package/dist/gaia-ops/hooks/modules/agents/response_contract.py +496 -0
- package/dist/gaia-ops/hooks/modules/agents/skill_injection_verifier.py +120 -0
- package/dist/gaia-ops/hooks/modules/agents/state_tracker.py +267 -0
- package/dist/gaia-ops/hooks/modules/agents/task_info_builder.py +74 -0
- package/dist/gaia-ops/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +152 -0
- package/dist/gaia-ops/hooks/modules/audit/__init__.py +28 -0
- package/dist/gaia-ops/hooks/modules/audit/event_detector.py +168 -0
- package/dist/gaia-ops/hooks/modules/audit/logger.py +131 -0
- package/dist/gaia-ops/hooks/modules/audit/metrics.py +134 -0
- package/dist/gaia-ops/hooks/modules/audit/workflow_auditor.py +611 -0
- package/dist/gaia-ops/hooks/modules/audit/workflow_recorder.py +296 -0
- package/dist/gaia-ops/hooks/modules/context/__init__.py +11 -0
- package/dist/gaia-ops/hooks/modules/context/agentic_loop_detector.py +165 -0
- package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +218 -0
- package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-ops/hooks/modules/context/context_injector.py +558 -0
- package/dist/gaia-ops/hooks/modules/context/context_writer.py +530 -0
- package/dist/gaia-ops/hooks/modules/context/contracts_loader.py +161 -0
- package/dist/gaia-ops/hooks/modules/core/__init__.py +40 -0
- package/dist/gaia-ops/hooks/modules/core/hook_entry.py +78 -0
- package/dist/gaia-ops/hooks/modules/core/paths.py +160 -0
- package/dist/gaia-ops/hooks/modules/core/plugin_mode.py +149 -0
- package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +577 -0
- package/dist/gaia-ops/hooks/modules/core/state.py +179 -0
- package/dist/gaia-ops/hooks/modules/core/stdin.py +24 -0
- package/dist/gaia-ops/hooks/modules/events/__init__.py +1 -0
- package/dist/gaia-ops/hooks/modules/events/event_writer.py +210 -0
- package/dist/gaia-ops/hooks/modules/memory/__init__.py +8 -0
- package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +216 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +122 -0
- package/dist/gaia-ops/hooks/modules/scanning/__init__.py +8 -0
- package/dist/gaia-ops/hooks/modules/scanning/scan_trigger.py +84 -0
- package/dist/gaia-ops/hooks/modules/security/__init__.py +120 -0
- package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +87 -0
- package/dist/gaia-ops/hooks/modules/security/approval_constants.py +23 -0
- package/dist/gaia-ops/hooks/modules/security/approval_grants.py +1638 -0
- package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +222 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +595 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +87 -0
- package/dist/gaia-ops/hooks/modules/security/command_semantics.py +181 -0
- package/dist/gaia-ops/hooks/modules/security/composition_rules.py +547 -0
- package/dist/gaia-ops/hooks/modules/security/flag_classifiers.py +873 -0
- package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +1131 -0
- package/dist/gaia-ops/hooks/modules/security/network_hosts.py +481 -0
- package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-ops/hooks/modules/security/shell_unwrapper.py +165 -0
- package/dist/gaia-ops/hooks/modules/security/tiers.py +196 -0
- package/dist/gaia-ops/hooks/modules/session/__init__.py +10 -0
- package/dist/gaia-ops/hooks/modules/session/pending_scanner.py +174 -0
- package/dist/gaia-ops/hooks/modules/session/session_context_writer.py +100 -0
- package/dist/gaia-ops/hooks/modules/session/session_event_injector.py +160 -0
- package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-ops/hooks/modules/session/session_registry.py +333 -0
- package/dist/gaia-ops/hooks/modules/tools/__init__.py +29 -0
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +1008 -0
- package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +231 -0
- package/dist/gaia-ops/hooks/modules/tools/hook_response.py +55 -0
- package/dist/gaia-ops/hooks/modules/tools/shell_parser.py +227 -0
- package/dist/gaia-ops/hooks/modules/tools/stage_decomposer.py +315 -0
- package/dist/gaia-ops/hooks/modules/tools/task_validator.py +294 -0
- package/dist/gaia-ops/hooks/modules/validation/__init__.py +23 -0
- package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +380 -0
- package/dist/gaia-ops/hooks/post_compact.py +43 -0
- package/dist/gaia-ops/hooks/post_tool_use.py +54 -0
- package/dist/gaia-ops/hooks/pre_compact.py +60 -0
- package/dist/gaia-ops/hooks/pre_tool_use.py +413 -0
- package/dist/gaia-ops/hooks/session_end_hook.py +77 -0
- package/dist/gaia-ops/hooks/session_start.py +81 -0
- package/dist/gaia-ops/hooks/stop_hook.py +70 -0
- package/dist/gaia-ops/hooks/subagent_start.py +71 -0
- package/dist/gaia-ops/hooks/subagent_stop.py +295 -0
- package/dist/gaia-ops/hooks/task_completed.py +70 -0
- package/dist/gaia-ops/hooks/user_prompt_submit.py +246 -0
- package/dist/gaia-ops/settings.json +72 -0
- package/dist/gaia-ops/skills/README.md +158 -0
- package/dist/gaia-ops/skills/agent-creation/SKILL.md +87 -0
- package/dist/gaia-ops/skills/agent-creation/examples.md +170 -0
- package/dist/gaia-ops/skills/agent-creation/reference.md +191 -0
- package/dist/gaia-ops/skills/agent-protocol/SKILL.md +93 -0
- package/dist/gaia-ops/skills/agent-protocol/examples.md +223 -0
- package/dist/gaia-ops/skills/agent-response/SKILL.md +69 -0
- package/dist/gaia-ops/skills/agentic-loop/SKILL.md +80 -0
- package/dist/gaia-ops/skills/agentic-loop/reference.md +378 -0
- package/dist/gaia-ops/skills/blog-writing/SKILL.md +98 -0
- package/dist/gaia-ops/skills/blog-writing/reference.md +130 -0
- package/dist/gaia-ops/skills/brief-spec/SKILL.md +185 -0
- package/dist/gaia-ops/skills/command-execution/SKILL.md +64 -0
- package/dist/gaia-ops/skills/command-execution/reference.md +83 -0
- package/dist/gaia-ops/skills/context-updater/SKILL.md +87 -0
- package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
- package/dist/gaia-ops/skills/developer-patterns/SKILL.md +50 -0
- package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
- package/dist/gaia-ops/skills/execution/SKILL.md +99 -0
- package/dist/gaia-ops/skills/fast-queries/SKILL.md +43 -0
- package/dist/gaia-ops/skills/gaia-compact/SKILL.md +74 -0
- package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +108 -0
- package/dist/gaia-ops/skills/gaia-patterns/reference.md +395 -0
- package/dist/gaia-ops/skills/gaia-planner/SKILL.md +37 -0
- package/dist/gaia-ops/skills/gaia-planner/reference.md +107 -0
- package/dist/gaia-ops/skills/gaia-release/SKILL.md +85 -0
- package/dist/gaia-ops/skills/gaia-release/reference.md +92 -0
- package/dist/gaia-ops/skills/gaia-self-check/SKILL.md +114 -0
- package/dist/gaia-ops/skills/gaia-self-check/reference.md +453 -0
- package/dist/gaia-ops/skills/gaia-verify/SKILL.md +77 -0
- package/dist/gaia-ops/skills/gaia-verify/reference.md +80 -0
- package/dist/gaia-ops/skills/git-conventions/SKILL.md +47 -0
- package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +60 -0
- package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
- package/dist/gaia-ops/skills/gmail-policy/SKILL.md +200 -0
- package/dist/gaia-ops/skills/gmail-policy/reference.md +150 -0
- package/dist/gaia-ops/skills/gmail-triage/SKILL.md +100 -0
- package/dist/gaia-ops/skills/gws-setup/SKILL.md +99 -0
- package/dist/gaia-ops/skills/gws-setup/reference.md +73 -0
- package/dist/gaia-ops/skills/investigation/SKILL.md +100 -0
- package/dist/gaia-ops/skills/memory-curation/SKILL.md +83 -0
- package/dist/gaia-ops/skills/memory-search/SKILL.md +88 -0
- package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +160 -0
- package/dist/gaia-ops/skills/orchestrator-approval/reference.md +174 -0
- package/dist/gaia-ops/skills/pending-approvals/SKILL.md +72 -0
- package/dist/gaia-ops/skills/pending-approvals/reference.md +214 -0
- package/dist/gaia-ops/skills/readme-writing/SKILL.md +71 -0
- package/dist/gaia-ops/skills/readme-writing/reference.md +188 -0
- package/dist/gaia-ops/skills/reference.md +135 -0
- package/dist/gaia-ops/skills/request-approval/SKILL.md +140 -0
- package/dist/gaia-ops/skills/request-approval/examples.md +140 -0
- package/dist/gaia-ops/skills/request-approval/reference.md +57 -0
- package/dist/gaia-ops/skills/schedule-task/SKILL.md +64 -0
- package/dist/gaia-ops/skills/schedule-task/reference.md +233 -0
- package/dist/gaia-ops/skills/security-tiers/SKILL.md +141 -0
- package/dist/gaia-ops/skills/security-tiers/destructive-commands-reference.md +623 -0
- package/dist/gaia-ops/skills/security-tiers/reference.md +39 -0
- package/dist/gaia-ops/skills/session-reflection/SKILL.md +69 -0
- package/dist/gaia-ops/skills/skill-creation/SKILL.md +92 -0
- package/dist/gaia-ops/skills/skill-creation/reference.md +29 -0
- package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +89 -0
- package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
- package/dist/gaia-ops/tools/__init__.py +9 -0
- package/dist/gaia-ops/tools/agentic-loop/decide-status.py +210 -0
- package/dist/gaia-ops/tools/agentic-loop/parse-metric.py +106 -0
- package/dist/gaia-ops/tools/agentic-loop/record-iteration.py +221 -0
- package/dist/gaia-ops/tools/context/README.md +132 -0
- package/dist/gaia-ops/tools/context/__init__.py +42 -0
- package/dist/gaia-ops/tools/context/_paths.py +20 -0
- package/dist/gaia-ops/tools/context/context_provider.py +721 -0
- package/dist/gaia-ops/tools/context/context_section_reader.py +342 -0
- package/dist/gaia-ops/tools/context/deep_merge.py +159 -0
- package/dist/gaia-ops/tools/context/pending_updates.py +760 -0
- package/dist/gaia-ops/tools/context/surface_router.py +278 -0
- package/dist/gaia-ops/tools/fast-queries/README.md +65 -0
- package/dist/gaia-ops/tools/fast-queries/__init__.py +30 -0
- package/dist/gaia-ops/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
- package/dist/gaia-ops/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
- package/dist/gaia-ops/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
- package/dist/gaia-ops/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
- package/dist/gaia-ops/tools/fast-queries/run_triage.sh +59 -0
- package/dist/gaia-ops/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
- package/dist/gaia-ops/tools/gaia_simulator/__init__.py +33 -0
- package/dist/gaia-ops/tools/gaia_simulator/cli.py +354 -0
- package/dist/gaia-ops/tools/gaia_simulator/extractor.py +457 -0
- package/dist/gaia-ops/tools/gaia_simulator/reporter.py +258 -0
- package/dist/gaia-ops/tools/gaia_simulator/routing_simulator.py +334 -0
- package/dist/gaia-ops/tools/gaia_simulator/runner.py +539 -0
- package/dist/gaia-ops/tools/gaia_simulator/skills_mapper.py +264 -0
- package/dist/gaia-ops/tools/memory/README.md +0 -0
- package/dist/gaia-ops/tools/memory/__init__.py +20 -0
- package/dist/gaia-ops/tools/memory/backfill_fts5.py +107 -0
- package/dist/gaia-ops/tools/memory/conflict_detector.py +295 -0
- package/dist/gaia-ops/tools/memory/episodic.py +1210 -0
- package/dist/gaia-ops/tools/memory/git_invalidator.py +262 -0
- package/dist/gaia-ops/tools/memory/paths.py +102 -0
- package/dist/gaia-ops/tools/memory/scoring.py +193 -0
- package/dist/gaia-ops/tools/memory/search_store.py +375 -0
- package/dist/gaia-ops/tools/persist_transcript_analysis.py +85 -0
- package/dist/gaia-ops/tools/review/__init__.py +1 -0
- package/dist/gaia-ops/tools/review/review_engine.py +157 -0
- package/dist/gaia-ops/tools/scan/__init__.py +35 -0
- package/dist/gaia-ops/tools/scan/config.py +247 -0
- package/dist/gaia-ops/tools/scan/merge.py +212 -0
- package/dist/gaia-ops/tools/scan/orchestrator.py +549 -0
- package/dist/gaia-ops/tools/scan/registry.py +127 -0
- package/dist/gaia-ops/tools/scan/scanners/__init__.py +18 -0
- package/dist/gaia-ops/tools/scan/scanners/base.py +137 -0
- package/dist/gaia-ops/tools/scan/scanners/environment.py +349 -0
- package/dist/gaia-ops/tools/scan/scanners/git.py +570 -0
- package/dist/gaia-ops/tools/scan/scanners/infrastructure.py +875 -0
- package/dist/gaia-ops/tools/scan/scanners/orchestration.py +600 -0
- package/dist/gaia-ops/tools/scan/scanners/stack.py +1085 -0
- package/dist/gaia-ops/tools/scan/scanners/tools.py +260 -0
- package/dist/gaia-ops/tools/scan/setup.py +686 -0
- package/dist/gaia-ops/tools/scan/tests/__init__.py +1 -0
- package/dist/gaia-ops/tools/scan/tests/conftest.py +796 -0
- package/dist/gaia-ops/tools/scan/tests/test_environment.py +323 -0
- package/dist/gaia-ops/tools/scan/tests/test_git.py +419 -0
- package/dist/gaia-ops/tools/scan/tests/test_infrastructure.py +382 -0
- package/dist/gaia-ops/tools/scan/tests/test_integration.py +920 -0
- package/dist/gaia-ops/tools/scan/tests/test_merge.py +269 -0
- package/dist/gaia-ops/tools/scan/tests/test_orchestration.py +304 -0
- package/dist/gaia-ops/tools/scan/tests/test_stack.py +604 -0
- package/dist/gaia-ops/tools/scan/tests/test_tools.py +349 -0
- package/dist/gaia-ops/tools/scan/ui.py +624 -0
- package/dist/gaia-ops/tools/scan/verify.py +270 -0
- package/dist/gaia-ops/tools/scan/walk.py +118 -0
- package/dist/gaia-ops/tools/scan/workspace.py +85 -0
- package/dist/gaia-ops/tools/validation/README.md +244 -0
- package/dist/gaia-ops/tools/validation/__init__.py +17 -0
- package/dist/gaia-ops/tools/validation/approval_gate.py +321 -0
- package/dist/gaia-ops/tools/validation/validate_skills.py +189 -0
- package/dist/gaia-security/.claude-plugin/plugin.json +24 -0
- package/dist/gaia-security/README.md +90 -0
- package/dist/gaia-security/config/universal-rules.json +102 -0
- package/dist/gaia-security/hooks/adapters/__init__.py +52 -0
- package/dist/gaia-security/hooks/adapters/base.py +219 -0
- package/dist/gaia-security/hooks/adapters/channel.py +17 -0
- package/dist/gaia-security/hooks/adapters/claude_code.py +1890 -0
- package/dist/gaia-security/hooks/adapters/types.py +194 -0
- package/dist/gaia-security/hooks/adapters/utils.py +25 -0
- package/dist/gaia-security/hooks/hooks.json +113 -0
- package/dist/gaia-security/hooks/modules/__init__.py +15 -0
- package/dist/gaia-security/hooks/modules/agents/__init__.py +29 -0
- package/dist/gaia-security/hooks/modules/agents/contract_validator.py +647 -0
- package/dist/gaia-security/hooks/modules/agents/response_contract.py +496 -0
- package/dist/gaia-security/hooks/modules/agents/skill_injection_verifier.py +120 -0
- package/dist/gaia-security/hooks/modules/agents/state_tracker.py +267 -0
- package/dist/gaia-security/hooks/modules/agents/task_info_builder.py +74 -0
- package/dist/gaia-security/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +152 -0
- package/dist/gaia-security/hooks/modules/audit/__init__.py +28 -0
- package/dist/gaia-security/hooks/modules/audit/event_detector.py +168 -0
- package/dist/gaia-security/hooks/modules/audit/logger.py +131 -0
- package/dist/gaia-security/hooks/modules/audit/metrics.py +134 -0
- package/dist/gaia-security/hooks/modules/audit/workflow_auditor.py +611 -0
- package/dist/gaia-security/hooks/modules/audit/workflow_recorder.py +296 -0
- package/dist/gaia-security/hooks/modules/context/__init__.py +11 -0
- package/dist/gaia-security/hooks/modules/context/agentic_loop_detector.py +165 -0
- package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +218 -0
- package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-security/hooks/modules/context/context_injector.py +558 -0
- package/dist/gaia-security/hooks/modules/context/context_writer.py +530 -0
- package/dist/gaia-security/hooks/modules/context/contracts_loader.py +161 -0
- package/dist/gaia-security/hooks/modules/core/__init__.py +40 -0
- package/dist/gaia-security/hooks/modules/core/hook_entry.py +78 -0
- package/dist/gaia-security/hooks/modules/core/paths.py +160 -0
- package/dist/gaia-security/hooks/modules/core/plugin_mode.py +149 -0
- package/dist/gaia-security/hooks/modules/core/plugin_setup.py +577 -0
- package/dist/gaia-security/hooks/modules/core/state.py +179 -0
- package/dist/gaia-security/hooks/modules/core/stdin.py +24 -0
- package/dist/gaia-security/hooks/modules/events/__init__.py +1 -0
- package/dist/gaia-security/hooks/modules/events/event_writer.py +210 -0
- package/dist/gaia-security/hooks/modules/memory/__init__.py +8 -0
- package/dist/gaia-security/hooks/modules/memory/episode_writer.py +216 -0
- package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +122 -0
- package/dist/gaia-security/hooks/modules/scanning/__init__.py +8 -0
- package/dist/gaia-security/hooks/modules/scanning/scan_trigger.py +84 -0
- package/dist/gaia-security/hooks/modules/security/__init__.py +120 -0
- package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +87 -0
- package/dist/gaia-security/hooks/modules/security/approval_constants.py +23 -0
- package/dist/gaia-security/hooks/modules/security/approval_grants.py +1638 -0
- package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-security/hooks/modules/security/approval_scopes.py +222 -0
- package/dist/gaia-security/hooks/modules/security/blocked_commands.py +595 -0
- package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +87 -0
- package/dist/gaia-security/hooks/modules/security/command_semantics.py +181 -0
- package/dist/gaia-security/hooks/modules/security/composition_rules.py +547 -0
- package/dist/gaia-security/hooks/modules/security/flag_classifiers.py +873 -0
- package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +1131 -0
- package/dist/gaia-security/hooks/modules/security/network_hosts.py +481 -0
- package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-security/hooks/modules/security/shell_unwrapper.py +165 -0
- package/dist/gaia-security/hooks/modules/security/tiers.py +196 -0
- package/dist/gaia-security/hooks/modules/session/__init__.py +10 -0
- package/dist/gaia-security/hooks/modules/session/pending_scanner.py +174 -0
- package/dist/gaia-security/hooks/modules/session/session_context_writer.py +100 -0
- package/dist/gaia-security/hooks/modules/session/session_event_injector.py +160 -0
- package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-security/hooks/modules/session/session_registry.py +333 -0
- package/dist/gaia-security/hooks/modules/tools/__init__.py +29 -0
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +1008 -0
- package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +231 -0
- package/dist/gaia-security/hooks/modules/tools/hook_response.py +55 -0
- package/dist/gaia-security/hooks/modules/tools/shell_parser.py +227 -0
- package/dist/gaia-security/hooks/modules/tools/stage_decomposer.py +315 -0
- package/dist/gaia-security/hooks/modules/tools/task_validator.py +294 -0
- package/dist/gaia-security/hooks/modules/validation/__init__.py +23 -0
- package/dist/gaia-security/hooks/modules/validation/commit_validator.py +380 -0
- package/dist/gaia-security/hooks/post_tool_use.py +54 -0
- package/dist/gaia-security/hooks/pre_tool_use.py +413 -0
- package/dist/gaia-security/hooks/session_end_hook.py +77 -0
- package/dist/gaia-security/hooks/session_start.py +81 -0
- package/dist/gaia-security/hooks/stop_hook.py +70 -0
- package/dist/gaia-security/hooks/user_prompt_submit.py +246 -0
- package/dist/gaia-security/settings.json +58 -0
- package/git-hooks/commit-msg +41 -0
- package/hooks/README.md +100 -0
- package/hooks/adapters/__init__.py +52 -0
- package/hooks/adapters/base.py +219 -0
- package/hooks/adapters/channel.py +17 -0
- package/hooks/adapters/claude_code.py +1890 -0
- package/hooks/adapters/types.py +194 -0
- package/hooks/adapters/utils.py +25 -0
- package/hooks/elicitation_result.py +179 -0
- package/hooks/hooks.json +84 -0
- package/hooks/modules/README.md +189 -0
- package/hooks/modules/__init__.py +15 -0
- package/hooks/modules/agents/__init__.py +29 -0
- package/hooks/modules/agents/contract_validator.py +647 -0
- package/hooks/modules/agents/response_contract.py +496 -0
- package/hooks/modules/agents/skill_injection_verifier.py +120 -0
- package/hooks/modules/agents/state_tracker.py +267 -0
- package/hooks/modules/agents/task_info_builder.py +74 -0
- package/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/hooks/modules/agents/transcript_reader.py +152 -0
- package/hooks/modules/audit/__init__.py +28 -0
- package/hooks/modules/audit/event_detector.py +168 -0
- package/hooks/modules/audit/logger.py +131 -0
- package/hooks/modules/audit/metrics.py +134 -0
- package/hooks/modules/audit/workflow_auditor.py +611 -0
- package/hooks/modules/audit/workflow_recorder.py +296 -0
- package/hooks/modules/context/__init__.py +11 -0
- package/hooks/modules/context/agentic_loop_detector.py +165 -0
- package/hooks/modules/context/anchor_tracker.py +317 -0
- package/hooks/modules/context/compact_context_builder.py +218 -0
- package/hooks/modules/context/context_freshness.py +145 -0
- package/hooks/modules/context/context_injector.py +558 -0
- package/hooks/modules/context/context_writer.py +530 -0
- package/hooks/modules/context/contracts_loader.py +161 -0
- package/hooks/modules/core/__init__.py +40 -0
- package/hooks/modules/core/hook_entry.py +78 -0
- package/hooks/modules/core/paths.py +160 -0
- package/hooks/modules/core/plugin_mode.py +149 -0
- package/hooks/modules/core/plugin_setup.py +577 -0
- package/hooks/modules/core/state.py +179 -0
- package/hooks/modules/core/stdin.py +24 -0
- package/hooks/modules/events/__init__.py +1 -0
- package/hooks/modules/events/event_writer.py +210 -0
- package/hooks/modules/evidence/__init__.py +34 -0
- package/hooks/modules/evidence/assertions.py +137 -0
- package/hooks/modules/evidence/index_writer.py +57 -0
- package/hooks/modules/evidence/loader.py +126 -0
- package/hooks/modules/evidence/runner.py +241 -0
- package/hooks/modules/memory/__init__.py +8 -0
- package/hooks/modules/memory/episode_writer.py +216 -0
- package/hooks/modules/orchestrator/__init__.py +1 -0
- package/hooks/modules/orchestrator/delegate_mode.py +122 -0
- package/hooks/modules/scanning/__init__.py +8 -0
- package/hooks/modules/scanning/scan_trigger.py +84 -0
- package/hooks/modules/security/__init__.py +120 -0
- package/hooks/modules/security/approval_cleanup.py +87 -0
- package/hooks/modules/security/approval_constants.py +23 -0
- package/hooks/modules/security/approval_grants.py +1638 -0
- package/hooks/modules/security/approval_messages.py +71 -0
- package/hooks/modules/security/approval_scopes.py +222 -0
- package/hooks/modules/security/blocked_commands.py +595 -0
- package/hooks/modules/security/blocked_message_formatter.py +87 -0
- package/hooks/modules/security/command_semantics.py +181 -0
- package/hooks/modules/security/composition_rules.py +547 -0
- package/hooks/modules/security/flag_classifiers.py +873 -0
- package/hooks/modules/security/gitops_validator.py +179 -0
- package/hooks/modules/security/mutative_verbs.py +1131 -0
- package/hooks/modules/security/network_hosts.py +481 -0
- package/hooks/modules/security/prompt_validator.py +40 -0
- package/hooks/modules/security/shell_unwrapper.py +165 -0
- package/hooks/modules/security/tiers.py +196 -0
- package/hooks/modules/session/__init__.py +10 -0
- package/hooks/modules/session/pending_scanner.py +174 -0
- package/hooks/modules/session/session_context_writer.py +100 -0
- package/hooks/modules/session/session_event_injector.py +160 -0
- package/hooks/modules/session/session_manager.py +31 -0
- package/hooks/modules/session/session_registry.py +333 -0
- package/hooks/modules/tools/__init__.py +29 -0
- package/hooks/modules/tools/bash_validator.py +1008 -0
- package/hooks/modules/tools/cloud_pipe_validator.py +231 -0
- package/hooks/modules/tools/hook_response.py +55 -0
- package/hooks/modules/tools/shell_parser.py +227 -0
- package/hooks/modules/tools/stage_decomposer.py +315 -0
- package/hooks/modules/tools/task_validator.py +294 -0
- package/hooks/modules/validation/__init__.py +23 -0
- package/hooks/modules/validation/commit_validator.py +380 -0
- package/hooks/post_compact.py +43 -0
- package/hooks/post_tool_use.py +54 -0
- package/hooks/pre_compact.py +60 -0
- package/hooks/pre_tool_use.py +413 -0
- package/hooks/session_end_hook.py +77 -0
- package/hooks/session_start.py +81 -0
- package/hooks/stop_hook.py +70 -0
- package/hooks/subagent_start.py +71 -0
- package/hooks/subagent_stop.py +295 -0
- package/hooks/task_completed.py +70 -0
- package/hooks/user_prompt_submit.py +246 -0
- package/index.js +83 -0
- package/package.json +103 -0
- package/pyproject.toml +32 -0
- package/skills/README.md +158 -0
- package/skills/agent-creation/SKILL.md +87 -0
- package/skills/agent-creation/examples.md +170 -0
- package/skills/agent-creation/reference.md +191 -0
- package/skills/agent-protocol/SKILL.md +93 -0
- package/skills/agent-protocol/examples.md +223 -0
- package/skills/agent-response/SKILL.md +69 -0
- package/skills/agentic-loop/SKILL.md +80 -0
- package/skills/agentic-loop/reference.md +378 -0
- package/skills/blog-writing/SKILL.md +98 -0
- package/skills/blog-writing/reference.md +130 -0
- package/skills/brief-spec/SKILL.md +185 -0
- package/skills/command-execution/SKILL.md +64 -0
- package/skills/command-execution/reference.md +83 -0
- package/skills/context-updater/SKILL.md +87 -0
- package/skills/context-updater/examples.md +71 -0
- package/skills/developer-patterns/SKILL.md +50 -0
- package/skills/developer-patterns/reference.md +112 -0
- package/skills/execution/SKILL.md +99 -0
- package/skills/fast-queries/SKILL.md +43 -0
- package/skills/gaia-compact/SKILL.md +74 -0
- package/skills/gaia-patterns/SKILL.md +108 -0
- package/skills/gaia-patterns/reference.md +395 -0
- package/skills/gaia-planner/SKILL.md +37 -0
- package/skills/gaia-planner/reference.md +107 -0
- package/skills/gaia-release/SKILL.md +85 -0
- package/skills/gaia-release/reference.md +92 -0
- package/skills/gaia-self-check/SKILL.md +114 -0
- package/skills/gaia-self-check/reference.md +453 -0
- package/skills/gaia-verify/SKILL.md +77 -0
- package/skills/gaia-verify/reference.md +80 -0
- package/skills/git-conventions/SKILL.md +47 -0
- package/skills/gitops-patterns/SKILL.md +60 -0
- package/skills/gitops-patterns/reference.md +183 -0
- package/skills/gmail-policy/SKILL.md +200 -0
- package/skills/gmail-policy/reference.md +150 -0
- package/skills/gmail-triage/SKILL.md +100 -0
- package/skills/gws-setup/SKILL.md +99 -0
- package/skills/gws-setup/reference.md +73 -0
- package/skills/investigation/SKILL.md +100 -0
- package/skills/memory-curation/SKILL.md +83 -0
- package/skills/memory-search/SKILL.md +88 -0
- package/skills/orchestrator-approval/SKILL.md +160 -0
- package/skills/orchestrator-approval/reference.md +174 -0
- package/skills/pending-approvals/SKILL.md +72 -0
- package/skills/pending-approvals/reference.md +214 -0
- package/skills/readme-writing/SKILL.md +71 -0
- package/skills/readme-writing/reference.md +188 -0
- package/skills/reference.md +135 -0
- package/skills/request-approval/SKILL.md +140 -0
- package/skills/request-approval/examples.md +140 -0
- package/skills/request-approval/reference.md +57 -0
- package/skills/schedule-task/SKILL.md +64 -0
- package/skills/schedule-task/reference.md +233 -0
- package/skills/security-tiers/SKILL.md +141 -0
- package/skills/security-tiers/destructive-commands-reference.md +623 -0
- package/skills/security-tiers/reference.md +39 -0
- package/skills/session-reflection/SKILL.md +69 -0
- package/skills/skill-creation/SKILL.md +92 -0
- package/skills/skill-creation/reference.md +29 -0
- package/skills/terraform-patterns/SKILL.md +89 -0
- package/skills/terraform-patterns/reference.md +93 -0
- package/templates/README.md +69 -0
- package/templates/managed-settings.template.json +43 -0
- package/tools/__init__.py +9 -0
- package/tools/agentic-loop/decide-status.py +210 -0
- package/tools/agentic-loop/parse-metric.py +106 -0
- package/tools/agentic-loop/record-iteration.py +221 -0
- package/tools/context/README.md +132 -0
- package/tools/context/__init__.py +42 -0
- package/tools/context/_paths.py +20 -0
- package/tools/context/context_provider.py +721 -0
- package/tools/context/context_section_reader.py +342 -0
- package/tools/context/deep_merge.py +159 -0
- package/tools/context/pending_updates.py +760 -0
- package/tools/context/surface_router.py +278 -0
- package/tools/fast-queries/README.md +65 -0
- package/tools/fast-queries/__init__.py +30 -0
- package/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
- package/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
- package/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
- package/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
- package/tools/fast-queries/run_triage.sh +59 -0
- package/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
- package/tools/gaia_simulator/__init__.py +33 -0
- package/tools/gaia_simulator/cli.py +354 -0
- package/tools/gaia_simulator/extractor.py +457 -0
- package/tools/gaia_simulator/reporter.py +258 -0
- package/tools/gaia_simulator/routing_simulator.py +334 -0
- package/tools/gaia_simulator/runner.py +539 -0
- package/tools/gaia_simulator/skills_mapper.py +264 -0
- package/tools/memory/README.md +0 -0
- package/tools/memory/__init__.py +20 -0
- package/tools/memory/backfill_fts5.py +107 -0
- package/tools/memory/conflict_detector.py +295 -0
- package/tools/memory/episodic.py +1210 -0
- package/tools/memory/git_invalidator.py +262 -0
- package/tools/memory/paths.py +102 -0
- package/tools/memory/scoring.py +193 -0
- package/tools/memory/search_store.py +375 -0
- package/tools/persist_transcript_analysis.py +85 -0
- package/tools/review/__init__.py +1 -0
- package/tools/review/review_engine.py +157 -0
- package/tools/scan/__init__.py +35 -0
- package/tools/scan/config.py +247 -0
- package/tools/scan/merge.py +212 -0
- package/tools/scan/orchestrator.py +549 -0
- package/tools/scan/registry.py +127 -0
- package/tools/scan/scanners/__init__.py +18 -0
- package/tools/scan/scanners/base.py +137 -0
- package/tools/scan/scanners/environment.py +349 -0
- package/tools/scan/scanners/git.py +570 -0
- package/tools/scan/scanners/infrastructure.py +875 -0
- package/tools/scan/scanners/orchestration.py +600 -0
- package/tools/scan/scanners/stack.py +1085 -0
- package/tools/scan/scanners/tools.py +260 -0
- package/tools/scan/setup.py +686 -0
- package/tools/scan/tests/__init__.py +1 -0
- package/tools/scan/tests/conftest.py +796 -0
- package/tools/scan/tests/test_environment.py +323 -0
- package/tools/scan/tests/test_git.py +419 -0
- package/tools/scan/tests/test_infrastructure.py +382 -0
- package/tools/scan/tests/test_integration.py +920 -0
- package/tools/scan/tests/test_merge.py +269 -0
- package/tools/scan/tests/test_orchestration.py +304 -0
- package/tools/scan/tests/test_stack.py +604 -0
- package/tools/scan/tests/test_tools.py +349 -0
- package/tools/scan/ui.py +624 -0
- package/tools/scan/verify.py +270 -0
- package/tools/scan/walk.py +118 -0
- package/tools/scan/workspace.py +85 -0
- package/tools/validation/README.md +244 -0
- package/tools/validation/__init__.py +17 -0
- package/tools/validation/approval_gate.py +321 -0
- package/tools/validation/validate_skills.py +189 -0
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
"""
|
|
2
|
+
gaia approvals -- Approval System v2 Track 1 CLI subcommand.
|
|
3
|
+
|
|
4
|
+
Subcommands:
|
|
5
|
+
list [--json] [--session SESSION_ID] [--orphans-only]
|
|
6
|
+
-- list pending approvals
|
|
7
|
+
(--orphans-only filters to
|
|
8
|
+
pendings from dead sessions)
|
|
9
|
+
show APPROVAL_ID [--json] -- show full detail of one approval
|
|
10
|
+
reject NONCE [--reason REASON] -- reject a pending approval
|
|
11
|
+
reject --all [--reason REASON] -- reject ALL pending approvals in one call
|
|
12
|
+
clean [--dry-run] -- remove expired/stale approvals
|
|
13
|
+
stats [--json] -- approval system statistics
|
|
14
|
+
|
|
15
|
+
All subcommands exit 0 on success, 1 on error.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
# Ensure hooks/ is on sys.path so approval_grants resolves correctly.
|
|
26
|
+
# This mirrors the pattern used in bin/gaia-scan.py.
|
|
27
|
+
_SCRIPT_DIR = Path(__file__).resolve().parent
|
|
28
|
+
_BIN_DIR = _SCRIPT_DIR.parent
|
|
29
|
+
_PLUGIN_ROOT = _BIN_DIR.parent
|
|
30
|
+
_HOOKS_DIR = _PLUGIN_ROOT / "hooks"
|
|
31
|
+
|
|
32
|
+
for _p in [str(_HOOKS_DIR), str(_PLUGIN_ROOT)]:
|
|
33
|
+
if _p not in sys.path:
|
|
34
|
+
sys.path.insert(0, _p)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _import_approval_grants():
|
|
38
|
+
"""Import approval_grants lazily to allow mocking in tests."""
|
|
39
|
+
from modules.security.approval_grants import (
|
|
40
|
+
cleanup_expired_grants,
|
|
41
|
+
get_pending_approvals_for_session,
|
|
42
|
+
load_pending_by_nonce_prefix,
|
|
43
|
+
reject_pending,
|
|
44
|
+
)
|
|
45
|
+
return {
|
|
46
|
+
"cleanup_expired_grants": cleanup_expired_grants,
|
|
47
|
+
"get_pending_approvals_for_session": get_pending_approvals_for_session,
|
|
48
|
+
"load_pending_by_nonce_prefix": load_pending_by_nonce_prefix,
|
|
49
|
+
"reject_pending": reject_pending,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _import_grants_dir():
|
|
54
|
+
"""Get the grants directory path for approval files.
|
|
55
|
+
|
|
56
|
+
Resolution order mirrors get_plugin_data_dir() in paths.py:
|
|
57
|
+
1. CLAUDE_PLUGIN_DATA env var (set by Claude Code at runtime) -- data
|
|
58
|
+
lives at <CLAUDE_PLUGIN_DATA>/cache/approvals/.
|
|
59
|
+
2. Delegate to the approval_grants module which calls get_plugin_data_dir(),
|
|
60
|
+
which in turn walks up from CWD to find .claude/.
|
|
61
|
+
|
|
62
|
+
Keeping CLAUDE_PLUGIN_DATA as the first check ensures the CLI finds the
|
|
63
|
+
same approvals directory the hooks use when invoked from any working
|
|
64
|
+
directory (e.g. from inside gaia-ops-dev/ during development).
|
|
65
|
+
"""
|
|
66
|
+
import os
|
|
67
|
+
plugin_data = os.environ.get("CLAUDE_PLUGIN_DATA")
|
|
68
|
+
if plugin_data:
|
|
69
|
+
return Path(plugin_data) / "cache" / "approvals"
|
|
70
|
+
from modules.security.approval_grants import _get_grants_dir
|
|
71
|
+
return _get_grants_dir()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _import_approval_grants_module():
|
|
75
|
+
"""Return the approval_grants module object for direct attribute access.
|
|
76
|
+
|
|
77
|
+
Separate from _import_approval_grants() so cmd_clean can reset
|
|
78
|
+
_last_cleanup_time and call cleanup_expired_grants atomically on the
|
|
79
|
+
same module reference. Kept as a separate injectable function so tests
|
|
80
|
+
can mock it without touching sys.modules.
|
|
81
|
+
"""
|
|
82
|
+
import modules.security.approval_grants as ag_mod
|
|
83
|
+
return ag_mod
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# Formatting helpers
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
def _format_age(seconds: float) -> str:
|
|
91
|
+
"""Format seconds into a human-readable age string."""
|
|
92
|
+
if seconds < 60:
|
|
93
|
+
return f"{int(seconds)}s"
|
|
94
|
+
if seconds < 3600:
|
|
95
|
+
return f"{int(seconds / 60)}m"
|
|
96
|
+
if seconds < 86400:
|
|
97
|
+
return f"{int(seconds / 3600)}h"
|
|
98
|
+
return f"{int(seconds / 86400)}d"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _nonce_short(nonce: str) -> str:
|
|
102
|
+
"""Return the 8-char short form used in P-XXXX display."""
|
|
103
|
+
return nonce[:8] if nonce else "?"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _approval_id_label(nonce: str) -> str:
|
|
107
|
+
"""Return the P-XXXX label for display."""
|
|
108
|
+
return f"P-{_nonce_short(nonce)}"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _pending_to_display(p: dict) -> dict:
|
|
112
|
+
"""Convert a raw pending dict to a display-friendly dict."""
|
|
113
|
+
nonce = p.get("nonce", "")
|
|
114
|
+
ts = float(p.get("timestamp", 0))
|
|
115
|
+
age_secs = time.time() - ts if ts else 0
|
|
116
|
+
ctx = p.get("context") or {}
|
|
117
|
+
return {
|
|
118
|
+
"approval_id": _approval_id_label(nonce),
|
|
119
|
+
"nonce_prefix": _nonce_short(nonce),
|
|
120
|
+
"command": p.get("command", ""),
|
|
121
|
+
"verb": p.get("danger_verb", ""),
|
|
122
|
+
"category": p.get("danger_category", ""),
|
|
123
|
+
"age": _format_age(age_secs),
|
|
124
|
+
"age_seconds": round(age_secs),
|
|
125
|
+
"session_id": p.get("session_id", ""),
|
|
126
|
+
"source": ctx.get("source", ""),
|
|
127
|
+
"description": ctx.get("description", ""),
|
|
128
|
+
"risk": ctx.get("risk", ""),
|
|
129
|
+
"rollback": ctx.get("rollback", ""),
|
|
130
|
+
"branch": ctx.get("branch", ""),
|
|
131
|
+
"files_changed": ctx.get("files_changed", []),
|
|
132
|
+
"scope_type": p.get("scope_type", ""),
|
|
133
|
+
"timestamp": ts,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# Subcommand: list
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
def _scan_pending_shared(exclude_live_sessions: bool = False) -> list:
|
|
142
|
+
"""Return all non-expired, non-rejected pending approvals across all sessions.
|
|
143
|
+
|
|
144
|
+
Thin wrapper around the shared ``scan_pending_approvals`` in
|
|
145
|
+
``modules.session.pending_scanner`` so CLI and hook consumers share one
|
|
146
|
+
implementation of pending discovery + liveness filtering.
|
|
147
|
+
|
|
148
|
+
When ``exclude_live_sessions=True``, only pendings whose owning session
|
|
149
|
+
is NOT currently alive (orphans) are returned — this backs the
|
|
150
|
+
``--orphans-only`` flag.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
Exception: propagated from ``_import_grants_dir()`` so ``cmd_list``
|
|
154
|
+
can catch it and return exit code 1 consistently.
|
|
155
|
+
"""
|
|
156
|
+
# Let ImportError / other failures from _import_grants_dir propagate up.
|
|
157
|
+
grants_dir = _import_grants_dir()
|
|
158
|
+
|
|
159
|
+
from modules.session.pending_scanner import scan_pending_approvals
|
|
160
|
+
|
|
161
|
+
scanned = scan_pending_approvals(
|
|
162
|
+
grants_dir, exclude_live_sessions=exclude_live_sessions
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# scan_pending_approvals returns a display-ish shape; we rehydrate each
|
|
166
|
+
# scanned result back into the on-disk pending dict keys that
|
|
167
|
+
# _pending_to_display expects. Single source of truth for the scan,
|
|
168
|
+
# but the CLI's display contract is preserved unchanged.
|
|
169
|
+
results = []
|
|
170
|
+
for s in scanned:
|
|
171
|
+
results.append({
|
|
172
|
+
"nonce": s.get("nonce_full") or s.get("nonce_short", ""),
|
|
173
|
+
"session_id": s.get("pending_session_id", ""),
|
|
174
|
+
"command": s.get("command", ""),
|
|
175
|
+
"danger_verb": s.get("verb", ""),
|
|
176
|
+
"danger_category": s.get("category", ""),
|
|
177
|
+
"scope_type": s.get("scope_type", ""),
|
|
178
|
+
"timestamp": s.get("timestamp", 0),
|
|
179
|
+
"context": s.get("context", {}),
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
results.sort(key=lambda d: d.get("timestamp", 0), reverse=True)
|
|
183
|
+
return results
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def cmd_list(args) -> int:
|
|
187
|
+
"""List pending approvals.
|
|
188
|
+
|
|
189
|
+
Without ``--session``, all sessions are shown so the CLI is useful as a
|
|
190
|
+
cross-session review tool. With ``--session SESSION_ID``, only that
|
|
191
|
+
session's approvals are shown.
|
|
192
|
+
|
|
193
|
+
With ``--orphans-only``, only pendings whose owning session is no longer
|
|
194
|
+
alive (per session_registry) are shown. This is the operator-facing
|
|
195
|
+
tool for diagnosing cross-session drift after the T11/T12 liveness
|
|
196
|
+
plumbing landed.
|
|
197
|
+
"""
|
|
198
|
+
session_id = getattr(args, "session", None)
|
|
199
|
+
orphans_only = getattr(args, "orphans_only", False)
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
if session_id is None:
|
|
203
|
+
# All sessions -- scan directly so we don't filter by current session.
|
|
204
|
+
raw = _scan_pending_shared(exclude_live_sessions=orphans_only)
|
|
205
|
+
else:
|
|
206
|
+
ag = _import_approval_grants()
|
|
207
|
+
raw = ag["get_pending_approvals_for_session"](session_id)
|
|
208
|
+
except Exception as exc:
|
|
209
|
+
_print_error(f"Failed to load approvals: {exc}", args)
|
|
210
|
+
return 1
|
|
211
|
+
|
|
212
|
+
items = [_pending_to_display(p) for p in raw]
|
|
213
|
+
|
|
214
|
+
if getattr(args, "json", False):
|
|
215
|
+
print(json.dumps({"pending": items, "count": len(items)}, indent=2))
|
|
216
|
+
return 0
|
|
217
|
+
|
|
218
|
+
if not items:
|
|
219
|
+
print("No pending approvals.")
|
|
220
|
+
return 0
|
|
221
|
+
|
|
222
|
+
# Table output
|
|
223
|
+
print(f"{'ID':<12} {'AGE':<6} {'VERB':<10} {'SOURCE':<16} COMMAND")
|
|
224
|
+
print("-" * 70)
|
|
225
|
+
for item in items:
|
|
226
|
+
cmd_preview = item["command"][:40]
|
|
227
|
+
source = item["source"][:14] if item["source"] else "-"
|
|
228
|
+
print(
|
|
229
|
+
f"{item['approval_id']:<12} "
|
|
230
|
+
f"{item['age']:<6} "
|
|
231
|
+
f"{item['verb']:<10} "
|
|
232
|
+
f"{source:<16} "
|
|
233
|
+
f"{cmd_preview}"
|
|
234
|
+
)
|
|
235
|
+
print(f"\n{len(items)} pending approval(s).")
|
|
236
|
+
return 0
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
# Subcommand: show
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
def cmd_show(args) -> int:
|
|
244
|
+
"""Show full details of a specific pending approval."""
|
|
245
|
+
approval_id: str = args.approval_id.lstrip("P-").lstrip("p-")
|
|
246
|
+
# Strip leading 'P-' prefix if present
|
|
247
|
+
if approval_id.upper().startswith("P-"):
|
|
248
|
+
approval_id = approval_id[2:]
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
ag = _import_approval_grants()
|
|
252
|
+
raw = ag["load_pending_by_nonce_prefix"](approval_id)
|
|
253
|
+
except Exception as exc:
|
|
254
|
+
_print_error(f"Failed to load approval: {exc}", args)
|
|
255
|
+
return 1
|
|
256
|
+
|
|
257
|
+
if raw is None:
|
|
258
|
+
_print_error(f"No pending approval found for ID: P-{approval_id}", args)
|
|
259
|
+
return 1
|
|
260
|
+
|
|
261
|
+
item = _pending_to_display(raw)
|
|
262
|
+
env = raw.get("environment") or {}
|
|
263
|
+
cwd = raw.get("cwd", "")
|
|
264
|
+
|
|
265
|
+
if getattr(args, "json", False):
|
|
266
|
+
detail = dict(item)
|
|
267
|
+
detail["environment"] = env
|
|
268
|
+
detail["cwd"] = cwd
|
|
269
|
+
print(json.dumps(detail, indent=2))
|
|
270
|
+
return 0
|
|
271
|
+
|
|
272
|
+
# Human-readable detail
|
|
273
|
+
lines = [
|
|
274
|
+
f"Approval {item['approval_id']}",
|
|
275
|
+
"",
|
|
276
|
+
f" Command : {item['command']}",
|
|
277
|
+
f" Verb : {item['verb']} ({item['category']})",
|
|
278
|
+
f" Age : {item['age']}",
|
|
279
|
+
f" Session : {item['session_id']}",
|
|
280
|
+
f" Scope type: {item['scope_type']}",
|
|
281
|
+
]
|
|
282
|
+
if item["source"]:
|
|
283
|
+
lines.append(f" Source : {item['source']}")
|
|
284
|
+
if item["description"] and item["description"] != item["command"]:
|
|
285
|
+
lines.append(f" Desc : {item['description']}")
|
|
286
|
+
if item["risk"]:
|
|
287
|
+
lines.append(f" Risk : {item['risk']}")
|
|
288
|
+
if item["rollback"]:
|
|
289
|
+
lines.append(f" Rollback : {item['rollback']}")
|
|
290
|
+
if item["branch"]:
|
|
291
|
+
lines.append(f" Branch : {item['branch']}")
|
|
292
|
+
if item["files_changed"]:
|
|
293
|
+
lines.append(f" Files : {', '.join(item['files_changed'])}")
|
|
294
|
+
if cwd:
|
|
295
|
+
lines.append(f" CWD : {cwd}")
|
|
296
|
+
if env:
|
|
297
|
+
lines.append(f" Env keys : {', '.join(sorted(env.keys()))}")
|
|
298
|
+
lines.append("")
|
|
299
|
+
lines.append(f" To reject : gaia approvals reject {approval_id}")
|
|
300
|
+
print("\n".join(lines))
|
|
301
|
+
return 0
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
# Subcommand: reject
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
def cmd_reject(args) -> int:
|
|
309
|
+
"""Reject a pending approval by nonce prefix, or all pending approvals.
|
|
310
|
+
|
|
311
|
+
With ``--all``: rejects every non-expired pending approval across all
|
|
312
|
+
sessions. Exits 0 whether or not any approvals existed.
|
|
313
|
+
|
|
314
|
+
Without ``--all``: rejects the single approval identified by NONCE
|
|
315
|
+
(P-XXXX label or raw hex prefix). Exits 1 when not found.
|
|
316
|
+
"""
|
|
317
|
+
reject_all = getattr(args, "all", False)
|
|
318
|
+
reason = getattr(args, "reason", None)
|
|
319
|
+
|
|
320
|
+
if reject_all:
|
|
321
|
+
return _cmd_reject_all(args, reason)
|
|
322
|
+
|
|
323
|
+
# Single-reject path (original behavior)
|
|
324
|
+
nonce = getattr(args, "nonce", None)
|
|
325
|
+
if nonce is None:
|
|
326
|
+
_print_error("NONCE is required when --all is not specified.", args)
|
|
327
|
+
return 1
|
|
328
|
+
|
|
329
|
+
nonce = nonce.strip()
|
|
330
|
+
# Accept P-XXXX or raw hex prefix
|
|
331
|
+
if nonce.upper().startswith("P-"):
|
|
332
|
+
nonce = nonce[2:]
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
ag = _import_approval_grants()
|
|
336
|
+
ok = ag["reject_pending"](nonce)
|
|
337
|
+
except Exception as exc:
|
|
338
|
+
_print_error(f"Failed to reject approval: {exc}", args)
|
|
339
|
+
return 1
|
|
340
|
+
|
|
341
|
+
if ok:
|
|
342
|
+
msg = f"Rejected P-{nonce}"
|
|
343
|
+
if reason:
|
|
344
|
+
msg += f" (reason: {reason})"
|
|
345
|
+
if getattr(args, "json", False):
|
|
346
|
+
print(json.dumps({"status": "rejected", "nonce_prefix": nonce, "reason": reason}))
|
|
347
|
+
else:
|
|
348
|
+
print(msg)
|
|
349
|
+
return 0
|
|
350
|
+
else:
|
|
351
|
+
_print_error(f"No pending approval found for P-{nonce}", args)
|
|
352
|
+
return 1
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _cmd_reject_all(args, reason: str | None) -> int:
|
|
356
|
+
"""Reject all pending approvals across all sessions.
|
|
357
|
+
|
|
358
|
+
Scans the same queue that ``gaia approvals list`` shows, then calls
|
|
359
|
+
``reject_pending`` for each non-expired, non-rejected pending approval.
|
|
360
|
+
Exits 0 always -- an empty queue is not an error.
|
|
361
|
+
"""
|
|
362
|
+
try:
|
|
363
|
+
# Bulk reject operates on the full queue regardless of liveness;
|
|
364
|
+
# we intentionally pass exclude_live_sessions=False so the operator
|
|
365
|
+
# can clear orphaned and live-session pendings in one call.
|
|
366
|
+
raw = _scan_pending_shared(exclude_live_sessions=False)
|
|
367
|
+
except Exception as exc:
|
|
368
|
+
_print_error(f"Failed to load approvals: {exc}", args)
|
|
369
|
+
return 1
|
|
370
|
+
|
|
371
|
+
if not raw:
|
|
372
|
+
if getattr(args, "json", False):
|
|
373
|
+
print(json.dumps({"status": "ok", "rejected": 0, "ids": []}))
|
|
374
|
+
else:
|
|
375
|
+
print("No pending approvals to reject.")
|
|
376
|
+
return 0
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
ag = _import_approval_grants()
|
|
380
|
+
reject_fn = ag["reject_pending"]
|
|
381
|
+
except Exception as exc:
|
|
382
|
+
_print_error(f"Failed to load approval module: {exc}", args)
|
|
383
|
+
return 1
|
|
384
|
+
|
|
385
|
+
rejected_ids = []
|
|
386
|
+
failed_ids = []
|
|
387
|
+
for pending in raw:
|
|
388
|
+
nonce = pending.get("nonce", "")
|
|
389
|
+
nonce_prefix = _nonce_short(nonce)
|
|
390
|
+
try:
|
|
391
|
+
ok = reject_fn(nonce_prefix)
|
|
392
|
+
if ok:
|
|
393
|
+
rejected_ids.append(f"P-{nonce_prefix}")
|
|
394
|
+
else:
|
|
395
|
+
failed_ids.append(f"P-{nonce_prefix}")
|
|
396
|
+
except Exception:
|
|
397
|
+
failed_ids.append(f"P-{nonce_prefix}")
|
|
398
|
+
|
|
399
|
+
n = len(rejected_ids)
|
|
400
|
+
if getattr(args, "json", False):
|
|
401
|
+
payload: dict = {
|
|
402
|
+
"status": "ok" if not failed_ids else "partial",
|
|
403
|
+
"rejected": n,
|
|
404
|
+
"ids": rejected_ids,
|
|
405
|
+
}
|
|
406
|
+
if reason:
|
|
407
|
+
payload["reason"] = reason
|
|
408
|
+
if failed_ids:
|
|
409
|
+
payload["failed"] = failed_ids
|
|
410
|
+
print(json.dumps(payload))
|
|
411
|
+
else:
|
|
412
|
+
summary = f"Rejected {n} approval(s): {', '.join(rejected_ids)}"
|
|
413
|
+
if reason:
|
|
414
|
+
summary += f" (reason: {reason})"
|
|
415
|
+
print(summary)
|
|
416
|
+
if failed_ids:
|
|
417
|
+
_print_error(f"Failed to reject: {', '.join(failed_ids)}", args)
|
|
418
|
+
|
|
419
|
+
return 0 if not failed_ids else 1
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# ---------------------------------------------------------------------------
|
|
423
|
+
# Subcommand: clean
|
|
424
|
+
# ---------------------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
def cmd_clean(args) -> int:
|
|
427
|
+
"""Remove expired and stale approvals."""
|
|
428
|
+
dry_run = getattr(args, "dry_run", False)
|
|
429
|
+
|
|
430
|
+
if dry_run:
|
|
431
|
+
# Inspect without deleting -- count files that would be removed
|
|
432
|
+
try:
|
|
433
|
+
grants_dir = _import_grants_dir()
|
|
434
|
+
except Exception as exc:
|
|
435
|
+
_print_error(f"Cannot access approvals directory: {exc}", args)
|
|
436
|
+
return 1
|
|
437
|
+
|
|
438
|
+
if not grants_dir.exists():
|
|
439
|
+
msg = "Approvals directory does not exist. Nothing to clean."
|
|
440
|
+
if getattr(args, "json", False):
|
|
441
|
+
print(json.dumps({"dry_run": True, "would_remove": 0, "message": msg}))
|
|
442
|
+
else:
|
|
443
|
+
print(msg)
|
|
444
|
+
return 0
|
|
445
|
+
|
|
446
|
+
would_remove = _count_stale_files(grants_dir)
|
|
447
|
+
if getattr(args, "json", False):
|
|
448
|
+
print(json.dumps({"dry_run": True, "would_remove": would_remove}))
|
|
449
|
+
else:
|
|
450
|
+
print(f"Dry run: {would_remove} expired/stale file(s) would be removed.")
|
|
451
|
+
return 0
|
|
452
|
+
|
|
453
|
+
# Real cleanup -- reset throttle to force run
|
|
454
|
+
try:
|
|
455
|
+
ag_mod = _import_approval_grants_module()
|
|
456
|
+
ag_mod._last_cleanup_time = 0.0
|
|
457
|
+
cleaned = ag_mod.cleanup_expired_grants()
|
|
458
|
+
except Exception as exc:
|
|
459
|
+
_print_error(f"Cleanup failed: {exc}", args)
|
|
460
|
+
return 1
|
|
461
|
+
|
|
462
|
+
if getattr(args, "json", False):
|
|
463
|
+
print(json.dumps({"status": "ok", "cleaned": cleaned}))
|
|
464
|
+
else:
|
|
465
|
+
print(f"Cleaned {cleaned} expired/stale approval file(s).")
|
|
466
|
+
return 0
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _count_stale_files(grants_dir: Path) -> int:
|
|
470
|
+
"""Count expired grant and pending files without deleting them."""
|
|
471
|
+
count = 0
|
|
472
|
+
now = time.time()
|
|
473
|
+
|
|
474
|
+
for f in grants_dir.glob("grant-*.json"):
|
|
475
|
+
try:
|
|
476
|
+
data = json.loads(f.read_text())
|
|
477
|
+
granted_at = float(data.get("granted_at", 0))
|
|
478
|
+
ttl = int(data.get("ttl_minutes", 5))
|
|
479
|
+
if ttl > 0 and (now - granted_at) / 60 > ttl:
|
|
480
|
+
count += 1
|
|
481
|
+
except Exception:
|
|
482
|
+
count += 1
|
|
483
|
+
|
|
484
|
+
for f in grants_dir.glob("pending-*.json"):
|
|
485
|
+
if "index" in f.name:
|
|
486
|
+
continue
|
|
487
|
+
try:
|
|
488
|
+
data = json.loads(f.read_text())
|
|
489
|
+
if data.get("status") == "rejected":
|
|
490
|
+
count += 1
|
|
491
|
+
continue
|
|
492
|
+
ts = float(data.get("timestamp", 0))
|
|
493
|
+
ttl = int(data.get("ttl_minutes", 5))
|
|
494
|
+
if ttl > 0 and (now - ts) / 60 > ttl:
|
|
495
|
+
count += 1
|
|
496
|
+
except Exception:
|
|
497
|
+
count += 1
|
|
498
|
+
|
|
499
|
+
return count
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# ---------------------------------------------------------------------------
|
|
503
|
+
# Subcommand: stats
|
|
504
|
+
# ---------------------------------------------------------------------------
|
|
505
|
+
|
|
506
|
+
def cmd_stats(args) -> int:
|
|
507
|
+
"""Show approval system statistics."""
|
|
508
|
+
try:
|
|
509
|
+
ag = _import_approval_grants()
|
|
510
|
+
grants_dir = _import_grants_dir()
|
|
511
|
+
except Exception as exc:
|
|
512
|
+
_print_error(f"Failed to access approval system: {exc}", args)
|
|
513
|
+
return 1
|
|
514
|
+
|
|
515
|
+
# Gather data
|
|
516
|
+
all_sessions_pending = []
|
|
517
|
+
active_grants = []
|
|
518
|
+
rejected_count = 0
|
|
519
|
+
expired_pending_count = 0
|
|
520
|
+
now = time.time()
|
|
521
|
+
|
|
522
|
+
if grants_dir.exists():
|
|
523
|
+
for f in grants_dir.glob("pending-*.json"):
|
|
524
|
+
if "index" in f.name:
|
|
525
|
+
continue
|
|
526
|
+
try:
|
|
527
|
+
data = json.loads(f.read_text())
|
|
528
|
+
if data.get("status") == "rejected":
|
|
529
|
+
rejected_count += 1
|
|
530
|
+
continue
|
|
531
|
+
ts = float(data.get("timestamp", 0))
|
|
532
|
+
ttl = int(data.get("ttl_minutes", 5))
|
|
533
|
+
if ttl > 0 and (now - ts) / 60 > ttl:
|
|
534
|
+
expired_pending_count += 1
|
|
535
|
+
continue
|
|
536
|
+
all_sessions_pending.append(data)
|
|
537
|
+
except Exception:
|
|
538
|
+
pass
|
|
539
|
+
|
|
540
|
+
for f in grants_dir.glob("grant-*.json"):
|
|
541
|
+
try:
|
|
542
|
+
data = json.loads(f.read_text())
|
|
543
|
+
granted_at = float(data.get("granted_at", 0))
|
|
544
|
+
ttl = int(data.get("ttl_minutes", 5))
|
|
545
|
+
if ttl == 0 or (now - granted_at) / 60 <= ttl:
|
|
546
|
+
active_grants.append(data)
|
|
547
|
+
except Exception:
|
|
548
|
+
pass
|
|
549
|
+
|
|
550
|
+
# Current session pending
|
|
551
|
+
session_pending = ag["get_pending_approvals_for_session"]()
|
|
552
|
+
|
|
553
|
+
# Verb breakdown
|
|
554
|
+
verb_counts: dict = {}
|
|
555
|
+
for p in all_sessions_pending:
|
|
556
|
+
verb = p.get("danger_verb", "unknown")
|
|
557
|
+
verb_counts[verb] = verb_counts.get(verb, 0) + 1
|
|
558
|
+
|
|
559
|
+
stats = {
|
|
560
|
+
"pending_current_session": len(session_pending),
|
|
561
|
+
"pending_all_sessions": len(all_sessions_pending),
|
|
562
|
+
"active_grants": len(active_grants),
|
|
563
|
+
"rejected": rejected_count,
|
|
564
|
+
"expired_pending": expired_pending_count,
|
|
565
|
+
"verb_breakdown": verb_counts,
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if getattr(args, "json", False):
|
|
569
|
+
print(json.dumps(stats, indent=2))
|
|
570
|
+
return 0
|
|
571
|
+
|
|
572
|
+
print("Approval System Stats")
|
|
573
|
+
print("---------------------")
|
|
574
|
+
print(f" Pending (this session) : {stats['pending_current_session']}")
|
|
575
|
+
print(f" Pending (all sessions) : {stats['pending_all_sessions']}")
|
|
576
|
+
print(f" Active grants : {stats['active_grants']}")
|
|
577
|
+
print(f" Rejected (pending) : {stats['rejected']}")
|
|
578
|
+
print(f" Expired (pending) : {stats['expired_pending']}")
|
|
579
|
+
if verb_counts:
|
|
580
|
+
print(" Verb breakdown:")
|
|
581
|
+
for verb, cnt in sorted(verb_counts.items(), key=lambda x: -x[1]):
|
|
582
|
+
print(f" {verb:<16} {cnt}")
|
|
583
|
+
return 0
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
# ---------------------------------------------------------------------------
|
|
587
|
+
# Error helper
|
|
588
|
+
# ---------------------------------------------------------------------------
|
|
589
|
+
|
|
590
|
+
def _print_error(msg: str, args=None) -> None:
|
|
591
|
+
"""Print error in the appropriate format."""
|
|
592
|
+
if args and getattr(args, "json", False):
|
|
593
|
+
print(json.dumps({"error": msg}))
|
|
594
|
+
else:
|
|
595
|
+
print(f"Error: {msg}", file=sys.stderr)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
# ---------------------------------------------------------------------------
|
|
599
|
+
# Plugin registration (called by bin/gaia dispatcher)
|
|
600
|
+
# ---------------------------------------------------------------------------
|
|
601
|
+
|
|
602
|
+
def register(subparsers) -> None:
|
|
603
|
+
"""Register the 'approvals' subcommand group with the root parser."""
|
|
604
|
+
p = subparsers.add_parser(
|
|
605
|
+
"approvals",
|
|
606
|
+
help="Manage T3 pending approvals",
|
|
607
|
+
description="View, reject, and clean up Gaia approval requests.",
|
|
608
|
+
)
|
|
609
|
+
sub = p.add_subparsers(dest="approvals_cmd", metavar="SUBCOMMAND")
|
|
610
|
+
sub.required = True
|
|
611
|
+
|
|
612
|
+
# list
|
|
613
|
+
p_list = sub.add_parser("list", help="List pending approvals")
|
|
614
|
+
p_list.add_argument("--json", action="store_true", help="JSON output")
|
|
615
|
+
p_list.add_argument("--session", metavar="SESSION_ID", help="Filter by session ID")
|
|
616
|
+
p_list.add_argument(
|
|
617
|
+
"--orphans-only",
|
|
618
|
+
action="store_true",
|
|
619
|
+
dest="orphans_only",
|
|
620
|
+
help="Show only pendings from sessions no longer alive (via session_registry)",
|
|
621
|
+
)
|
|
622
|
+
p_list.set_defaults(func=cmd_list)
|
|
623
|
+
|
|
624
|
+
# show
|
|
625
|
+
p_show = sub.add_parser("show", help="Show detail for a specific approval")
|
|
626
|
+
p_show.add_argument("approval_id", metavar="APPROVAL_ID", help="P-XXXX identifier or nonce prefix")
|
|
627
|
+
p_show.add_argument("--json", action="store_true", help="JSON output")
|
|
628
|
+
p_show.set_defaults(func=cmd_show)
|
|
629
|
+
|
|
630
|
+
# reject
|
|
631
|
+
p_reject = sub.add_parser(
|
|
632
|
+
"reject",
|
|
633
|
+
help="Reject a pending approval (or all with --all)",
|
|
634
|
+
description=(
|
|
635
|
+
"Reject a pending T3 approval.\n\n"
|
|
636
|
+
"Single reject: provide NONCE (P-XXXX or raw hex prefix).\n"
|
|
637
|
+
"Bulk reject: use --all to reject every pending approval in one call."
|
|
638
|
+
),
|
|
639
|
+
)
|
|
640
|
+
p_reject.add_argument(
|
|
641
|
+
"nonce",
|
|
642
|
+
metavar="NONCE",
|
|
643
|
+
nargs="?",
|
|
644
|
+
help="P-XXXX identifier or nonce prefix (omit when using --all)",
|
|
645
|
+
)
|
|
646
|
+
p_reject.add_argument(
|
|
647
|
+
"--all",
|
|
648
|
+
action="store_true",
|
|
649
|
+
dest="all",
|
|
650
|
+
help="Reject ALL pending approvals (ignores NONCE)",
|
|
651
|
+
)
|
|
652
|
+
p_reject.add_argument("--reason", metavar="REASON", help="Rejection reason applied to all rejected approvals")
|
|
653
|
+
p_reject.add_argument("--json", action="store_true", help="JSON output")
|
|
654
|
+
p_reject.set_defaults(func=cmd_reject)
|
|
655
|
+
|
|
656
|
+
# clean
|
|
657
|
+
p_clean = sub.add_parser("clean", help="Remove expired/stale approvals")
|
|
658
|
+
p_clean.add_argument("--dry-run", action="store_true", dest="dry_run",
|
|
659
|
+
help="Show what would be removed without deleting")
|
|
660
|
+
p_clean.add_argument("--json", action="store_true", help="JSON output")
|
|
661
|
+
p_clean.set_defaults(func=cmd_clean)
|
|
662
|
+
|
|
663
|
+
# stats
|
|
664
|
+
p_stats = sub.add_parser("stats", help="Show approval system statistics")
|
|
665
|
+
p_stats.add_argument("--json", action="store_true", help="JSON output")
|
|
666
|
+
p_stats.set_defaults(func=cmd_stats)
|
|
667
|
+
|
|
668
|
+
p.set_defaults(func=_approvals_default)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def cmd_approvals(args) -> int:
|
|
672
|
+
"""Top-level dispatcher for 'gaia approvals'.
|
|
673
|
+
|
|
674
|
+
Called by bin/gaia which invokes cmd_{subcommand}(args). For grouped
|
|
675
|
+
subcommands like approvals, this function delegates to the specific
|
|
676
|
+
handler set via set_defaults(func=...) in register().
|
|
677
|
+
"""
|
|
678
|
+
func = getattr(args, "func", None)
|
|
679
|
+
if func is not None and func is not _approvals_default:
|
|
680
|
+
return func(args)
|
|
681
|
+
return _approvals_default(args)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _approvals_default(args) -> int:
|
|
685
|
+
"""Default handler when no sub-subcommand is given."""
|
|
686
|
+
print("Usage: gaia approvals {list,show,reject,clean,stats} [options]")
|
|
687
|
+
print(" gaia approvals reject --all [--reason TEXT] # bulk reject")
|
|
688
|
+
print("Run 'gaia approvals --help' for more information.")
|
|
689
|
+
return 0
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
# ---------------------------------------------------------------------------
|
|
693
|
+
# Standalone shim (for development/testing without bin/gaia)
|
|
694
|
+
# ---------------------------------------------------------------------------
|
|
695
|
+
|
|
696
|
+
def _build_standalone_parser() -> argparse.ArgumentParser:
|
|
697
|
+
parser = argparse.ArgumentParser(
|
|
698
|
+
prog="python bin/cli/approvals.py",
|
|
699
|
+
description="Gaia approvals subcommand (standalone mode)",
|
|
700
|
+
)
|
|
701
|
+
subparsers = parser.add_subparsers(dest="approvals_cmd", metavar="SUBCOMMAND")
|
|
702
|
+
subparsers.required = True
|
|
703
|
+
|
|
704
|
+
p_list = subparsers.add_parser("list", help="List pending approvals")
|
|
705
|
+
p_list.add_argument("--json", action="store_true")
|
|
706
|
+
p_list.add_argument("--session", metavar="SESSION_ID")
|
|
707
|
+
p_list.add_argument(
|
|
708
|
+
"--orphans-only", action="store_true", dest="orphans_only",
|
|
709
|
+
help="Show only pendings from sessions no longer alive",
|
|
710
|
+
)
|
|
711
|
+
p_list.set_defaults(func=cmd_list)
|
|
712
|
+
|
|
713
|
+
p_show = subparsers.add_parser("show", help="Show approval detail")
|
|
714
|
+
p_show.add_argument("approval_id", metavar="APPROVAL_ID")
|
|
715
|
+
p_show.add_argument("--json", action="store_true")
|
|
716
|
+
p_show.set_defaults(func=cmd_show)
|
|
717
|
+
|
|
718
|
+
p_reject = subparsers.add_parser("reject", help="Reject a pending approval (or all with --all)")
|
|
719
|
+
p_reject.add_argument("nonce", metavar="NONCE", nargs="?")
|
|
720
|
+
p_reject.add_argument("--all", action="store_true", dest="all", help="Reject all pending approvals")
|
|
721
|
+
p_reject.add_argument("--reason", metavar="REASON")
|
|
722
|
+
p_reject.add_argument("--json", action="store_true")
|
|
723
|
+
p_reject.set_defaults(func=cmd_reject)
|
|
724
|
+
|
|
725
|
+
p_clean = subparsers.add_parser("clean", help="Remove expired approvals")
|
|
726
|
+
p_clean.add_argument("--dry-run", action="store_true", dest="dry_run")
|
|
727
|
+
p_clean.add_argument("--json", action="store_true")
|
|
728
|
+
p_clean.set_defaults(func=cmd_clean)
|
|
729
|
+
|
|
730
|
+
p_stats = subparsers.add_parser("stats", help="Approval system stats")
|
|
731
|
+
p_stats.add_argument("--json", action="store_true")
|
|
732
|
+
p_stats.set_defaults(func=cmd_stats)
|
|
733
|
+
|
|
734
|
+
return parser
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
if __name__ == "__main__":
|
|
738
|
+
parser = _build_standalone_parser()
|
|
739
|
+
parsed = parser.parse_args()
|
|
740
|
+
sys.exit(parsed.func(parsed))
|