@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
|
@@ -30,9 +30,16 @@ without the data needed to decide. The job is **verbatim relay, not
|
|
|
30
30
|
re-authoring**: rewriting any of the 7 sealed fields breaks the fingerprint and
|
|
31
31
|
`verify_fingerprint` (`gaia/approvals/chain.py`) raises `ChainTamperError`.
|
|
32
32
|
|
|
33
|
-
## Step 0 --
|
|
33
|
+
## Step 0 -- Verify the approval against the DB (mandatory before SHOWN)
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
A subagent's reported `approval_id` is an unverified claim, not a fact. The agent runs in its own context and can relay an id that is stale, from another session, or simply wrong -- and a stale id presented as a fresh block walks the user into consenting to nothing real (or to a grant that no longer exists). The DB is the source of truth; the agent's report is a pointer into it that you must resolve, never the authority itself.
|
|
36
|
+
|
|
37
|
+
So before AskUserQuestion, two checks against the DB, in order:
|
|
38
|
+
|
|
39
|
+
1. **The approval exists, is fresh, and is from the current session.** Query `gaia approvals pending --session "$CLAUDE_SESSION_ID"` (or `--json` for parsing). The reported `approval_id` MUST appear in that result. If it appears only under `--all-sessions` but not the current session, it is leakage from another session (a test session such as `e2e-sim`, a prior run) -- **do not present**. If it does not appear at all, it does not exist or was already consumed/rejected -- **do not present**. Freshness is the `created_at` of the pending row plus its presence as still-`pending`; an id the agent reports that is not currently pending in *this* session is not a fresh block, whatever the agent says.
|
|
40
|
+
2. **The payload is untampered.** Call `verify_fingerprint(approval_id, payload_json, con) -> bool` from `gaia/approvals/chain.py`. It raises `ChainTamperError` if the payload was modified between subagent emission and your relay (security boundary, do not present), and `ValueError` if no REQUESTED event exists for this `approval_id`. Either case: **do not present**, report the failure, stop.
|
|
41
|
+
|
|
42
|
+
**For a `command_set` (plan-first batch) the agent does not know the id at all.** The hook mints the `approval_id` at SubagentStop (`_intake_command_set_pending` -- see Rule 3); the subagent emits the `command_set` with **no** `approval_id`. So you do not have an agent-reported id to trust even if you wanted to -- you ALWAYS recover the freshly minted id from `gaia approvals pending` for the current session. This is the general shape made unavoidable: the DB mints, the orchestrator recovers, the agent never owns the id.
|
|
36
43
|
|
|
37
44
|
## Mandatory presentation -- 5 labeled fields + nonce-suffixed label
|
|
38
45
|
|
|
@@ -71,12 +78,27 @@ Fields above are extracted from the DB-stored canonical payload (`payload_json`
|
|
|
71
78
|
grant consumed by the first retry (`consume_db_semantic_grant` in
|
|
72
79
|
`gaia/store/writer.py`). A second invocation is a new APPROVAL_REQUEST.
|
|
73
80
|
|
|
74
|
-
3. **
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
3. **Batch grant is `COMMAND_SET` -- one consent, N commands.** Legacy
|
|
82
|
+
`verb_family` was removed; its replacement, `COMMAND_SET`, is now wired
|
|
83
|
+
end-to-end (intake, activation, consume). When a subagent emits a plan-first
|
|
84
|
+
`APPROVAL_REQUEST` carrying a `command_set` of >= 2 `{command, rationale}`
|
|
85
|
+
items and **no** `approval_id`, the SubagentStop processor
|
|
86
|
+
(`handoff_persister._intake_command_set_pending`) mints ONE pending
|
|
87
|
+
`COMMAND_SET` with one `approval_id`. You present that single approval: list
|
|
88
|
+
**all N commands** in the question body, but use **one** Approve label with
|
|
89
|
+
**one** `[P-{nonce8}]` suffix -- one consent covers the whole batch. On
|
|
90
|
+
approval, `activate_db_pending_by_prefix` Step 3b creates a single
|
|
91
|
+
`COMMAND_SET` grant (60-min TTL); each command is consumed byte-for-byte on
|
|
92
|
+
its own retry. `batch_scope` is still ignored (the signal is `command_set`).
|
|
78
93
|
See `reference.md` -> "On batch intents".
|
|
79
94
|
|
|
95
|
+
You present the batch the subagent chose to send; you do not steer it toward
|
|
96
|
+
batching. Whether grouping is warranted is the subagent's judgment (known
|
|
97
|
+
batch, >= 2, friction reduced -- see `subagent-request-approval`). A singular
|
|
98
|
+
approval arriving where you imagined a batch is not a defect to correct: the
|
|
99
|
+
default is just-in-time, and a batch you would have manufactured asks the
|
|
100
|
+
user to consent to commands that may never run.
|
|
101
|
+
|
|
80
102
|
4. **Re-dispatch, do not resume.** `mode` does not survive a SendMessage resume:
|
|
81
103
|
the resume runs in `default` and re-blocks the next protected operation even
|
|
82
104
|
after the Gaia grant activated. Prefer a fresh re-dispatch with the same
|
|
@@ -97,5 +119,6 @@ wording, see `reference.md` -> "GOOD vs BAD Examples", "Option Label Patterns",
|
|
|
97
119
|
| "I'll skip the [P-...] suffix, it's cosmetic" | The hook extracts the nonce from the label to find the right pending row; without it, targeted activation fails and no grant is created. |
|
|
98
120
|
| "Similar command, slightly different path -- I'll reuse / wrap it" | Grants match the statement signature byte-for-byte. Any wrapper, redirect, flag, or path drift is a different signature and a fresh re-block. |
|
|
99
121
|
| "The same command emitted a new approval_id" | Grants are single-use and consumed on the first retry. A second run is a new APPROVAL_REQUEST -- approve again. |
|
|
100
|
-
| "I'll set batch_scope to approve many at once" |
|
|
122
|
+
| "I'll set batch_scope to approve many at once" | `batch_scope` is ignored -- but a real batch path exists: a plan-first `command_set` (>= 2 items, no `approval_id`) is intaken into ONE pending `COMMAND_SET`. Present that single approval (N commands shown, one `[P-...]` nonce, one consent), not N separate approvals. |
|
|
101
123
|
| "I can paraphrase a field before relaying" | The fingerprint covers all 7 sealed fields; any modification raises `ChainTamperError` in Step 0 and the presentation is refused. |
|
|
124
|
+
| **"The agent reported an `approval_id`, so it's a real fresh block"** -- trusting a nonce relayed by the subagent | The agent's reported id is an unverified pointer, not a fact. It can be stale or belong to another session -- subagents have presented a STALE nonce from a test session (`e2e-sim`) as if it were a fresh block. Resolve every reported id against `gaia approvals pending --session "$CLAUDE_SESSION_ID"` (Step 0): it must be currently pending in *this* session. Visible only under `--all-sessions`, or absent entirely, means do not present. For `command_set` the hook mints the id and the agent never has one -- you always recover it from the DB. |
|
|
@@ -107,32 +107,49 @@ contain `[P-<hex>]`. Reject labels never carry a nonce. The captured hex is the
|
|
|
107
107
|
`get_pending(all_sessions=True)` and selects the one whose `id` starts with
|
|
108
108
|
`P-{prefix}`.
|
|
109
109
|
|
|
110
|
-
## On batch intents --
|
|
110
|
+
## On batch intents -- the COMMAND_SET grant (one consent, N commands)
|
|
111
111
|
|
|
112
112
|
The old `verb_family` design (one approval covering many commands of the same
|
|
113
113
|
`base_cmd + verb`) **was removed**. The module docstring in
|
|
114
114
|
`hooks/modules/security/approval_grants.py` is explicit: "The legacy verb_family
|
|
115
115
|
path has been removed."
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
Its replacement is the `COMMAND_SET` grant: an explicit list of
|
|
118
118
|
`{command, rationale}` items, each matched **byte-for-byte** (D10: no whitespace
|
|
119
119
|
normalization, no quote canonicalization, no shell expansion) and consumed
|
|
120
120
|
individually (`create_command_set_grant` and `match_command_set_grant` in
|
|
121
121
|
`approval_grants.py`).
|
|
122
122
|
|
|
123
|
-
**Current state of the code
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
`
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
123
|
+
**Current state of the code: all three sides are wired -- intake, activation,
|
|
124
|
+
consume.** It is a **plan-first** flow: the subagent declares the batch up-front
|
|
125
|
+
by emitting an `APPROVAL_REQUEST` whose `approval_request` carries a
|
|
126
|
+
`command_set` list and **no** `approval_id`.
|
|
127
|
+
|
|
128
|
+
- **Intake.** The SubagentStop processor
|
|
129
|
+
`hooks/modules/agents/handoff_persister.py` ->
|
|
130
|
+
`_intake_command_set_pending()` reads the `command_set`; when it holds **>= 2**
|
|
131
|
+
items it calls `gaia.approvals.store.insert_requested()` with a payload that
|
|
132
|
+
contains the `command_set` key, minting **exactly ONE** pending `COMMAND_SET`
|
|
133
|
+
approval with one `approval_id`. A set of `<= 1` item is declined (no
|
|
134
|
+
COMMAND_SET is minted for one command).
|
|
135
|
+
- **Activation.** When the user approves, `activate_db_pending_by_prefix()`
|
|
136
|
+
(`hooks/modules/security/approval_grants.py`) reads `payload["command_set"]`,
|
|
137
|
+
and because it has > 1 item branches at **Step 3b** into
|
|
138
|
+
`create_command_set_grant()`, inserting ONE `COMMAND_SET` grant row (status
|
|
139
|
+
`PENDING`, `command_set_json` holding the whole set, 60-min TTL via
|
|
140
|
+
`DEFAULT_COMMAND_SET_TTL_MINUTES`) instead of a singular
|
|
141
|
+
`SCOPE_SEMANTIC_SIGNATURE` grant.
|
|
142
|
+
- **Consume.** On each retry, `bash_validator` calls `match_command_set_grant()`
|
|
143
|
+
(byte-for-byte index match), then `mark_command_set_item_consumed()`; a
|
|
144
|
+
consumed index never matches again (replay protection), and when every index
|
|
145
|
+
is consumed the grant flips to `CONSUMED`.
|
|
146
|
+
|
|
147
|
+
**Practical consequence:** a `batch_scope` field still does nothing -- the signal
|
|
148
|
+
is `command_set`. To approve a sweep of N related commands under one consent,
|
|
149
|
+
present the single `COMMAND_SET` approval the intake minted: show **all N
|
|
150
|
+
commands** in the question body, with **one** Approve label carrying **one**
|
|
151
|
+
`[P-{nonce8}]` suffix. The user gives one consent; each command then runs on its
|
|
152
|
+
own retry within the 60-minute window. You do NOT issue N separate approvals.
|
|
136
153
|
|
|
137
154
|
## Grant Activation Mechanics
|
|
138
155
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: readme-writing
|
|
3
|
-
description: Use when writing or updating a README for a Gaia component folder (agents/, skills/, hooks/, commands/, config/, bin/, tests/, build/,
|
|
3
|
+
description: Use when writing or updating a README for a Gaia component folder (agents/, skills/, hooks/, commands/, config/, bin/, tests/, build/, or the repo root)
|
|
4
4
|
metadata:
|
|
5
5
|
user-invocable: false
|
|
6
6
|
type: technique
|
|
@@ -185,4 +185,3 @@ Copy this when writing a README from scratch. Fill every section -- do not delet
|
|
|
185
185
|
| `bin/` | Low -- CLI tools, user-invoked | No |
|
|
186
186
|
| `tests/` | Low -- run by CI or developer | No |
|
|
187
187
|
| `build/` | Medium -- triggered by npm run build | Optional |
|
|
188
|
-
| `templates/` | Low -- read by build scripts | No |
|
|
@@ -17,7 +17,11 @@ security-tiers classifies every operation into four tiers so an agent knows whet
|
|
|
17
17
|
| **T0** | Read-only; observes state, changes nothing | No | get, list, describe, show, logs, status |
|
|
18
18
|
| **T1** | Local validation; no remote calls, no state | No | validate, lint, fmt, check |
|
|
19
19
|
| **T2** | Simulation / dry-run; may read remote, never writes | No | plan, diff, dry-run, template |
|
|
20
|
-
| **T3** | State-mutating; creates, updates, or destroys | **Yes** | apply, create, delete,
|
|
20
|
+
| **T3** | State-mutating; creates, updates, or destroys | **Yes** | apply, create, delete, push, deploy |
|
|
21
|
+
|
|
22
|
+
`git commit` and `git add` are **not** T3 -- they are local-only operations (they touch the working tree and local refs, never remote state), so they classify as safe by elimination. Only `git push` mutates remote state and is T3. This matches `GIT_LOCAL_SAFE_SUBCOMMANDS` in `mutative_verbs.py`, where `commit` and `add` are listed as local-safe.
|
|
23
|
+
|
|
24
|
+
**T3 gates a direction, not a category of verb.** An operation needs consent because it moves the system toward *more* capability (it grants) or *less* recoverability (it destroys). An operation that only moves the other way -- that *reduces* capability already granted -- does not need consent, because the worst it can do is take back power that was given. So within Gaia's own consent layer, `gaia approvals revoke|reject|reject-all|clean` are **not** T3: they only revoke or discard grants Gaia itself issued, never reaching outside the local approval store. The asymmetry is deliberate -- `gaia approvals approve` *grants* capability without the AskUserQuestion flow, so it stays T3. This is anchored to the `gaia approvals` group in `CONSENT_REDUCING_SUBCOMMAND_EXCEPTIONS` (`mutative_verbs.py`), not generalized to every CLI's "revoke" -- a cloud IAM revoke is a real remote mutation and remains T3.
|
|
21
25
|
|
|
22
26
|
## Classification heuristic
|
|
23
27
|
|
|
@@ -36,7 +36,9 @@ Read on-demand by infrastructure agents. Not injected automatically.
|
|
|
36
36
|
- `kubectl apply -f manifest.yaml`
|
|
37
37
|
- `helm upgrade` (without `--dry-run`)
|
|
38
38
|
- `flux reconcile` (write operations)
|
|
39
|
-
- `git
|
|
39
|
+
- `git push` (any branch) -- mutates remote state
|
|
40
|
+
|
|
41
|
+
Note: `git commit` and `git add` are **not** T3. They are local-only (working tree + local refs, never remote), classified safe by elimination via `GIT_LOCAL_SAFE_SUBCOMMANDS` in `mutative_verbs.py`. Only `git push` reaches remote state.
|
|
40
42
|
|
|
41
43
|
## Edge Cases
|
|
42
44
|
|
|
@@ -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
|
|
|
@@ -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")
|
|
@@ -16,6 +16,22 @@ import sys
|
|
|
16
16
|
from typing import Any, Dict, List, Optional
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Glyphs
|
|
21
|
+
#
|
|
22
|
+
# Hoisted to module-level constants so they are never written as escape
|
|
23
|
+
# sequences *inside* an f-string replacement field. A backslash within an
|
|
24
|
+
# f-string expression (e.g. f"{self._green('◇')}") is a SyntaxError on
|
|
25
|
+
# Python 3.11 (our declared minimum); it is only permitted on 3.12+ via PEP
|
|
26
|
+
# 701. Referencing a bare name inside the braces keeps the same rendered
|
|
27
|
+
# output while staying 3.11-compatible.
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
_GLYPH_DIAMOND_OUTLINE = "◇" # ◇
|
|
31
|
+
_GLYPH_WARNING = "⚠" # ⚠
|
|
32
|
+
_GLYPH_CORNER_BL = "└" # └
|
|
33
|
+
|
|
34
|
+
|
|
19
35
|
# ---------------------------------------------------------------------------
|
|
20
36
|
# ANSI color helpers
|
|
21
37
|
# ---------------------------------------------------------------------------
|
|
@@ -119,7 +135,7 @@ class RailUI:
|
|
|
119
135
|
name: Section title (e.g. "Stack", "Infrastructure").
|
|
120
136
|
lines: Detail lines to display under the section.
|
|
121
137
|
"""
|
|
122
|
-
self._write(f"{self._green(
|
|
138
|
+
self._write(f"{self._green(_GLYPH_DIAMOND_OUTLINE)} {self._cyan(name)}")
|
|
123
139
|
for line in lines:
|
|
124
140
|
self._write(f"{self._rail()} {line}")
|
|
125
141
|
self._write(self._rail())
|
|
@@ -131,7 +147,7 @@ class RailUI:
|
|
|
131
147
|
names: List of section names to join with middle-dot.
|
|
132
148
|
"""
|
|
133
149
|
joined = self._cyan(" \u00b7 ".join(names))
|
|
134
|
-
self._write(f"{self._green(
|
|
150
|
+
self._write(f"{self._green(_GLYPH_DIAMOND_OUTLINE)} {joined}")
|
|
135
151
|
self._write(self._rail())
|
|
136
152
|
|
|
137
153
|
def warning(self, count: int, messages: List[str]) -> None:
|
|
@@ -141,7 +157,7 @@ class RailUI:
|
|
|
141
157
|
count: Total number of warnings.
|
|
142
158
|
messages: Warning messages to display.
|
|
143
159
|
"""
|
|
144
|
-
self._write(f"{self._yellow(
|
|
160
|
+
self._write(f"{self._yellow(_GLYPH_WARNING)} {self._yellow(f'Warnings ({count})')}")
|
|
145
161
|
for msg in messages:
|
|
146
162
|
self._write(f"{self._rail()} {msg}")
|
|
147
163
|
self._write(self._rail())
|
|
@@ -193,7 +209,7 @@ class RailUI:
|
|
|
193
209
|
Args:
|
|
194
210
|
message: Footer message text.
|
|
195
211
|
"""
|
|
196
|
-
self._write(f"{self._dim(
|
|
212
|
+
self._write(f"{self._dim(_GLYPH_CORNER_BL)} {message}")
|
|
197
213
|
|
|
198
214
|
|
|
199
215
|
# ---------------------------------------------------------------------------
|
|
@@ -45,8 +45,8 @@ class CheckResult:
|
|
|
45
45
|
def check_symlinks(project_root: Path) -> CheckResult:
|
|
46
46
|
"""Verify that all expected symlinks exist in .claude/.
|
|
47
47
|
|
|
48
|
-
Checks for: agents, tools, hooks, commands,
|
|
49
|
-
skills, CHANGELOG.md (
|
|
48
|
+
Checks for: agents, tools, hooks, commands, config,
|
|
49
|
+
skills, CHANGELOG.md (7 total).
|
|
50
50
|
|
|
51
51
|
Args:
|
|
52
52
|
project_root: Project root directory.
|
|
@@ -56,7 +56,7 @@ def check_symlinks(project_root: Path) -> CheckResult:
|
|
|
56
56
|
"""
|
|
57
57
|
names = [
|
|
58
58
|
"agents", "tools", "hooks", "commands",
|
|
59
|
-
"
|
|
59
|
+
"config", "skills",
|
|
60
60
|
"CHANGELOG.md",
|
|
61
61
|
]
|
|
62
62
|
valid = 0
|
|
@@ -25,7 +25,7 @@ without requiring explicit imports in agent code.
|
|
|
25
25
|
- ✅ Subject line rules (max 72 chars, no period at end)
|
|
26
26
|
- ✅ Forbidden footers (no "Generated with" footers)
|
|
27
27
|
|
|
28
|
-
**Configuration:**
|
|
28
|
+
**Configuration:** Standards are inlined as module-level constants in `hooks/modules/validation/commit_validator.py` (`TYPE_ALLOWED`, `SUBJECT_MAX_LENGTH`, `SUBJECT_RULES`, `BODY_MAX_LINE_LENGTH`, `ENFORCEMENT`). Forbidden-footer detection lives in `bash_validator`.
|
|
29
29
|
**Logs:** `.claude/logs/commit-violations.jsonl`
|
|
30
30
|
|
|
31
31
|
---
|
|
@@ -108,16 +108,11 @@ This validation module works with skills in a **hybrid model**:
|
|
|
108
108
|
|
|
109
109
|
```
|
|
110
110
|
┌──────────────────────────────────────────────────────────┐
|
|
111
|
-
│ config/git_standards.json (SSOT) │
|
|
112
|
-
│ - Conventional commit types │
|
|
113
|
-
│ - Forbidden footers │
|
|
114
|
-
│ - Max lengths │
|
|
115
|
-
└──────────────────────────────────────────────────────────┘
|
|
116
|
-
│
|
|
117
|
-
▼
|
|
118
|
-
┌──────────────────────────────────────────────────────────┐
|
|
119
111
|
│ hooks/modules/validation/ (Commit Validation) │
|
|
120
112
|
│ └─ commit_validator.py │
|
|
113
|
+
│ ├─ Standards inlined as module-level constants │
|
|
114
|
+
│ │ (types, subject/body max lengths, rules) │
|
|
115
|
+
│ ├─ Forbidden footers handled by bash_validator │
|
|
121
116
|
│ └─ Used by bash_validator.py only │
|
|
122
117
|
└──────────────────────────────────────────────────────────┘
|
|
123
118
|
│
|
|
@@ -178,24 +173,20 @@ Note: commit_validator.py moved to hooks/modules/validation/
|
|
|
178
173
|
|
|
179
174
|
## Configuration
|
|
180
175
|
|
|
181
|
-
**Git Standards:**
|
|
176
|
+
**Git Standards:** Inlined as module-level constants in `hooks/modules/validation/commit_validator.py`.
|
|
182
177
|
|
|
183
178
|
Example:
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
"enforcement": {
|
|
192
|
-
"enabled": true,
|
|
193
|
-
"block_on_failure": true,
|
|
194
|
-
"log_violations": true
|
|
195
|
-
}
|
|
196
|
-
}
|
|
179
|
+
```python
|
|
180
|
+
TYPE_ALLOWED = ("feat", "fix", "refactor", "docs", "test", "chore",
|
|
181
|
+
"ci", "perf", "style", "build")
|
|
182
|
+
SUBJECT_MAX_LENGTH = 72
|
|
183
|
+
SUBJECT_RULES = {"no_period_at_end": True, "no_emoji": True,
|
|
184
|
+
"imperative_mood": True, "capitalize_first_letter": False}
|
|
185
|
+
ENFORCEMENT = {"enabled": True, "block_on_failure": True, "log_violations": True}
|
|
197
186
|
```
|
|
198
187
|
|
|
188
|
+
Forbidden-footer detection lives, hardcoded, in `bash_validator`.
|
|
189
|
+
|
|
199
190
|
---
|
|
200
191
|
|
|
201
192
|
## Logs
|
|
@@ -232,7 +223,7 @@ Example entry:
|
|
|
232
223
|
|
|
233
224
|
## See Also
|
|
234
225
|
|
|
235
|
-
-
|
|
226
|
+
- `hooks/modules/validation/commit_validator.py` - Git standards (inlined constants)
|
|
236
227
|
- `.claude/skills/subagent-request-approval/SKILL.md` - Approval-request workflow patterns
|
|
237
228
|
- `.claude/skills/execution/SKILL.md` - Execution workflow patterns
|
|
238
229
|
- `CLAUDE.md` - Orchestrator protocol with T3 workflow
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gaia-security",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.5",
|
|
4
4
|
"description": "Keeps you in the loop only when it matters. Gaia Security analyzes every command and classifies it into risk tiers: read-only queries run freely, simulations and validations pass through, and state-changing operations (create, delete, apply, push) pause for your explicit approval before executing. Irreversible commands like dropping databases or deleting cloud infrastructure are permanently blocked.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "jaguilar87",
|
|
@@ -19,6 +19,7 @@ Provides:
|
|
|
19
19
|
- parse_rollback_executed(): Parse rollback_executed clause (advisory)
|
|
20
20
|
- parse_context_consumption(): Parse context_consumption clause (advisory)
|
|
21
21
|
- parse_memory_suggestions(): Parse memory_suggestions clause (advisory)
|
|
22
|
+
- parse_user_facing_summary(): Parse user_facing_summary clause (advisory)
|
|
22
23
|
"""
|
|
23
24
|
|
|
24
25
|
import json
|
|
@@ -655,6 +656,23 @@ def parse_memory_suggestions(contract: dict) -> List[str]:
|
|
|
655
656
|
return [str(item) for item in raw if item is not None]
|
|
656
657
|
|
|
657
658
|
|
|
659
|
+
def parse_user_facing_summary(contract: dict) -> Optional[str]:
|
|
660
|
+
"""Parse the optional top-level ``user_facing_summary`` clause (advisory).
|
|
661
|
+
|
|
662
|
+
The single human-audience field in the contract: a short prose summary the
|
|
663
|
+
subagent writes once for the user. The orchestrator relays it near-verbatim
|
|
664
|
+
on a single-agent COMPLETE (N=1) instead of re-synthesizing ``key_outputs``.
|
|
665
|
+
|
|
666
|
+
Strictly additive and advisory -- the validator never rejects based on this
|
|
667
|
+
field. Returns the trimmed string when present and non-empty, else None.
|
|
668
|
+
"""
|
|
669
|
+
raw = contract.get("user_facing_summary")
|
|
670
|
+
if not isinstance(raw, str):
|
|
671
|
+
return None
|
|
672
|
+
text = raw.strip()
|
|
673
|
+
return text or None
|
|
674
|
+
|
|
675
|
+
|
|
658
676
|
def extract_plan_status_from_output(agent_output: str) -> str:
|
|
659
677
|
"""Extract the effective plan_status string from agent output.
|
|
660
678
|
|