@jaguilar87/gaia 5.0.7 → 5.0.8
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/CHANGELOG.md +2 -0
- package/bin/cli/approvals.py +145 -236
- package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-ops/hooks/adapters/claude_code.py +73 -1
- package/dist/gaia-ops/hooks/modules/agents/handoff_persister.py +13 -2
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +19 -0
- package/dist/gaia-ops/skills/agent-approval-protocol/SKILL.md +28 -12
- package/dist/gaia-ops/skills/agent-approval-protocol/reference.md +5 -3
- package/dist/gaia-ops/skills/agent-protocol/examples.md +12 -1
- package/dist/gaia-ops/skills/orchestrator-present-approval/SKILL.md +8 -2
- package/dist/gaia-ops/skills/orchestrator-present-approval/template.md +11 -10
- package/dist/gaia-ops/skills/subagent-request-approval/SKILL.md +11 -0
- package/dist/gaia-ops/skills/subagent-request-approval/reference.md +21 -3
- package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-security/hooks/adapters/claude_code.py +73 -1
- package/dist/gaia-security/hooks/modules/agents/handoff_persister.py +13 -2
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +19 -0
- package/gaia/approvals/__init__.py +2 -1
- package/gaia/approvals/store.py +78 -6
- package/hooks/adapters/claude_code.py +73 -1
- package/hooks/modules/agents/handoff_persister.py +13 -2
- package/hooks/modules/tools/bash_validator.py +19 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/agent-approval-protocol/SKILL.md +28 -12
- package/skills/agent-approval-protocol/reference.md +5 -3
- package/skills/agent-protocol/examples.md +12 -1
- package/skills/orchestrator-present-approval/SKILL.md +8 -2
- package/skills/orchestrator-present-approval/template.md +11 -10
- package/skills/subagent-request-approval/SKILL.md +11 -0
- package/skills/subagent-request-approval/reference.md +21 -3
- package/gaia/approvals/revert.py +0 -282
package/gaia/approvals/revert.py
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
"""gaia.approvals.revert -- Inverse-command derivation for approval revert.
|
|
2
|
-
|
|
3
|
-
Per D14 (plan design decision), revert works by querying EXECUTED events
|
|
4
|
-
for an approval, deriving candidate inverse commands using a hardcoded
|
|
5
|
-
best-effort mapping, and presenting them for user confirmation.
|
|
6
|
-
|
|
7
|
-
Public API:
|
|
8
|
-
InverseCommand -- dataclass for a candidate inverse operation
|
|
9
|
-
derive_inverse(event) -> InverseCommand | None
|
|
10
|
-
derive_inverses_for_approval(approval_id, con) -> list[InverseCommand]
|
|
11
|
-
|
|
12
|
-
The InverseCommand dataclass carries:
|
|
13
|
-
event_id: int -- the original EXECUTED event id
|
|
14
|
-
original_command: str -- the original command (from payload or metadata)
|
|
15
|
-
inverse_command: str | None -- the derived inverse, or None if NOT REVERSIBLE
|
|
16
|
-
reversible: bool -- False when no inverse can be derived
|
|
17
|
-
notes: str -- human-readable explanation
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
from __future__ import annotations
|
|
21
|
-
|
|
22
|
-
import json
|
|
23
|
-
import re
|
|
24
|
-
import sqlite3
|
|
25
|
-
from dataclasses import dataclass, field
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
from typing import Any, Dict, List, Optional
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@dataclass
|
|
31
|
-
class InverseCommand:
|
|
32
|
-
"""Candidate inverse operation for an EXECUTED approval event."""
|
|
33
|
-
|
|
34
|
-
event_id: int
|
|
35
|
-
original_command: str
|
|
36
|
-
inverse_command: Optional[str]
|
|
37
|
-
reversible: bool
|
|
38
|
-
notes: str
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# ---------------------------------------------------------------------------
|
|
42
|
-
# Hardcoded verb -> inverse mapping (D14)
|
|
43
|
-
# ---------------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
# Pattern-based inverse rules. Each entry is (pattern_re, inverse_template_fn).
|
|
46
|
-
# The pattern is matched against the original command. The template function
|
|
47
|
-
# receives the re.Match object and returns the inverse command string.
|
|
48
|
-
_INVERSE_RULES: list[tuple[re.Pattern, Any]] = []
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _rule(pattern: str):
|
|
52
|
-
"""Decorator to register an inverse rule."""
|
|
53
|
-
def decorator(fn):
|
|
54
|
-
_INVERSE_RULES.append((re.compile(pattern), fn))
|
|
55
|
-
return fn
|
|
56
|
-
return decorator
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@_rule(r"^gaia\s+brief\s+set-status\s+(\S+)\s+done\s*$")
|
|
60
|
-
def _brief_done_to_pending(m):
|
|
61
|
-
brief_id = m.group(1)
|
|
62
|
-
return f"gaia brief set-status {brief_id} pending"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
@_rule(r"^gaia\s+brief\s+set-status\s+(\S+)\s+active\s*$")
|
|
66
|
-
def _brief_active_to_draft(m):
|
|
67
|
-
brief_id = m.group(1)
|
|
68
|
-
return f"gaia brief set-status {brief_id} draft"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
@_rule(r"^gaia\s+brief\s+set-status\s+(\S+)\s+pending\s*$")
|
|
72
|
-
def _brief_pending_to_draft(m):
|
|
73
|
-
brief_id = m.group(1)
|
|
74
|
-
return f"gaia brief set-status {brief_id} draft"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
@_rule(r"^git\s+branch\s+(\S+)\s*$")
|
|
78
|
-
def _git_branch_create_to_delete(m):
|
|
79
|
-
branch = m.group(1)
|
|
80
|
-
if branch.startswith("-"):
|
|
81
|
-
# Already a delete flag -- not a create
|
|
82
|
-
return None
|
|
83
|
-
return f"git branch -D {branch}"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
@_rule(r"^git\s+branch\s+-[bB]\s+(\S+)\s*$")
|
|
87
|
-
def _git_branch_b_to_delete(m):
|
|
88
|
-
branch = m.group(1)
|
|
89
|
-
return f"git branch -D {branch}"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@_rule(r"^rm\s+(.+)\s*$")
|
|
93
|
-
def _rm_not_reversible(_m):
|
|
94
|
-
# rm has no generic inverse; caller sees NOT REVERSIBLE
|
|
95
|
-
return None
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def _derive_from_command_string(command: str) -> InverseCommand | None:
|
|
99
|
-
"""Apply hardcoded rules to a single command string.
|
|
100
|
-
|
|
101
|
-
Returns an InverseCommand on the first match, or None when no rule matches.
|
|
102
|
-
"""
|
|
103
|
-
cmd = command.strip()
|
|
104
|
-
for pattern, fn in _INVERSE_RULES:
|
|
105
|
-
m = pattern.match(cmd)
|
|
106
|
-
if m:
|
|
107
|
-
inverse = fn(m)
|
|
108
|
-
if inverse is not None:
|
|
109
|
-
return InverseCommand(
|
|
110
|
-
event_id=0,
|
|
111
|
-
original_command=cmd,
|
|
112
|
-
inverse_command=inverse,
|
|
113
|
-
reversible=True,
|
|
114
|
-
notes=f"Derived from pattern: {pattern.pattern}",
|
|
115
|
-
)
|
|
116
|
-
else:
|
|
117
|
-
return InverseCommand(
|
|
118
|
-
event_id=0,
|
|
119
|
-
original_command=cmd,
|
|
120
|
-
inverse_command=None,
|
|
121
|
-
reversible=False,
|
|
122
|
-
notes="NOT REVERSIBLE -- matched pattern has no safe inverse",
|
|
123
|
-
)
|
|
124
|
-
return None
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _is_file_create(payload: dict, original_cmd: str) -> bool:
|
|
128
|
-
"""Heuristic: detect if the payload represents a file creation."""
|
|
129
|
-
commands = payload.get("commands") or []
|
|
130
|
-
scope = payload.get("scope") or ""
|
|
131
|
-
# A Write tool on a new path is stored with 'write' or 'create' in operation.
|
|
132
|
-
operation = (payload.get("operation") or "").lower()
|
|
133
|
-
if "write" in operation or "create" in operation:
|
|
134
|
-
return True
|
|
135
|
-
# If the scope looks like a file path and command is a write-like verb
|
|
136
|
-
if scope and ("write" in original_cmd.lower() or "create" in original_cmd.lower()):
|
|
137
|
-
return True
|
|
138
|
-
return False
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def derive_inverse(event: Dict[str, Any]) -> InverseCommand:
|
|
142
|
-
"""Derive a candidate inverse command for a single EXECUTED event.
|
|
143
|
-
|
|
144
|
-
Best-effort approach per D14:
|
|
145
|
-
- Tries hardcoded verb patterns first.
|
|
146
|
-
- Detects file create -> suggests rm <path>.
|
|
147
|
-
- Falls through to "NOT REVERSIBLE" with original command for reference.
|
|
148
|
-
|
|
149
|
-
Args:
|
|
150
|
-
event: A dict from store.replay_for_approval() representing an
|
|
151
|
-
EXECUTED event row. Must have 'id', 'payload_json', and
|
|
152
|
-
optionally 'metadata_json'.
|
|
153
|
-
|
|
154
|
-
Returns:
|
|
155
|
-
InverseCommand with reversible=True if an inverse was derived, or
|
|
156
|
-
reversible=False with inverse_command=None and a "NOT REVERSIBLE"
|
|
157
|
-
note.
|
|
158
|
-
"""
|
|
159
|
-
event_id = event.get("id", 0)
|
|
160
|
-
payload_json = event.get("payload_json") or ""
|
|
161
|
-
metadata_json = event.get("metadata_json") or ""
|
|
162
|
-
|
|
163
|
-
payload: dict = {}
|
|
164
|
-
if payload_json:
|
|
165
|
-
try:
|
|
166
|
-
payload = json.loads(payload_json)
|
|
167
|
-
except (json.JSONDecodeError, TypeError):
|
|
168
|
-
pass
|
|
169
|
-
|
|
170
|
-
metadata: dict = {}
|
|
171
|
-
if metadata_json:
|
|
172
|
-
try:
|
|
173
|
-
metadata = json.loads(metadata_json)
|
|
174
|
-
except (json.JSONDecodeError, TypeError):
|
|
175
|
-
pass
|
|
176
|
-
|
|
177
|
-
# Collect commands to try inverting.
|
|
178
|
-
commands = payload.get("commands") or []
|
|
179
|
-
exact_content = payload.get("exact_content") or ""
|
|
180
|
-
|
|
181
|
-
# Build list of individual command strings to invert.
|
|
182
|
-
cmd_strings: list[str] = []
|
|
183
|
-
if commands:
|
|
184
|
-
cmd_strings = [str(c).strip() for c in commands if c]
|
|
185
|
-
elif exact_content:
|
|
186
|
-
# Split newline-separated commands per D13.
|
|
187
|
-
cmd_strings = [l.strip() for l in exact_content.splitlines() if l.strip()]
|
|
188
|
-
|
|
189
|
-
if not cmd_strings:
|
|
190
|
-
# No commands found -- check operation field.
|
|
191
|
-
operation = payload.get("operation") or ""
|
|
192
|
-
if operation:
|
|
193
|
-
cmd_strings = [operation]
|
|
194
|
-
|
|
195
|
-
if not cmd_strings:
|
|
196
|
-
return InverseCommand(
|
|
197
|
-
event_id=event_id,
|
|
198
|
-
original_command="(no command recorded)",
|
|
199
|
-
inverse_command=None,
|
|
200
|
-
reversible=False,
|
|
201
|
-
notes="NOT REVERSIBLE -- no command data found in event payload",
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
# For multi-command events, try to invert each command.
|
|
205
|
-
# If ALL have inverses, return a compound inverse. If any fails, NOT REVERSIBLE.
|
|
206
|
-
inverses = []
|
|
207
|
-
original_summary = "; ".join(cmd_strings)
|
|
208
|
-
|
|
209
|
-
for cmd in cmd_strings:
|
|
210
|
-
result = _derive_from_command_string(cmd)
|
|
211
|
-
if result is None:
|
|
212
|
-
# No rule matched -- check for file create heuristic.
|
|
213
|
-
scope = payload.get("scope") or ""
|
|
214
|
-
if scope and _is_file_create(payload, cmd):
|
|
215
|
-
# Suggest rm <scope> as the inverse.
|
|
216
|
-
scope_path = scope.strip()
|
|
217
|
-
inverses.append(
|
|
218
|
-
InverseCommand(
|
|
219
|
-
event_id=event_id,
|
|
220
|
-
original_command=cmd,
|
|
221
|
-
inverse_command=f"rm {scope_path}",
|
|
222
|
-
reversible=True,
|
|
223
|
-
notes=f"File create detected -- inverse is rm {scope_path} (requires confirm)",
|
|
224
|
-
)
|
|
225
|
-
)
|
|
226
|
-
else:
|
|
227
|
-
return InverseCommand(
|
|
228
|
-
event_id=event_id,
|
|
229
|
-
original_command=original_summary,
|
|
230
|
-
inverse_command=None,
|
|
231
|
-
reversible=False,
|
|
232
|
-
notes=f"NOT REVERSIBLE -- no inverse rule matches: {cmd!r}",
|
|
233
|
-
)
|
|
234
|
-
else:
|
|
235
|
-
result.event_id = event_id
|
|
236
|
-
inverses.append(result)
|
|
237
|
-
|
|
238
|
-
if len(inverses) == 1:
|
|
239
|
-
return inverses[0]
|
|
240
|
-
|
|
241
|
-
# Multiple inverses -- combine into a compound inverse.
|
|
242
|
-
combined_inverse = " && ".join(ic.inverse_command for ic in inverses if ic.inverse_command)
|
|
243
|
-
combined_notes = "; ".join(ic.notes for ic in inverses)
|
|
244
|
-
return InverseCommand(
|
|
245
|
-
event_id=event_id,
|
|
246
|
-
original_command=original_summary,
|
|
247
|
-
inverse_command=combined_inverse if combined_inverse else None,
|
|
248
|
-
reversible=bool(combined_inverse),
|
|
249
|
-
notes=combined_notes,
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
def derive_inverses_for_approval(
|
|
254
|
-
approval_id: str,
|
|
255
|
-
con: sqlite3.Connection,
|
|
256
|
-
) -> List[InverseCommand]:
|
|
257
|
-
"""Return a list of InverseCommand for all EXECUTED events of an approval.
|
|
258
|
-
|
|
259
|
-
Args:
|
|
260
|
-
approval_id: The P-{uuid4} approval identifier.
|
|
261
|
-
con: An open sqlite3.Connection.
|
|
262
|
-
|
|
263
|
-
Returns:
|
|
264
|
-
List of InverseCommand, one per EXECUTED event, in insertion order.
|
|
265
|
-
Empty list if no EXECUTED events exist.
|
|
266
|
-
"""
|
|
267
|
-
cur = con.execute(
|
|
268
|
-
"SELECT id, payload_json, metadata_json FROM approval_events "
|
|
269
|
-
"WHERE approval_id = ? AND event_type = 'EXECUTED' "
|
|
270
|
-
"ORDER BY id ASC",
|
|
271
|
-
(approval_id,),
|
|
272
|
-
)
|
|
273
|
-
rows = cur.fetchall()
|
|
274
|
-
result = []
|
|
275
|
-
for row in rows:
|
|
276
|
-
event = {
|
|
277
|
-
"id": row[0],
|
|
278
|
-
"payload_json": row[1],
|
|
279
|
-
"metadata_json": row[2],
|
|
280
|
-
}
|
|
281
|
-
result.append(derive_inverse(event))
|
|
282
|
-
return result
|