@jaguilar87/gaia 5.0.2 → 5.0.4

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 (63) 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 +54 -0
  5. package/bin/cli/approvals.py +23 -21
  6. package/config/surface-routing.json +0 -1
  7. package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
  8. package/dist/gaia-ops/config/surface-routing.json +0 -1
  9. package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +18 -0
  10. package/dist/gaia-ops/hooks/modules/agents/handoff_persister.py +212 -2
  11. package/dist/gaia-ops/hooks/modules/agents/response_contract.py +26 -0
  12. package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +15 -0
  13. package/dist/gaia-ops/hooks/modules/security/__init__.py +0 -5
  14. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +122 -19
  15. package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +99 -7
  16. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +125 -24
  17. package/dist/gaia-ops/skills/agent-contract-handoff/SKILL.md +3 -0
  18. package/dist/gaia-ops/skills/agent-response/SKILL.md +4 -2
  19. package/dist/gaia-ops/skills/gaia-patterns/reference.md +2 -2
  20. package/dist/gaia-ops/skills/orchestrator-present-approval/SKILL.md +20 -5
  21. package/dist/gaia-ops/skills/orchestrator-present-approval/reference.md +32 -15
  22. package/dist/gaia-ops/skills/security-tiers/SKILL.md +5 -1
  23. package/dist/gaia-ops/skills/security-tiers/reference.md +3 -1
  24. package/dist/gaia-ops/skills/subagent-request-approval/SKILL.md +43 -6
  25. package/dist/gaia-ops/skills/subagent-request-approval/reference.md +66 -16
  26. package/dist/gaia-ops/tools/context/README.md +1 -1
  27. package/dist/gaia-ops/tools/gaia_simulator/extractor.py +0 -1
  28. package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
  29. package/dist/gaia-security/hooks/modules/agents/contract_validator.py +18 -0
  30. package/dist/gaia-security/hooks/modules/agents/handoff_persister.py +212 -2
  31. package/dist/gaia-security/hooks/modules/agents/response_contract.py +26 -0
  32. package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +15 -0
  33. package/dist/gaia-security/hooks/modules/security/__init__.py +0 -5
  34. package/dist/gaia-security/hooks/modules/security/approval_grants.py +122 -19
  35. package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +99 -7
  36. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +125 -24
  37. package/gaia/state/transitions.py +4 -4
  38. package/gaia/store/writer.py +56 -0
  39. package/hooks/modules/README.md +2 -4
  40. package/hooks/modules/agents/contract_validator.py +18 -0
  41. package/hooks/modules/agents/handoff_persister.py +212 -2
  42. package/hooks/modules/agents/response_contract.py +26 -0
  43. package/hooks/modules/agents/transcript_reader.py +15 -0
  44. package/hooks/modules/security/__init__.py +0 -5
  45. package/hooks/modules/security/approval_grants.py +122 -19
  46. package/hooks/modules/security/mutative_verbs.py +99 -7
  47. package/hooks/modules/tools/bash_validator.py +125 -24
  48. package/package.json +1 -1
  49. package/pyproject.toml +1 -1
  50. package/skills/agent-contract-handoff/SKILL.md +3 -0
  51. package/skills/agent-response/SKILL.md +4 -2
  52. package/skills/gaia-patterns/reference.md +2 -2
  53. package/skills/orchestrator-present-approval/SKILL.md +20 -5
  54. package/skills/orchestrator-present-approval/reference.md +32 -15
  55. package/skills/security-tiers/SKILL.md +5 -1
  56. package/skills/security-tiers/reference.md +3 -1
  57. package/skills/subagent-request-approval/SKILL.md +43 -6
  58. package/skills/subagent-request-approval/reference.md +66 -16
  59. package/tools/context/README.md +1 -1
  60. package/tools/gaia_simulator/extractor.py +0 -1
  61. package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +0 -179
  62. package/dist/gaia-security/hooks/modules/security/gitops_validator.py +0 -179
  63. package/hooks/modules/security/gitops_validator.py +0 -179
@@ -63,12 +63,47 @@ prose are invisible to the presentation -- the user would approve blind.
63
63
  - **The grant is single-use.** It is consumed on your first matching retry. A
64
64
  second run within the TTL will not match -- it needs a fresh approval.
65
65
 
66
- ## Batch / many-command intents
67
-
68
- There is **no `batch_scope` field** and no production batch grant: each blocked
69
- command is one single-use approval, so a sweep of N commands is N approvals.
70
- The `COMMAND_SET` mechanism exists in code but no path activates it from an
71
- approval, so emitting `batch_scope` does nothing. See `reference.md` for why.
66
+ ## Batch / many-command intents -- COMMAND_SET as a judgment, not a default
67
+
68
+ Grouping commands under one consent is a **judgment call you earn, not the
69
+ reflex you reach for**. The default is singular, just-in-time approval: attempt
70
+ the command, let the hook block it, request that one. Reach for `COMMAND_SET`
71
+ **only when all three hold** -- the batch is already **known** (the commands are
72
+ determined, not predicted), there are **>= 2** of them, and grouping **actually
73
+ reduces friction** versus approving each as it arrives. If any fails (a single
74
+ command, a sequential flow where the next depends on the last's output, or a
75
+ set you cannot yet name), use the singular path. The principle with its
76
+ consequence: **grouping trades the user's per-command visibility for fewer
77
+ prompts; make that trade only when the batch is real and known, because a batch
78
+ you guessed at asks the user to approve commands that may never run.**
79
+
80
+ The hard prohibition this rules out: **never invent or predict commands just to
81
+ have something to group.** Speculatively enumerating a `command_set` to "save
82
+ turns" inverts the cost -- it manufactures ceremony (a multi-command consent
83
+ surface) around work that was never determined, which is more overhead than the
84
+ just-in-time blocks it was meant to avoid. If you do not already know the
85
+ commands, you do not have a batch.
86
+
87
+ When the three conditions do hold, emit an `APPROVAL_REQUEST` whose
88
+ `approval_request` carries a `command_set` -- a list of `{command, rationale}`
89
+ items -- and **no `approval_id`** (nothing has been attempted yet). The
90
+ per-command rationale is what makes the grouped consent honest: the user sees
91
+ why each *known* command is in the batch before approving (D10).
92
+
93
+ What happens to that envelope: the SubagentStop processor
94
+ (`hooks/modules/agents/handoff_persister.py` -> `_intake_command_set_pending`)
95
+ reads the `command_set`, and when it holds **>= 2** items it calls
96
+ `gaia.approvals.store.insert_requested` with a payload containing the
97
+ `command_set` key. That mints **exactly ONE pending `COMMAND_SET` approval**
98
+ with one `approval_id` -- so a batch of N commands is **one consent, N
99
+ commands**, not N approvals. A set of `<= 1` item is not a batch: it does not
100
+ mint a COMMAND_SET (use the normal singular block path for a single command).
101
+
102
+ On the user's approval, that one pending activates into a single `COMMAND_SET`
103
+ grant (60-minute TTL); each item is then consumed byte-for-byte on its own
104
+ retry, with replay protection, until the whole set is `CONSUMED`. See
105
+ `reference.md` for the envelope shape, the intake processor, the grant TTL, and
106
+ the consume path.
72
107
 
73
108
  ## Pointers
74
109
 
@@ -84,3 +119,5 @@ approval, so emitting `batch_scope` does nothing. See `reference.md` for why.
84
119
  - **Fabricating `approval_id`, fingerprint, or `sealed_payload`** -- the orchestrator validates against the DB; invented values never match.
85
120
  - **Reusing a prior approval** -- single-use, consumed on first retry.
86
121
  - **Emitting `batch_scope`** -- the field does not exist; it is ignored.
122
+ - **Grouping by reflex** -- reaching for `COMMAND_SET` because a batch *might* form, instead of because a known batch of >= 2 already exists that grouping makes cheaper. The default is singular just-in-time; grouping is the exception you justify.
123
+ - **Predicting commands to fill a batch** -- inventing commands you have not determined so a `command_set` has >= 2 items. You cannot ask consent for work that does not yet exist; the speculative batch is pure overhead.
@@ -69,35 +69,85 @@ On your retry, `check_approval_grant()` matches it and immediately consumes it
69
69
  TTL will NOT match -- the grant is gone. This is replay protection by design;
70
70
  re-approve if you need to run the command again.
71
71
 
72
- ## Batch / COMMAND_SET -- designed, not wired
72
+ ## Batch / COMMAND_SET -- wired
73
73
 
74
74
  The legacy `verb_family` multi-use grant was removed (see module docstring in
75
- `approval_grants.py`: "The legacy verb_family path has been removed"). Its intended
75
+ `approval_grants.py`: "The legacy verb_family path has been removed"). Its
76
76
  replacement is the `COMMAND_SET` grant -- an explicit list of `{command, rationale}`
77
77
  items, each matched byte-for-byte and consumed individually
78
78
  (`approval_grants.create_command_set_grant()`; `approval_grants.match_command_set_grant()`).
79
+ All three sides are now wired end-to-end -- **intake**, **activation**, and
80
+ **consume** -- so one consent covers N commands.
81
+
82
+ **Intake -- plan-first, one pending.** The batch is declared up-front: you emit
83
+ an `APPROVAL_REQUEST` whose `approval_request` carries a `command_set` list and
84
+ **no `approval_id`** (you have attempted nothing). The production intake caller
85
+ is the SubagentStop processor `handoff_persister.persist_handoff()`, which calls
86
+ `_intake_command_set_pending()`. That helper normalizes the `command_set` and,
87
+ when it holds **>= 2** `{command, rationale}` items, builds a sealed_payload
88
+ carrying the `command_set` key (mirroring the shape
89
+ `bash_validator._build_sealed_payload()` emits) and calls
90
+ `gaia.approvals.store.insert_requested()` -- minting **exactly ONE** pending
91
+ `COMMAND_SET` approval with one `approval_id`. A set of length `<= 1` is not a
92
+ batch: the intake declines and the singular semantic-signature path owns it (no
93
+ COMMAND_SET is ever minted for one command). The intake runs independently of
94
+ the audit handoff-row write, so a batch consent is never lost to an unrelated
95
+ DB failure.
96
+
97
+ **Envelope shape.** The sealed_payload the intake writes carries a `command_set`
98
+ key holding the verbatim list of `{command, rationale}` items, and `commands`
99
+ listing every command string in the set:
79
100
 
80
- **Current state:** only the CHECK side is wired. `bash_validator._validate_single_command()`
81
- calls `match_command_set_grant()` and consumes a matched item, but **no
82
- production path calls `approval_grants.create_command_set_grant()`** -- it exists only in the
83
- module and its tests. The activation paths only call
84
- `approval_grants.activate_db_pending_by_prefix()`, which creates a single-use
85
- `SCOPE_SEMANTIC_SIGNATURE` grant.
101
+ ```json
102
+ {
103
+ "operation": "MUTATIVE command intercepted: push",
104
+ "exact_content": "git add -A",
105
+ "commands": ["git add -A", "git commit -m 'v1.2.0'", "git push origin main"],
106
+ "command_set": [
107
+ {"command": "git add -A", "rationale": "stage release files"},
108
+ {"command": "git commit -m 'v1.2.0'", "rationale": "record the release commit"},
109
+ {"command": "git push origin main", "rationale": "publish to the remote"}
110
+ ]
111
+ }
112
+ ```
86
113
 
87
- **Consequence:** a `batch_scope` field in `approval_request`, or the word "batch"
88
- in a label, does nothing. Each blocked command produces its own single-use
89
- semantic grant and its own approval. For a sweep of N commands, expect N
90
- approvals until the COMMAND_SET create path is wired.
114
+ **Activation -- one consent, one grant.** When the user approves, the
115
+ ElicitationResult hook (`approval_grants.activate_db_pending_by_prefix()`)
116
+ detects the `command_set` and branches to `approval_grants.create_command_set_grant()`,
117
+ which inserts a single `COMMAND_SET` grant row into `approval_grants`
118
+ (status `PENDING`, `command_set_json` holding the whole set). The grant TTL is
119
+ **60 minutes** (`DEFAULT_COMMAND_SET_TTL_MINUTES`), aligned to the singular
120
+ active-grant TTL so the batch does not expire mid-consume across sessions.
121
+
122
+ **Consume -- item by item, replay-protected.** On each retry,
123
+ `bash_validator._validate_single_command()` calls `match_command_set_grant()`,
124
+ which finds the matching command's index byte-for-byte and returns it; the
125
+ validator then calls `mark_command_set_item_consumed()`, appending that index to
126
+ `consumed_indexes_json`. A consumed index never matches again (replay
127
+ protection), and when every index is consumed the grant flips to `CONSUMED`.
128
+ Wrapping an approved command -- adding `cd`, a redirect, a pipe, or a flag --
129
+ produces a different string and matches nothing in the set; it requires fresh
130
+ approval.
131
+
132
+ **Consequence:** for a set of N related T3 commands, emit the `command_set`
133
+ envelope and the user approves once. Each command runs on its own retry,
134
+ single-use within the 60-minute window.
91
135
 
92
136
  ## Status to emit -- with vs without approval_id
93
137
 
94
138
  Always `plan_status: "APPROVAL_REQUEST"`. The presence of `approval_id` tells the
95
139
  orchestrator which path:
96
140
 
97
- - **With `approval_id`** -- the hook blocked; orchestrator validates the
98
- fingerprint and activates the grant on user approval.
99
- - **Without `approval_id`** -- plan-first (you are presenting a T3 plan before
100
- attempting); the orchestrator gates on user consent before any execution.
141
+ - **With `approval_id`** -- the hook blocked a single command; orchestrator
142
+ validates the fingerprint and activates the single-use semantic grant on user
143
+ approval.
144
+ - **Without `approval_id`, with a `command_set` of >= 2 items** -- plan-first
145
+ batch. The SubagentStop intake processor mints ONE pending `COMMAND_SET` and
146
+ the orchestrator presents that single approval (N commands, one nonce) before
147
+ any execution. See "Batch / COMMAND_SET -- wired" above.
148
+ - **Without `approval_id` and without a multi-item `command_set`** -- plan-first
149
+ single (you are presenting one T3 plan before attempting); the orchestrator
150
+ gates on user consent before any execution.
101
151
 
102
152
  ## Examples
103
153
 
@@ -77,7 +77,7 @@ Agent contracts live in `~/.gaia/gaia.db` (`project_context_contracts` + `agent_
77
77
  **cloud-troubleshooter:**
78
78
  - project_identity, stack, git, environment, infrastructure, orchestration
79
79
  - cluster_details, infrastructure_topology, terraform_infrastructure
80
- - gitops_configuration, application_services, monitoring_observability, architecture_overview
80
+ - gitops_configuration, application_services, architecture_overview
81
81
 
82
82
  The same contracts are exposed under `write_permissions`:
83
83
  - `readable_sections`
@@ -221,7 +221,6 @@ class LogExtractor:
221
221
  # Exit 2 BLOCK (block_response is None):
222
222
  # - "Command blocked by security policy ..." -- permanent deny list
223
223
  # - "Commit message validation failed ..." -- validation error
224
- # - "GitOps policy violation ..." -- GitOps validation
225
224
  # - "Empty command not allowed"
226
225
  if (
227
226
  reason.startswith("Dangerous")
@@ -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,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
- )