@jaguilar87/gaia 5.0.7 → 5.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +13 -0
  4. package/bin/README.md +6 -1
  5. package/bin/cli/approvals.py +486 -474
  6. package/bin/cli/brief.py +13 -0
  7. package/bin/cli/doctor.py +1 -1
  8. package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
  9. package/dist/gaia-ops/hooks/adapters/claude_code.py +92 -86
  10. package/dist/gaia-ops/hooks/modules/agents/handoff_persister.py +13 -2
  11. package/dist/gaia-ops/hooks/modules/context/context_injector.py +23 -7
  12. package/dist/gaia-ops/hooks/modules/events/event_writer.py +63 -96
  13. package/dist/gaia-ops/hooks/modules/security/__init__.py +0 -2
  14. package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +238 -69
  15. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +506 -1103
  16. package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +24 -1
  17. package/dist/gaia-ops/hooks/modules/session/pending_scanner.py +150 -90
  18. package/dist/gaia-ops/hooks/modules/session/session_manifest.py +257 -28
  19. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +19 -0
  20. package/dist/gaia-ops/hooks/post_compact.py +1 -0
  21. package/dist/gaia-ops/hooks/pre_compact.py +1 -0
  22. package/dist/gaia-ops/hooks/user_prompt_submit.py +20 -0
  23. package/dist/gaia-ops/skills/agent-approval-protocol/SKILL.md +50 -14
  24. package/dist/gaia-ops/skills/agent-approval-protocol/reference.md +16 -9
  25. package/dist/gaia-ops/skills/agent-protocol/examples.md +12 -1
  26. package/dist/gaia-ops/skills/gaia-patterns/reference.md +2 -2
  27. package/dist/gaia-ops/skills/orchestrator-present-approval/SKILL.md +69 -22
  28. package/dist/gaia-ops/skills/orchestrator-present-approval/reference.md +16 -3
  29. package/dist/gaia-ops/skills/orchestrator-present-approval/template.md +20 -14
  30. package/dist/gaia-ops/skills/pending-approvals/SKILL.md +16 -11
  31. package/dist/gaia-ops/skills/subagent-request-approval/SKILL.md +28 -3
  32. package/dist/gaia-ops/skills/subagent-request-approval/reference.md +34 -8
  33. package/dist/gaia-ops/tools/migration/README.md +10 -12
  34. package/dist/gaia-ops/tools/scan/orchestrator.py +194 -10
  35. package/dist/gaia-ops/tools/scan/tests/test_integration.py +1 -2
  36. package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
  37. package/dist/gaia-security/hooks/adapters/claude_code.py +92 -86
  38. package/dist/gaia-security/hooks/modules/agents/handoff_persister.py +13 -2
  39. package/dist/gaia-security/hooks/modules/context/context_injector.py +23 -7
  40. package/dist/gaia-security/hooks/modules/events/event_writer.py +63 -96
  41. package/dist/gaia-security/hooks/modules/security/__init__.py +0 -2
  42. package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +238 -69
  43. package/dist/gaia-security/hooks/modules/security/approval_grants.py +506 -1103
  44. package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +24 -1
  45. package/dist/gaia-security/hooks/modules/session/pending_scanner.py +150 -90
  46. package/dist/gaia-security/hooks/modules/session/session_manifest.py +257 -28
  47. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +19 -0
  48. package/dist/gaia-security/hooks/user_prompt_submit.py +20 -0
  49. package/gaia/approvals/__init__.py +2 -1
  50. package/gaia/approvals/store.py +165 -15
  51. package/gaia/store/schema.sql +38 -1
  52. package/gaia/store/writer.py +400 -0
  53. package/hooks/adapters/claude_code.py +92 -86
  54. package/hooks/elicitation_result.py +20 -75
  55. package/hooks/modules/agents/handoff_persister.py +13 -2
  56. package/hooks/modules/context/context_injector.py +23 -7
  57. package/hooks/modules/events/event_writer.py +63 -96
  58. package/hooks/modules/security/__init__.py +0 -2
  59. package/hooks/modules/security/approval_cleanup.py +238 -69
  60. package/hooks/modules/security/approval_grants.py +506 -1103
  61. package/hooks/modules/security/mutative_verbs.py +24 -1
  62. package/hooks/modules/session/pending_scanner.py +150 -90
  63. package/hooks/modules/session/session_manifest.py +257 -28
  64. package/hooks/modules/tools/bash_validator.py +19 -0
  65. package/hooks/post_compact.py +1 -0
  66. package/hooks/pre_compact.py +1 -0
  67. package/hooks/user_prompt_submit.py +20 -0
  68. package/package.json +1 -1
  69. package/pyproject.toml +1 -1
  70. package/scripts/bootstrap_database.sh +66 -17
  71. package/scripts/migrations/README.md +26 -14
  72. package/scripts/migrations/schema.checksum +2 -2
  73. package/scripts/migrations/v18_to_v19.sql +36 -0
  74. package/scripts/migrations/v19_to_v20.sql +20 -0
  75. package/skills/agent-approval-protocol/SKILL.md +50 -14
  76. package/skills/agent-approval-protocol/reference.md +16 -9
  77. package/skills/agent-protocol/examples.md +12 -1
  78. package/skills/gaia-patterns/reference.md +2 -2
  79. package/skills/orchestrator-present-approval/SKILL.md +69 -22
  80. package/skills/orchestrator-present-approval/reference.md +16 -3
  81. package/skills/orchestrator-present-approval/template.md +20 -14
  82. package/skills/pending-approvals/SKILL.md +16 -11
  83. package/skills/subagent-request-approval/SKILL.md +28 -3
  84. package/skills/subagent-request-approval/reference.md +34 -8
  85. package/tools/migration/README.md +10 -12
  86. package/tools/scan/orchestrator.py +194 -10
  87. package/tools/scan/tests/test_integration.py +1 -2
  88. package/bin/cli/plans.py +0 -517
  89. package/dist/gaia-ops/tools/context/deep_merge.py +0 -159
  90. package/dist/gaia-ops/tools/migration/migrate_04_harness_events.py +0 -132
  91. package/dist/gaia-ops/tools/migration/migrate_04_harness_events.sh +0 -23
  92. package/dist/gaia-ops/tools/scan/merge.py +0 -213
  93. package/dist/gaia-ops/tools/scan/tests/test_merge.py +0 -269
  94. package/gaia/approvals/revert.py +0 -282
  95. package/tools/context/deep_merge.py +0 -159
  96. package/tools/migration/migrate_04_harness_events.py +0 -132
  97. package/tools/migration/migrate_04_harness_events.sh +0 -23
  98. package/tools/scan/merge.py +0 -213
  99. package/tools/scan/tests/test_merge.py +0 -269
@@ -11,23 +11,26 @@ Pipeline:
11
11
  3. Collect and combine scanner sections (handling environment sub-keys)
12
12
  4. Cross-populate derived fields
13
13
  5. Return ScanOutput
14
+
15
+ Section ownership rules (inlined from the retired tools/scan/merge.py):
16
+ Rule 1: Scanner-owned sections -> full replace
17
+ Rule 2: Agent-enriched sections -> never touch
18
+ Rule 3: Mixed sections -> selective update at sub-key level
19
+ Rule 4: Unknown/user-custom sections -> preserve
20
+ Rule 5: Metadata -> always update
14
21
  """
15
22
 
23
+ import copy
16
24
  import logging
17
25
  import time
18
26
  from concurrent.futures import ThreadPoolExecutor, as_completed
19
27
  from dataclasses import dataclass, field
20
28
  from datetime import datetime, timezone
21
29
  from pathlib import Path
22
- from typing import Any, Dict, List, Optional
30
+ from typing import Any, Dict, List, Optional, Set
23
31
 
24
32
  from tools.scan import __version__ as scanner_package_version
25
33
  from tools.scan.config import ScanConfig
26
- from tools.scan.merge import (
27
- AGENT_ENRICHED_SECTIONS,
28
- collect_scanner_sections,
29
- merge_context,
30
- )
31
34
  from tools.scan.registry import ScannerRegistry
32
35
  from tools.scan.scanners.base import BaseScanner, ScanResult
33
36
  from tools.scan.workspace import WorkspaceInfo, detect_workspace_type
@@ -35,6 +38,190 @@ from tools.scan.workspace import WorkspaceInfo, detect_workspace_type
35
38
  logger = logging.getLogger(__name__)
36
39
 
37
40
 
41
+ # ---------------------------------------------------------------------------
42
+ # Section ownership constants (Rule 1 / Rule 2 / Rule 3)
43
+ # ---------------------------------------------------------------------------
44
+
45
+ # Sections fully owned by scanners -- replaced entirely on each scan (Rule 1)
46
+ # Top-level sections only; sub-key ownership handled separately
47
+ SCANNER_OWNED_TOP_LEVEL: Dict[str, str] = {
48
+ "project_identity": "stack",
49
+ "stack": "stack",
50
+ "git": "git",
51
+ "infrastructure": "infrastructure",
52
+ "orchestration": "orchestration",
53
+ # "environment" is NOT listed here because it has sub-key ownership
54
+ }
55
+
56
+ # Sub-key ownership within the `environment` section (Rule 4 / sub-section)
57
+ # Maps environment sub-key -> owning scanner name
58
+ ENVIRONMENT_SUBKEY_OWNERS: Dict[str, str] = {
59
+ "tools": "tools",
60
+ "tool_preferences": "tools",
61
+ "os": "environment",
62
+ "runtimes": "environment",
63
+ "env_files": "environment",
64
+ }
65
+
66
+ # Agent-enriched sections -- never modified by scanners (Rule 2)
67
+ AGENT_ENRICHED_SECTIONS: frozenset = frozenset([
68
+ "operational_guidelines",
69
+ "cluster_details",
70
+ "infrastructure_topology",
71
+ "monitoring_observability",
72
+ "architecture_overview",
73
+ "gcp_services",
74
+ "workload_identity",
75
+ ])
76
+
77
+ # Mixed sections with partial scanner ownership (Rule 3)
78
+ # Maps section_name -> set of scanner-owned field names
79
+ MIXED_SECTION_SCANNER_FIELDS: Dict[str, Set[str]] = {
80
+ "terraform_infrastructure": {"layout"},
81
+ "gitops_configuration": {"repository"},
82
+ "application_services": {"base_path", "services"},
83
+ }
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Section collection and merge helpers
88
+ # ---------------------------------------------------------------------------
89
+
90
+ def collect_scanner_sections(
91
+ scanner_results: Dict[str, Any],
92
+ ) -> Dict[str, Any]:
93
+ """Collect and combine sections from all scanner results.
94
+
95
+ Handles the environment section specially: both `tools` and `environment`
96
+ scanners produce sub-keys under `environment`, so their outputs are
97
+ combined into a single `environment` section.
98
+
99
+ Args:
100
+ scanner_results: Mapping of scanner_name -> ScanResult (must have
101
+ a `sections` attribute that is a dict).
102
+
103
+ Returns:
104
+ Combined sections dict from all scanners.
105
+ """
106
+ combined: Dict[str, Any] = {}
107
+ environment_parts: Dict[str, Any] = {}
108
+
109
+ for _scanner_name, scan_result in scanner_results.items():
110
+ sections = scan_result.sections if hasattr(scan_result, "sections") else {}
111
+
112
+ for section_name, section_data in sections.items():
113
+ if section_name == "environment":
114
+ # Merge environment sub-keys from both scanners
115
+ if isinstance(section_data, dict):
116
+ for key, value in section_data.items():
117
+ if key != "_source":
118
+ environment_parts[key] = value
119
+ else:
120
+ # Non-environment sections: direct assignment (last scanner wins,
121
+ # but each section should have exactly one owner)
122
+ combined[section_name] = section_data
123
+
124
+ # Reassemble environment section if we got any parts
125
+ if environment_parts:
126
+ combined["environment"] = {
127
+ "_source": "scanner:environment+tools",
128
+ **environment_parts,
129
+ }
130
+
131
+ return combined
132
+
133
+
134
+ def _merge_environment_section(
135
+ result: Dict[str, Any],
136
+ scan_sections: Dict[str, Any],
137
+ ) -> None:
138
+ """Merge the `environment` section with sub-key level ownership.
139
+
140
+ Two scanners contribute to the `environment` section:
141
+ - `tools` scanner owns: tools, tool_preferences
142
+ - `environment` scanner owns: os, runtimes, env_files
143
+
144
+ Each scanner's sub-keys replace their owned portion; the other scanner's
145
+ sub-keys are preserved. The `_source` field gets a combined tag.
146
+
147
+ Args:
148
+ result: The result dict being built (mutated in place).
149
+ scan_sections: Combined sections from all scanners.
150
+ """
151
+ if "environment" not in scan_sections:
152
+ return
153
+
154
+ scan_env = scan_sections["environment"]
155
+
156
+ if "environment" not in result:
157
+ result["environment"] = {}
158
+
159
+ env = result["environment"]
160
+
161
+ # Replace each sub-key based on ownership
162
+ for subkey in ENVIRONMENT_SUBKEY_OWNERS:
163
+ if subkey in scan_env:
164
+ env[subkey] = copy.deepcopy(scan_env[subkey])
165
+
166
+ # Set combined _source tag
167
+ env["_source"] = "scanner:environment+tools"
168
+
169
+
170
+ def _merge_sections(
171
+ existing: Dict[str, Any],
172
+ scan_sections: Dict[str, Any],
173
+ ) -> Dict[str, Any]:
174
+ """Merge scanner results with existing project-context sections.
175
+
176
+ Applies the ownership rules to produce the final merged sections dict.
177
+ Called with existing={} in normal scan runs (display-only path).
178
+
179
+ Args:
180
+ existing: Current sections (may be empty when called from scan).
181
+ scan_sections: Combined sections from all scanners.
182
+
183
+ Returns:
184
+ Merged sections dict. The merge is deterministic: same inputs always
185
+ produce the same output.
186
+ """
187
+ result = copy.deepcopy(existing)
188
+
189
+ # --- Rule 1: Scanner-owned top-level sections -> full replace ---
190
+ for section_name in SCANNER_OWNED_TOP_LEVEL:
191
+ if section_name in scan_sections:
192
+ result[section_name] = copy.deepcopy(scan_sections[section_name])
193
+
194
+ # --- Sub-section level ownership for `environment` ---
195
+ _merge_environment_section(result, scan_sections)
196
+
197
+ # --- Rule 2: Agent-enriched sections -> never touch ---
198
+ # These are already in `result` from the deepcopy of `existing`.
199
+ # (No action needed -- they are preserved by the deepcopy.)
200
+
201
+ # --- Rule 3: Mixed sections -> selective update ---
202
+ for section_name, scanner_fields in MIXED_SECTION_SCANNER_FIELDS.items():
203
+ if section_name in scan_sections:
204
+ scan_data = scan_sections[section_name]
205
+ if section_name not in result:
206
+ result[section_name] = {}
207
+ # Only update scanner-owned fields; preserve agent fields
208
+ for field_name in scanner_fields:
209
+ if field_name in scan_data:
210
+ result[section_name][field_name] = copy.deepcopy(
211
+ scan_data[field_name]
212
+ )
213
+
214
+ # --- Rule 5: Unknown/user-custom sections -> preserve ---
215
+ # Any section in `existing` not covered above is preserved by the deepcopy.
216
+ # We do NOT add new unknown sections from scan_sections.
217
+
218
+ return result
219
+
220
+
221
+ # ---------------------------------------------------------------------------
222
+ # ScanOutput dataclass
223
+ # ---------------------------------------------------------------------------
224
+
38
225
  @dataclass(frozen=True)
39
226
  class ScanOutput:
40
227
  """Aggregated output from all scanners.
@@ -187,11 +374,9 @@ class ScanOrchestrator:
187
374
  scan_sections = collect_scanner_sections(scanner_results)
188
375
 
189
376
  # Merge with empty existing context (no JSON persistence)
190
- section_owners = self.registry.get_section_owners()
191
- merged_sections = merge_context(
377
+ merged_sections = _merge_sections(
192
378
  existing={},
193
379
  scan_sections=scan_sections,
194
- section_owners=section_owners,
195
380
  )
196
381
 
197
382
  # Determine which sections were updated vs preserved
@@ -210,7 +395,6 @@ class ScanOrchestrator:
210
395
  self._cross_populate_monorepo(merged_sections)
211
396
 
212
397
  # Remove empty {} placeholders for agent-enriched and mixed sections
213
- from tools.scan.merge import MIXED_SECTION_SCANNER_FIELDS
214
398
  remove_if_empty = (
215
399
  AGENT_ENRICHED_SECTIONS
216
400
  | frozenset(MIXED_SECTION_SCANNER_FIELDS.keys())
@@ -23,8 +23,7 @@ from unittest.mock import patch
23
23
  import pytest
24
24
 
25
25
  from tools.scan.config import ScanConfig
26
- from tools.scan.merge import AGENT_ENRICHED_SECTIONS
27
- from tools.scan.orchestrator import ScanOrchestrator, ScanOutput
26
+ from tools.scan.orchestrator import AGENT_ENRICHED_SECTIONS, ScanOrchestrator, ScanOutput
28
27
  from tools.scan.registry import ScannerRegistry
29
28
  from tools.scan.scanners.base import BaseScanner, ScanResult
30
29
  from tools.scan.tests.conftest import create_git_dir