@jaguilar87/gaia 5.0.2 → 5.0.5

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 (154) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/ARCHITECTURE.md +0 -1
  4. package/CHANGELOG.md +110 -0
  5. package/INSTALL.md +0 -2
  6. package/README.md +1 -6
  7. package/bin/README.md +0 -1
  8. package/bin/cli/_install_helpers.py +1 -1
  9. package/bin/cli/approvals.py +23 -21
  10. package/bin/cli/cleanup.py +0 -1
  11. package/bin/cli/doctor.py +1 -1
  12. package/bin/cli/memory.py +2 -0
  13. package/bin/cli/update.py +1 -1
  14. package/bin/pre-publish-validate.js +48 -5
  15. package/config/README.md +22 -44
  16. package/config/surface-routing.json +0 -2
  17. package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
  18. package/dist/gaia-ops/config/README.md +22 -44
  19. package/dist/gaia-ops/config/surface-routing.json +0 -2
  20. package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +18 -0
  21. package/dist/gaia-ops/hooks/modules/agents/handoff_persister.py +214 -2
  22. package/dist/gaia-ops/hooks/modules/agents/response_contract.py +26 -0
  23. package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +15 -0
  24. package/dist/gaia-ops/hooks/modules/security/__init__.py +0 -5
  25. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +124 -19
  26. package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +99 -7
  27. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +127 -24
  28. package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +90 -55
  29. package/dist/gaia-ops/skills/README.md +1 -1
  30. package/dist/gaia-ops/skills/agent-contract-handoff/SKILL.md +3 -0
  31. package/dist/gaia-ops/skills/agent-response/SKILL.md +4 -2
  32. package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +1 -1
  33. package/dist/gaia-ops/skills/gaia-patterns/reference.md +2 -3
  34. package/dist/gaia-ops/skills/gaia-release/SKILL.md +60 -24
  35. package/dist/gaia-ops/skills/gaia-release/reference.md +35 -11
  36. package/dist/gaia-ops/skills/git-conventions/SKILL.md +6 -2
  37. package/dist/gaia-ops/skills/orchestrator-present-approval/SKILL.md +30 -7
  38. package/dist/gaia-ops/skills/orchestrator-present-approval/reference.md +32 -15
  39. package/dist/gaia-ops/skills/readme-writing/SKILL.md +1 -1
  40. package/dist/gaia-ops/skills/readme-writing/reference.md +0 -1
  41. package/dist/gaia-ops/skills/security-tiers/SKILL.md +5 -1
  42. package/dist/gaia-ops/skills/security-tiers/reference.md +3 -1
  43. package/dist/gaia-ops/skills/subagent-request-approval/SKILL.md +43 -6
  44. package/dist/gaia-ops/skills/subagent-request-approval/reference.md +66 -16
  45. package/dist/gaia-ops/tools/context/README.md +1 -1
  46. package/dist/gaia-ops/tools/gaia_simulator/extractor.py +0 -1
  47. package/dist/gaia-ops/tools/scan/ui.py +20 -4
  48. package/dist/gaia-ops/tools/scan/verify.py +3 -3
  49. package/dist/gaia-ops/tools/validation/README.md +15 -24
  50. package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
  51. package/dist/gaia-security/hooks/modules/agents/contract_validator.py +18 -0
  52. package/dist/gaia-security/hooks/modules/agents/handoff_persister.py +214 -2
  53. package/dist/gaia-security/hooks/modules/agents/response_contract.py +26 -0
  54. package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +15 -0
  55. package/dist/gaia-security/hooks/modules/security/__init__.py +0 -5
  56. package/dist/gaia-security/hooks/modules/security/approval_grants.py +124 -19
  57. package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +99 -7
  58. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +127 -24
  59. package/dist/gaia-security/hooks/modules/validation/commit_validator.py +90 -55
  60. package/gaia/state/transitions.py +4 -4
  61. package/gaia/store/writer.py +56 -0
  62. package/hooks/modules/README.md +2 -4
  63. package/hooks/modules/agents/contract_validator.py +18 -0
  64. package/hooks/modules/agents/handoff_persister.py +214 -2
  65. package/hooks/modules/agents/response_contract.py +26 -0
  66. package/hooks/modules/agents/transcript_reader.py +15 -0
  67. package/hooks/modules/security/__init__.py +0 -5
  68. package/hooks/modules/security/approval_grants.py +124 -19
  69. package/hooks/modules/security/mutative_verbs.py +99 -7
  70. package/hooks/modules/tools/bash_validator.py +127 -24
  71. package/hooks/modules/validation/commit_validator.py +90 -55
  72. package/index.js +2 -12
  73. package/package.json +4 -6
  74. package/pyproject.toml +3 -3
  75. package/scripts/bootstrap_database.sh +88 -439
  76. package/scripts/check_schema_drift.py +208 -0
  77. package/scripts/migrations/README.md +78 -28
  78. package/scripts/migrations/schema.checksum +8 -0
  79. package/scripts/release-prepare.mjs +199 -0
  80. package/skills/README.md +1 -1
  81. package/skills/agent-contract-handoff/SKILL.md +3 -0
  82. package/skills/agent-response/SKILL.md +4 -2
  83. package/skills/gaia-patterns/SKILL.md +1 -1
  84. package/skills/gaia-patterns/reference.md +2 -3
  85. package/skills/gaia-release/SKILL.md +60 -24
  86. package/skills/gaia-release/reference.md +35 -11
  87. package/skills/git-conventions/SKILL.md +6 -2
  88. package/skills/orchestrator-present-approval/SKILL.md +30 -7
  89. package/skills/orchestrator-present-approval/reference.md +32 -15
  90. package/skills/readme-writing/SKILL.md +1 -1
  91. package/skills/readme-writing/reference.md +0 -1
  92. package/skills/security-tiers/SKILL.md +5 -1
  93. package/skills/security-tiers/reference.md +3 -1
  94. package/skills/subagent-request-approval/SKILL.md +43 -6
  95. package/skills/subagent-request-approval/reference.md +66 -16
  96. package/tools/context/README.md +1 -1
  97. package/tools/gaia_simulator/extractor.py +0 -1
  98. package/tools/scan/ui.py +20 -4
  99. package/tools/scan/verify.py +3 -3
  100. package/tools/validation/README.md +15 -24
  101. package/commands/README.md +0 -64
  102. package/commands/gaia.md +0 -37
  103. package/commands/scan-project.md +0 -74
  104. package/config/crons-schema.md +0 -81
  105. package/config/git_standards.json +0 -72
  106. package/dist/gaia-ops/commands/gaia.md +0 -37
  107. package/dist/gaia-ops/config/crons-schema.md +0 -81
  108. package/dist/gaia-ops/config/git_standards.json +0 -72
  109. package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +0 -179
  110. package/dist/gaia-ops/tools/agentic-loop/decide-status.py +0 -210
  111. package/dist/gaia-ops/tools/agentic-loop/parse-metric.py +0 -106
  112. package/dist/gaia-ops/tools/agentic-loop/record-iteration.py +0 -223
  113. package/dist/gaia-security/hooks/modules/security/gitops_validator.py +0 -179
  114. package/git-hooks/commit-msg +0 -41
  115. package/hooks/modules/security/gitops_validator.py +0 -179
  116. package/scripts/migrations/v10_to_v11.sql +0 -170
  117. package/scripts/migrations/v10_to_v11_fresh.sql +0 -18
  118. package/scripts/migrations/v11_to_v12.sql +0 -195
  119. package/scripts/migrations/v11_to_v12_fresh.sql +0 -19
  120. package/scripts/migrations/v12_to_v13.sql +0 -48
  121. package/scripts/migrations/v12_to_v13_fresh.sql +0 -17
  122. package/scripts/migrations/v13_to_v14.sql +0 -44
  123. package/scripts/migrations/v13_to_v14_fresh.sql +0 -17
  124. package/scripts/migrations/v14_to_v15.sql +0 -71
  125. package/scripts/migrations/v14_to_v15_fresh.sql +0 -19
  126. package/scripts/migrations/v15_to_v16.sql +0 -57
  127. package/scripts/migrations/v15_to_v16_fresh.sql +0 -18
  128. package/scripts/migrations/v16_to_v17.sql +0 -51
  129. package/scripts/migrations/v16_to_v17_fresh.sql +0 -18
  130. package/scripts/migrations/v17_to_v18.sql +0 -66
  131. package/scripts/migrations/v17_to_v18_fresh.sql +0 -24
  132. package/scripts/migrations/v1_to_v2.sql +0 -97
  133. package/scripts/migrations/v2_to_v3.sql +0 -68
  134. package/scripts/migrations/v2_to_v3_merge.sql +0 -69
  135. package/scripts/migrations/v3_to_v4.sql +0 -67
  136. package/scripts/migrations/v3_to_v4_fresh.sql +0 -20
  137. package/scripts/migrations/v4_to_v5.sql +0 -55
  138. package/scripts/migrations/v4_to_v5_fresh.sql +0 -20
  139. package/scripts/migrations/v5_to_v6.sql +0 -48
  140. package/scripts/migrations/v5_to_v6_fresh.sql +0 -17
  141. package/scripts/migrations/v6_to_v7.sql +0 -26
  142. package/scripts/migrations/v6_to_v7_fresh.sql +0 -13
  143. package/scripts/migrations/v7_to_v8.sql +0 -44
  144. package/scripts/migrations/v7_to_v8_fresh.sql +0 -14
  145. package/scripts/migrations/v8_to_v9.sql +0 -87
  146. package/scripts/migrations/v8_to_v9_fresh.sql +0 -15
  147. package/scripts/migrations/v9_to_v10.sql +0 -109
  148. package/scripts/migrations/v9_to_v10_episodes_workspace.sql +0 -109
  149. package/scripts/migrations/v9_to_v10_fresh.sql +0 -18
  150. package/templates/README.md +0 -70
  151. package/templates/managed-settings.template.json +0 -43
  152. package/tools/agentic-loop/decide-status.py +0 -210
  153. package/tools/agentic-loop/parse-metric.py +0 -106
  154. package/tools/agentic-loop/record-iteration.py +0 -223
@@ -1,223 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- record-iteration.py
4
-
5
- Atomically update state.json and append to worklog.md after each iteration.
6
- The LLM never writes state.json directly — this script is the only writer.
7
-
8
- Usage:
9
- python3 record-iteration.py \
10
- --state-file state.json \
11
- --worklog worklog.md \
12
- --iteration 5 \
13
- --metric-value 94.5 \
14
- --status keep \
15
- --description "Handle hyphenated verbs" \
16
- --insight "delete-objects splits correctly" \
17
- --next "Check camelCase+hyphen combined"
18
-
19
- Optional flags:
20
- --changed TEXT What was modified (default: same as description)
21
- --metric-name TEXT Name of the metric recorded (default: "metric")
22
-
23
- Atomic write guarantee: state.json is written to a .tmp sibling, fsynced,
24
- then renamed over the original. Either the full write lands or the original
25
- is untouched.
26
- """
27
-
28
- from __future__ import annotations
29
-
30
- import argparse
31
- import json
32
- import os
33
- import sys
34
- import tempfile
35
- from datetime import datetime, timezone
36
-
37
-
38
- def load_state(path: str) -> dict:
39
- """Load existing state.json or return an empty skeleton."""
40
- if not os.path.exists(path):
41
- return {
42
- "iteration": 0,
43
- "current_metric": None,
44
- "best_metric": None,
45
- "consecutive_discards": 0,
46
- "pivot_count": 0,
47
- "timestamp": None,
48
- "status": None,
49
- }
50
- try:
51
- with open(path, "r") as fh:
52
- data = json.load(fh)
53
- return data
54
- except (OSError, json.JSONDecodeError) as exc:
55
- print(f"error: cannot read state file '{path}': {exc}", file=sys.stderr)
56
- sys.exit(1)
57
-
58
-
59
- def atomic_write_json(path: str, data: dict) -> None:
60
- """Write *data* to *path* atomically using write-fsync-rename."""
61
- dir_name = os.path.dirname(os.path.abspath(path))
62
- # Use a temp file in the same directory so rename is on the same filesystem.
63
- try:
64
- fd, tmp_path = tempfile.mkstemp(dir=dir_name, suffix=".tmp")
65
- try:
66
- with os.fdopen(fd, "w") as fh:
67
- json.dump(data, fh, indent=2)
68
- fh.write("\n")
69
- fh.flush()
70
- os.fsync(fh.fileno())
71
- os.replace(tmp_path, path)
72
- except Exception:
73
- # Clean up orphaned temp file on failure.
74
- try:
75
- os.unlink(tmp_path)
76
- except OSError:
77
- pass
78
- raise
79
- except OSError as exc:
80
- print(f"error: atomic write to '{path}' failed: {exc}", file=sys.stderr)
81
- sys.exit(1)
82
-
83
-
84
- def append_worklog(
85
- path: str,
86
- iteration: int,
87
- description: str,
88
- metric_name: str,
89
- metric_value: float,
90
- status: str,
91
- changed: str,
92
- insight: str,
93
- next_step: str,
94
- best_metric: float | None,
95
- ) -> None:
96
- """Append a structured run entry to worklog.md."""
97
- status_upper = status.upper()
98
-
99
- # Build result sentence
100
- if best_metric is None:
101
- result_text = f"{metric_name}={metric_value} (first run, no prior best)"
102
- else:
103
- comparison = (
104
- f"improved from {best_metric}"
105
- if metric_value > best_metric
106
- else (
107
- f"unchanged from {best_metric}"
108
- if metric_value == best_metric
109
- else f"regressed from {best_metric}"
110
- )
111
- )
112
- result_text = f"{metric_name}={metric_value} ({comparison})"
113
-
114
- entry = (
115
- f"\n### Run {iteration}: {description} — {metric_name}={metric_value} ({status_upper})\n"
116
- f"- **Changed:** {changed}\n"
117
- f"- **Result:** {result_text}\n"
118
- f"- **Insight:** {insight}\n"
119
- f"- **Next:** {next_step}\n"
120
- )
121
-
122
- try:
123
- with open(path, "a") as fh:
124
- fh.write(entry)
125
- except OSError as exc:
126
- print(f"error: cannot append to worklog '{path}': {exc}", file=sys.stderr)
127
- sys.exit(1)
128
-
129
-
130
- def main() -> None:
131
- parser = argparse.ArgumentParser(
132
- description="Atomically record an agentic-loop iteration into state.json and worklog.md.",
133
- formatter_class=argparse.RawDescriptionHelpFormatter,
134
- epilog="""
135
- Status values:
136
- keep — metric improved; best is updated, consecutive_discards reset to 0
137
- discard — metric did not improve; consecutive_discards incremented
138
- pivot — forced strategy change (also increments pivot_count)
139
- stop — terminal state; loop should halt
140
-
141
- Exit codes:
142
- 0 success
143
- 1 error (message on stderr)
144
- """,
145
- )
146
- parser.add_argument("--state-file", required=True, metavar="PATH", help="Path to state.json")
147
- parser.add_argument("--worklog", required=True, metavar="PATH", help="Path to worklog.md (append-only)")
148
- parser.add_argument("--iteration", required=True, type=int, help="Current iteration number (1-based)")
149
- parser.add_argument("--metric-value", required=True, type=float, metavar="NUM", help="Numeric metric value this run")
150
- parser.add_argument(
151
- "--status",
152
- required=True,
153
- choices=["keep", "discard", "pivot", "stop"],
154
- help="Outcome classification for this iteration",
155
- )
156
- parser.add_argument("--description", required=True, help="Short description of what changed this run")
157
- parser.add_argument("--insight", required=True, help="What was learned from this run")
158
- parser.add_argument("--next", required=True, dest="next_step", help="What to try in the next iteration")
159
- parser.add_argument(
160
- "--changed",
161
- default=None,
162
- metavar="TEXT",
163
- help="What was specifically modified (defaults to --description)",
164
- )
165
- parser.add_argument(
166
- "--metric-name",
167
- default="metric",
168
- metavar="NAME",
169
- help="Name label for the metric (default: metric)",
170
- )
171
- args = parser.parse_args()
172
-
173
- changed = args.changed if args.changed is not None else args.description
174
-
175
- # --- Load current state ---
176
- state = load_state(args.state_file)
177
-
178
- prev_best: float | None = state.get("best_metric")
179
-
180
- # --- Compute new state values ---
181
- state["iteration"] = args.iteration
182
- state["current_metric"] = args.metric_value
183
- state["status"] = args.status
184
- state["timestamp"] = datetime.now(tz=timezone.utc).isoformat()
185
-
186
- if args.status == "keep":
187
- # Keep: this run is better; promote to best.
188
- state["best_metric"] = args.metric_value
189
- state["consecutive_discards"] = 0
190
- elif args.status == "discard":
191
- # Do not update best; increment discard counter.
192
- state["consecutive_discards"] = int(state.get("consecutive_discards") or 0) + 1
193
- elif args.status == "pivot":
194
- # Pivot: counts as a discard for streak purposes, but also advances pivot_count.
195
- state["consecutive_discards"] = int(state.get("consecutive_discards") or 0) + 1
196
- state["pivot_count"] = int(state.get("pivot_count") or 0) + 1
197
- elif args.status == "stop":
198
- # Terminal — no counter changes needed beyond recording.
199
- pass
200
-
201
- # --- Atomic write ---
202
- atomic_write_json(args.state_file, state)
203
-
204
- # --- Append worklog ---
205
- append_worklog(
206
- path=args.worklog,
207
- iteration=args.iteration,
208
- description=args.description,
209
- metric_name=args.metric_name,
210
- metric_value=args.metric_value,
211
- status=args.status,
212
- changed=changed,
213
- insight=args.insight,
214
- next_step=args.next_step,
215
- best_metric=prev_best,
216
- )
217
-
218
- # Emit updated state summary for easy inspection.
219
- print(json.dumps(state, indent=2))
220
-
221
-
222
- if __name__ == "__main__":
223
- main()
@@ -1,179 +0,0 @@
1
- """
2
- GitOps workflow validation for kubectl, helm, and flux commands.
3
-
4
- Ensures commands follow GitOps principles:
5
- - No direct cluster modifications
6
- - Use --dry-run for apply operations
7
- - Prefer read-only commands
8
- """
9
-
10
- import re
11
- import logging
12
- from typing import List, Optional
13
- from dataclasses import dataclass, field
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
-
18
- @dataclass
19
- class GitOpsValidationResult:
20
- """Result of GitOps validation."""
21
- allowed: bool
22
- reason: str
23
- severity: str = "info" # info, warning, high, critical
24
- suggestions: List[str] = field(default_factory=list)
25
-
26
-
27
- # Safe read-only commands (always allowed)
28
- SAFE_KUBECTL_COMMANDS = [
29
- r'kubectl\s+get',
30
- r'kubectl\s+describe',
31
- r'kubectl\s+logs',
32
- r'kubectl\s+top',
33
- r'kubectl\s+explain',
34
- r'kubectl\s+version',
35
- r'kubectl\s+cluster-info',
36
- r'kubectl\s+config\s+view',
37
- r'kubectl\s+api-resources',
38
- r'kubectl\s+api-versions',
39
- ]
40
-
41
- SAFE_FLUX_COMMANDS = [
42
- r'flux\s+get',
43
- r'flux\s+check',
44
- r'flux\s+version',
45
- r'flux\s+logs',
46
- r'flux\s+stats',
47
- r'flux\s+tree',
48
- ]
49
-
50
- SAFE_HELM_COMMANDS = [
51
- r'helm\s+list',
52
- r'helm\s+status',
53
- r'helm\s+history',
54
- r'helm\s+template',
55
- r'helm\s+lint',
56
- r'helm\s+version',
57
- r'helm\s+show',
58
- r'helm\s+search',
59
- ]
60
-
61
- # Forbidden commands (modify cluster state)
62
- FORBIDDEN_KUBECTL_COMMANDS = [
63
- r'kubectl\s+apply(?!\s+.*--dry-run)',
64
- r'kubectl\s+create(?!\s+.*--dry-run)',
65
- r'kubectl\s+patch',
66
- r'kubectl\s+replace',
67
- r'kubectl\s+delete',
68
- r'kubectl\s+scale',
69
- r'kubectl\s+rollout\s+restart',
70
- r'kubectl\s+annotate(?!\s+.*--dry-run)',
71
- r'kubectl\s+label(?!\s+.*--dry-run)',
72
- ]
73
-
74
- FORBIDDEN_FLUX_COMMANDS = [
75
- r'flux\s+create',
76
- r'flux\s+delete',
77
- r'flux\s+suspend',
78
- r'flux\s+resume',
79
- ]
80
-
81
- FORBIDDEN_HELM_COMMANDS = [
82
- r'helm\s+install(?!\s+.*--dry-run)',
83
- r'helm\s+upgrade(?!\s+.*--dry-run)',
84
- r'helm\s+uninstall',
85
- r'helm\s+rollback',
86
- ]
87
-
88
-
89
- def is_safe_gitops_command(command: str) -> bool:
90
- """Check if command is explicitly safe (read-only)."""
91
- safe_patterns = SAFE_KUBECTL_COMMANDS + SAFE_FLUX_COMMANDS + SAFE_HELM_COMMANDS
92
- for pattern in safe_patterns:
93
- if re.search(pattern, command, re.IGNORECASE):
94
- return True
95
- return False
96
-
97
-
98
- def is_forbidden_gitops_command(command: str) -> bool:
99
- """Check if command is forbidden (modifies cluster state)."""
100
- forbidden_patterns = (
101
- FORBIDDEN_KUBECTL_COMMANDS +
102
- FORBIDDEN_FLUX_COMMANDS +
103
- FORBIDDEN_HELM_COMMANDS
104
- )
105
- for pattern in forbidden_patterns:
106
- if re.search(pattern, command, re.IGNORECASE):
107
- return True
108
- return False
109
-
110
-
111
- def validate_gitops_workflow(
112
- command: str,
113
- agent_type: Optional[str] = None
114
- ) -> GitOpsValidationResult:
115
- """
116
- Validate command against GitOps security principles.
117
-
118
- Args:
119
- command: Shell command to validate
120
- agent_type: Optional agent type for stricter validation
121
-
122
- Returns:
123
- GitOpsValidationResult with status and suggestions
124
- """
125
- # Check if command is explicitly safe
126
- if is_safe_gitops_command(command):
127
- return GitOpsValidationResult(
128
- allowed=True,
129
- reason="Read-only operation - safe to execute",
130
- )
131
-
132
- # Check if command is forbidden
133
- if is_forbidden_gitops_command(command):
134
- suggestions = []
135
-
136
- # Provide specific suggestions based on command type
137
- if "kubectl apply" in command and "--dry-run" not in command:
138
- suggestions.extend([
139
- "Use: kubectl apply --dry-run=client -f <file>",
140
- "Create manifests in gitops repository first",
141
- "Commit changes and let Flux CD reconcile"
142
- ])
143
- elif "flux reconcile" in command and "--dry-run" not in command:
144
- suggestions.extend([
145
- "Use: flux reconcile <resource> --dry-run",
146
- "Follow GitOps workflow: commit -> push -> automatic reconciliation"
147
- ])
148
- elif "helm install" in command or "helm upgrade" in command:
149
- suggestions.extend([
150
- "Use: helm template or helm upgrade --dry-run",
151
- "Deploy via HelmRelease manifests in gitops repository"
152
- ])
153
- else:
154
- suggestions.append("Use read-only commands or --dry-run alternatives")
155
-
156
- return GitOpsValidationResult(
157
- allowed=False,
158
- reason="Command violates GitOps principles - modifies cluster state directly",
159
- severity="critical",
160
- suggestions=suggestions,
161
- )
162
-
163
- # For gitops-operator agent, be extra strict
164
- if agent_type == "gitops-operator":
165
- if ("apply" in command or "create" in command) and "--dry-run" not in command:
166
- return GitOpsValidationResult(
167
- allowed=False,
168
- reason="GitOps operator must use --dry-run for all apply operations",
169
- severity="high",
170
- suggestions=["Add --dry-run=client flag to command"],
171
- )
172
-
173
- # Default: allow but warn about unclear intent
174
- return GitOpsValidationResult(
175
- allowed=True,
176
- reason="Command not explicitly validated - proceed with caution",
177
- severity="warning",
178
- suggestions=["Verify command follows GitOps principles"],
179
- )
@@ -1,41 +0,0 @@
1
- #!/bin/sh
2
- #
3
- # commit-msg hook: strip Claude Code attribution footers from commit messages.
4
- #
5
- # This hook runs at the git level, catching ALL commits regardless of whether
6
- # they originate from Claude Code's Bash tool, a subagent, or the user's terminal.
7
- #
8
- # Patterns match those in hooks/modules/tools/bash_validator.py _strip_claude_footers()
9
- # to maintain a single source of truth for what constitutes a forbidden footer.
10
- #
11
- # Installation:
12
- # cp git-hooks/commit-msg .git/hooks/commit-msg
13
- # chmod +x .git/hooks/commit-msg
14
- #
15
- # Or via gaia-init (automatic).
16
-
17
- COMMIT_MSG_FILE="$1"
18
-
19
- if [ ! -f "${COMMIT_MSG_FILE}" ]; then
20
- exit 0
21
- fi
22
-
23
- # Create a temp file for the cleaned message
24
- TEMP_FILE=$(mktemp)
25
- trap 'rm -f "${TEMP_FILE}"' EXIT
26
-
27
- # Read the commit message and strip forbidden footer lines:
28
- # - Co-Authored-By: containing "Claude" (any case)
29
- # - "Generated with Claude Code" or "[Claude Code]" (any case)
30
- # - Emoji-prefixed "Generated with" lines
31
- sed -E \
32
- -e '/^[[:space:]]*[Cc][Oo]-[Aa][Uu][Tt][Hh][Oo][Rr][Ee][Dd]-[Bb][Yy]:.*[Cc][Ll][Aa][Uu][Dd][Ee]/d' \
33
- -e '/^[[:space:]]*[Gg][Ee][Nn][Ee][Rr][Aa][Tt][Ee][Dd] [Ww][Ii][Tt][Hh].*[Cc][Ll][Aa][Uu][Dd][Ee] [Cc][Oo][Dd][Ee]/d' \
34
- -e '/^[[:space:]]*..?[[:space:]]*[Gg][Ee][Nn][Ee][Rr][Aa][Tt][Ee][Dd] [Ww][Ii][Tt][Hh]/d' \
35
- "${COMMIT_MSG_FILE}" > "${TEMP_FILE}"
36
-
37
- # Remove trailing blank lines (collapse to single trailing newline)
38
- # This prevents empty trailers after stripping
39
- sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' "${TEMP_FILE}" > "${COMMIT_MSG_FILE}"
40
-
41
- exit 0
@@ -1,179 +0,0 @@
1
- """
2
- GitOps workflow validation for kubectl, helm, and flux commands.
3
-
4
- Ensures commands follow GitOps principles:
5
- - No direct cluster modifications
6
- - Use --dry-run for apply operations
7
- - Prefer read-only commands
8
- """
9
-
10
- import re
11
- import logging
12
- from typing import List, Optional
13
- from dataclasses import dataclass, field
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
-
18
- @dataclass
19
- class GitOpsValidationResult:
20
- """Result of GitOps validation."""
21
- allowed: bool
22
- reason: str
23
- severity: str = "info" # info, warning, high, critical
24
- suggestions: List[str] = field(default_factory=list)
25
-
26
-
27
- # Safe read-only commands (always allowed)
28
- SAFE_KUBECTL_COMMANDS = [
29
- r'kubectl\s+get',
30
- r'kubectl\s+describe',
31
- r'kubectl\s+logs',
32
- r'kubectl\s+top',
33
- r'kubectl\s+explain',
34
- r'kubectl\s+version',
35
- r'kubectl\s+cluster-info',
36
- r'kubectl\s+config\s+view',
37
- r'kubectl\s+api-resources',
38
- r'kubectl\s+api-versions',
39
- ]
40
-
41
- SAFE_FLUX_COMMANDS = [
42
- r'flux\s+get',
43
- r'flux\s+check',
44
- r'flux\s+version',
45
- r'flux\s+logs',
46
- r'flux\s+stats',
47
- r'flux\s+tree',
48
- ]
49
-
50
- SAFE_HELM_COMMANDS = [
51
- r'helm\s+list',
52
- r'helm\s+status',
53
- r'helm\s+history',
54
- r'helm\s+template',
55
- r'helm\s+lint',
56
- r'helm\s+version',
57
- r'helm\s+show',
58
- r'helm\s+search',
59
- ]
60
-
61
- # Forbidden commands (modify cluster state)
62
- FORBIDDEN_KUBECTL_COMMANDS = [
63
- r'kubectl\s+apply(?!\s+.*--dry-run)',
64
- r'kubectl\s+create(?!\s+.*--dry-run)',
65
- r'kubectl\s+patch',
66
- r'kubectl\s+replace',
67
- r'kubectl\s+delete',
68
- r'kubectl\s+scale',
69
- r'kubectl\s+rollout\s+restart',
70
- r'kubectl\s+annotate(?!\s+.*--dry-run)',
71
- r'kubectl\s+label(?!\s+.*--dry-run)',
72
- ]
73
-
74
- FORBIDDEN_FLUX_COMMANDS = [
75
- r'flux\s+create',
76
- r'flux\s+delete',
77
- r'flux\s+suspend',
78
- r'flux\s+resume',
79
- ]
80
-
81
- FORBIDDEN_HELM_COMMANDS = [
82
- r'helm\s+install(?!\s+.*--dry-run)',
83
- r'helm\s+upgrade(?!\s+.*--dry-run)',
84
- r'helm\s+uninstall',
85
- r'helm\s+rollback',
86
- ]
87
-
88
-
89
- def is_safe_gitops_command(command: str) -> bool:
90
- """Check if command is explicitly safe (read-only)."""
91
- safe_patterns = SAFE_KUBECTL_COMMANDS + SAFE_FLUX_COMMANDS + SAFE_HELM_COMMANDS
92
- for pattern in safe_patterns:
93
- if re.search(pattern, command, re.IGNORECASE):
94
- return True
95
- return False
96
-
97
-
98
- def is_forbidden_gitops_command(command: str) -> bool:
99
- """Check if command is forbidden (modifies cluster state)."""
100
- forbidden_patterns = (
101
- FORBIDDEN_KUBECTL_COMMANDS +
102
- FORBIDDEN_FLUX_COMMANDS +
103
- FORBIDDEN_HELM_COMMANDS
104
- )
105
- for pattern in forbidden_patterns:
106
- if re.search(pattern, command, re.IGNORECASE):
107
- return True
108
- return False
109
-
110
-
111
- def validate_gitops_workflow(
112
- command: str,
113
- agent_type: Optional[str] = None
114
- ) -> GitOpsValidationResult:
115
- """
116
- Validate command against GitOps security principles.
117
-
118
- Args:
119
- command: Shell command to validate
120
- agent_type: Optional agent type for stricter validation
121
-
122
- Returns:
123
- GitOpsValidationResult with status and suggestions
124
- """
125
- # Check if command is explicitly safe
126
- if is_safe_gitops_command(command):
127
- return GitOpsValidationResult(
128
- allowed=True,
129
- reason="Read-only operation - safe to execute",
130
- )
131
-
132
- # Check if command is forbidden
133
- if is_forbidden_gitops_command(command):
134
- suggestions = []
135
-
136
- # Provide specific suggestions based on command type
137
- if "kubectl apply" in command and "--dry-run" not in command:
138
- suggestions.extend([
139
- "Use: kubectl apply --dry-run=client -f <file>",
140
- "Create manifests in gitops repository first",
141
- "Commit changes and let Flux CD reconcile"
142
- ])
143
- elif "flux reconcile" in command and "--dry-run" not in command:
144
- suggestions.extend([
145
- "Use: flux reconcile <resource> --dry-run",
146
- "Follow GitOps workflow: commit -> push -> automatic reconciliation"
147
- ])
148
- elif "helm install" in command or "helm upgrade" in command:
149
- suggestions.extend([
150
- "Use: helm template or helm upgrade --dry-run",
151
- "Deploy via HelmRelease manifests in gitops repository"
152
- ])
153
- else:
154
- suggestions.append("Use read-only commands or --dry-run alternatives")
155
-
156
- return GitOpsValidationResult(
157
- allowed=False,
158
- reason="Command violates GitOps principles - modifies cluster state directly",
159
- severity="critical",
160
- suggestions=suggestions,
161
- )
162
-
163
- # For gitops-operator agent, be extra strict
164
- if agent_type == "gitops-operator":
165
- if ("apply" in command or "create" in command) and "--dry-run" not in command:
166
- return GitOpsValidationResult(
167
- allowed=False,
168
- reason="GitOps operator must use --dry-run for all apply operations",
169
- severity="high",
170
- suggestions=["Add --dry-run=client flag to command"],
171
- )
172
-
173
- # Default: allow but warn about unclear intent
174
- return GitOpsValidationResult(
175
- allowed=True,
176
- reason="Command not explicitly validated - proceed with caution",
177
- severity="warning",
178
- suggestions=["Verify command follows GitOps principles"],
179
- )