@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/ARCHITECTURE.md +0 -1
- package/CHANGELOG.md +54 -0
- package/bin/cli/approvals.py +23 -21
- package/config/surface-routing.json +0 -1
- package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-ops/config/surface-routing.json +0 -1
- package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +18 -0
- package/dist/gaia-ops/hooks/modules/agents/handoff_persister.py +212 -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 +122 -19
- package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +99 -7
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +125 -24
- 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/reference.md +2 -2
- package/dist/gaia-ops/skills/orchestrator-present-approval/SKILL.md +20 -5
- package/dist/gaia-ops/skills/orchestrator-present-approval/reference.md +32 -15
- 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-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 +212 -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 +122 -19
- package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +99 -7
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +125 -24
- 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 +212 -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 +122 -19
- package/hooks/modules/security/mutative_verbs.py +99 -7
- package/hooks/modules/tools/bash_validator.py +125 -24
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/agent-contract-handoff/SKILL.md +3 -0
- package/skills/agent-response/SKILL.md +4 -2
- package/skills/gaia-patterns/reference.md +2 -2
- package/skills/orchestrator-present-approval/SKILL.md +20 -5
- package/skills/orchestrator-present-approval/reference.md +32 -15
- 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/dist/gaia-ops/hooks/modules/security/gitops_validator.py +0 -179
- package/dist/gaia-security/hooks/modules/security/gitops_validator.py +0 -179
- 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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 --
|
|
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
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
**
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
98
|
-
fingerprint and activates the grant on user
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
package/tools/context/README.md
CHANGED
|
@@ -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,
|
|
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
|
-
)
|