@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/ARCHITECTURE.md +0 -1
- package/CHANGELOG.md +110 -0
- package/INSTALL.md +0 -2
- package/README.md +1 -6
- package/bin/README.md +0 -1
- package/bin/cli/_install_helpers.py +1 -1
- package/bin/cli/approvals.py +23 -21
- package/bin/cli/cleanup.py +0 -1
- package/bin/cli/doctor.py +1 -1
- package/bin/cli/memory.py +2 -0
- package/bin/cli/update.py +1 -1
- package/bin/pre-publish-validate.js +48 -5
- package/config/README.md +22 -44
- package/config/surface-routing.json +0 -2
- package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-ops/config/README.md +22 -44
- package/dist/gaia-ops/config/surface-routing.json +0 -2
- package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +18 -0
- package/dist/gaia-ops/hooks/modules/agents/handoff_persister.py +214 -2
- package/dist/gaia-ops/hooks/modules/agents/response_contract.py +26 -0
- package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +15 -0
- package/dist/gaia-ops/hooks/modules/security/__init__.py +0 -5
- package/dist/gaia-ops/hooks/modules/security/approval_grants.py +124 -19
- package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +99 -7
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +127 -24
- package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +90 -55
- package/dist/gaia-ops/skills/README.md +1 -1
- package/dist/gaia-ops/skills/agent-contract-handoff/SKILL.md +3 -0
- package/dist/gaia-ops/skills/agent-response/SKILL.md +4 -2
- package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +1 -1
- package/dist/gaia-ops/skills/gaia-patterns/reference.md +2 -3
- package/dist/gaia-ops/skills/gaia-release/SKILL.md +60 -24
- package/dist/gaia-ops/skills/gaia-release/reference.md +35 -11
- package/dist/gaia-ops/skills/git-conventions/SKILL.md +6 -2
- package/dist/gaia-ops/skills/orchestrator-present-approval/SKILL.md +30 -7
- package/dist/gaia-ops/skills/orchestrator-present-approval/reference.md +32 -15
- package/dist/gaia-ops/skills/readme-writing/SKILL.md +1 -1
- package/dist/gaia-ops/skills/readme-writing/reference.md +0 -1
- package/dist/gaia-ops/skills/security-tiers/SKILL.md +5 -1
- package/dist/gaia-ops/skills/security-tiers/reference.md +3 -1
- package/dist/gaia-ops/skills/subagent-request-approval/SKILL.md +43 -6
- package/dist/gaia-ops/skills/subagent-request-approval/reference.md +66 -16
- package/dist/gaia-ops/tools/context/README.md +1 -1
- package/dist/gaia-ops/tools/gaia_simulator/extractor.py +0 -1
- package/dist/gaia-ops/tools/scan/ui.py +20 -4
- package/dist/gaia-ops/tools/scan/verify.py +3 -3
- package/dist/gaia-ops/tools/validation/README.md +15 -24
- package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-security/hooks/modules/agents/contract_validator.py +18 -0
- package/dist/gaia-security/hooks/modules/agents/handoff_persister.py +214 -2
- package/dist/gaia-security/hooks/modules/agents/response_contract.py +26 -0
- package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +15 -0
- package/dist/gaia-security/hooks/modules/security/__init__.py +0 -5
- package/dist/gaia-security/hooks/modules/security/approval_grants.py +124 -19
- package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +99 -7
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +127 -24
- package/dist/gaia-security/hooks/modules/validation/commit_validator.py +90 -55
- package/gaia/state/transitions.py +4 -4
- package/gaia/store/writer.py +56 -0
- package/hooks/modules/README.md +2 -4
- package/hooks/modules/agents/contract_validator.py +18 -0
- package/hooks/modules/agents/handoff_persister.py +214 -2
- package/hooks/modules/agents/response_contract.py +26 -0
- package/hooks/modules/agents/transcript_reader.py +15 -0
- package/hooks/modules/security/__init__.py +0 -5
- package/hooks/modules/security/approval_grants.py +124 -19
- package/hooks/modules/security/mutative_verbs.py +99 -7
- package/hooks/modules/tools/bash_validator.py +127 -24
- package/hooks/modules/validation/commit_validator.py +90 -55
- package/index.js +2 -12
- package/package.json +4 -6
- package/pyproject.toml +3 -3
- package/scripts/bootstrap_database.sh +88 -439
- package/scripts/check_schema_drift.py +208 -0
- package/scripts/migrations/README.md +78 -28
- package/scripts/migrations/schema.checksum +8 -0
- package/scripts/release-prepare.mjs +199 -0
- package/skills/README.md +1 -1
- package/skills/agent-contract-handoff/SKILL.md +3 -0
- package/skills/agent-response/SKILL.md +4 -2
- package/skills/gaia-patterns/SKILL.md +1 -1
- package/skills/gaia-patterns/reference.md +2 -3
- package/skills/gaia-release/SKILL.md +60 -24
- package/skills/gaia-release/reference.md +35 -11
- package/skills/git-conventions/SKILL.md +6 -2
- package/skills/orchestrator-present-approval/SKILL.md +30 -7
- package/skills/orchestrator-present-approval/reference.md +32 -15
- package/skills/readme-writing/SKILL.md +1 -1
- package/skills/readme-writing/reference.md +0 -1
- package/skills/security-tiers/SKILL.md +5 -1
- package/skills/security-tiers/reference.md +3 -1
- package/skills/subagent-request-approval/SKILL.md +43 -6
- package/skills/subagent-request-approval/reference.md +66 -16
- package/tools/context/README.md +1 -1
- package/tools/gaia_simulator/extractor.py +0 -1
- package/tools/scan/ui.py +20 -4
- package/tools/scan/verify.py +3 -3
- package/tools/validation/README.md +15 -24
- package/commands/README.md +0 -64
- package/commands/gaia.md +0 -37
- package/commands/scan-project.md +0 -74
- package/config/crons-schema.md +0 -81
- package/config/git_standards.json +0 -72
- package/dist/gaia-ops/commands/gaia.md +0 -37
- package/dist/gaia-ops/config/crons-schema.md +0 -81
- package/dist/gaia-ops/config/git_standards.json +0 -72
- package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +0 -179
- package/dist/gaia-ops/tools/agentic-loop/decide-status.py +0 -210
- package/dist/gaia-ops/tools/agentic-loop/parse-metric.py +0 -106
- package/dist/gaia-ops/tools/agentic-loop/record-iteration.py +0 -223
- package/dist/gaia-security/hooks/modules/security/gitops_validator.py +0 -179
- package/git-hooks/commit-msg +0 -41
- package/hooks/modules/security/gitops_validator.py +0 -179
- package/scripts/migrations/v10_to_v11.sql +0 -170
- package/scripts/migrations/v10_to_v11_fresh.sql +0 -18
- package/scripts/migrations/v11_to_v12.sql +0 -195
- package/scripts/migrations/v11_to_v12_fresh.sql +0 -19
- package/scripts/migrations/v12_to_v13.sql +0 -48
- package/scripts/migrations/v12_to_v13_fresh.sql +0 -17
- package/scripts/migrations/v13_to_v14.sql +0 -44
- package/scripts/migrations/v13_to_v14_fresh.sql +0 -17
- package/scripts/migrations/v14_to_v15.sql +0 -71
- package/scripts/migrations/v14_to_v15_fresh.sql +0 -19
- package/scripts/migrations/v15_to_v16.sql +0 -57
- package/scripts/migrations/v15_to_v16_fresh.sql +0 -18
- package/scripts/migrations/v16_to_v17.sql +0 -51
- package/scripts/migrations/v16_to_v17_fresh.sql +0 -18
- package/scripts/migrations/v17_to_v18.sql +0 -66
- package/scripts/migrations/v17_to_v18_fresh.sql +0 -24
- package/scripts/migrations/v1_to_v2.sql +0 -97
- package/scripts/migrations/v2_to_v3.sql +0 -68
- package/scripts/migrations/v2_to_v3_merge.sql +0 -69
- package/scripts/migrations/v3_to_v4.sql +0 -67
- package/scripts/migrations/v3_to_v4_fresh.sql +0 -20
- package/scripts/migrations/v4_to_v5.sql +0 -55
- package/scripts/migrations/v4_to_v5_fresh.sql +0 -20
- package/scripts/migrations/v5_to_v6.sql +0 -48
- package/scripts/migrations/v5_to_v6_fresh.sql +0 -17
- package/scripts/migrations/v6_to_v7.sql +0 -26
- package/scripts/migrations/v6_to_v7_fresh.sql +0 -13
- package/scripts/migrations/v7_to_v8.sql +0 -44
- package/scripts/migrations/v7_to_v8_fresh.sql +0 -14
- package/scripts/migrations/v8_to_v9.sql +0 -87
- package/scripts/migrations/v8_to_v9_fresh.sql +0 -15
- package/scripts/migrations/v9_to_v10.sql +0 -109
- package/scripts/migrations/v9_to_v10_episodes_workspace.sql +0 -109
- package/scripts/migrations/v9_to_v10_fresh.sql +0 -18
- package/templates/README.md +0 -70
- package/templates/managed-settings.template.json +0 -43
- package/tools/agentic-loop/decide-status.py +0 -210
- package/tools/agentic-loop/parse-metric.py +0 -106
- 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
|
-
)
|
package/git-hooks/commit-msg
DELETED
|
@@ -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
|
-
)
|