@jaguilar87/gaia 5.0.7 → 5.0.9

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 (99) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +13 -0
  4. package/bin/README.md +6 -1
  5. package/bin/cli/approvals.py +486 -474
  6. package/bin/cli/brief.py +13 -0
  7. package/bin/cli/doctor.py +1 -1
  8. package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
  9. package/dist/gaia-ops/hooks/adapters/claude_code.py +92 -86
  10. package/dist/gaia-ops/hooks/modules/agents/handoff_persister.py +13 -2
  11. package/dist/gaia-ops/hooks/modules/context/context_injector.py +23 -7
  12. package/dist/gaia-ops/hooks/modules/events/event_writer.py +63 -96
  13. package/dist/gaia-ops/hooks/modules/security/__init__.py +0 -2
  14. package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +238 -69
  15. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +506 -1103
  16. package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +24 -1
  17. package/dist/gaia-ops/hooks/modules/session/pending_scanner.py +150 -90
  18. package/dist/gaia-ops/hooks/modules/session/session_manifest.py +257 -28
  19. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +19 -0
  20. package/dist/gaia-ops/hooks/post_compact.py +1 -0
  21. package/dist/gaia-ops/hooks/pre_compact.py +1 -0
  22. package/dist/gaia-ops/hooks/user_prompt_submit.py +20 -0
  23. package/dist/gaia-ops/skills/agent-approval-protocol/SKILL.md +50 -14
  24. package/dist/gaia-ops/skills/agent-approval-protocol/reference.md +16 -9
  25. package/dist/gaia-ops/skills/agent-protocol/examples.md +12 -1
  26. package/dist/gaia-ops/skills/gaia-patterns/reference.md +2 -2
  27. package/dist/gaia-ops/skills/orchestrator-present-approval/SKILL.md +69 -22
  28. package/dist/gaia-ops/skills/orchestrator-present-approval/reference.md +16 -3
  29. package/dist/gaia-ops/skills/orchestrator-present-approval/template.md +20 -14
  30. package/dist/gaia-ops/skills/pending-approvals/SKILL.md +16 -11
  31. package/dist/gaia-ops/skills/subagent-request-approval/SKILL.md +28 -3
  32. package/dist/gaia-ops/skills/subagent-request-approval/reference.md +34 -8
  33. package/dist/gaia-ops/tools/migration/README.md +10 -12
  34. package/dist/gaia-ops/tools/scan/orchestrator.py +194 -10
  35. package/dist/gaia-ops/tools/scan/tests/test_integration.py +1 -2
  36. package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
  37. package/dist/gaia-security/hooks/adapters/claude_code.py +92 -86
  38. package/dist/gaia-security/hooks/modules/agents/handoff_persister.py +13 -2
  39. package/dist/gaia-security/hooks/modules/context/context_injector.py +23 -7
  40. package/dist/gaia-security/hooks/modules/events/event_writer.py +63 -96
  41. package/dist/gaia-security/hooks/modules/security/__init__.py +0 -2
  42. package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +238 -69
  43. package/dist/gaia-security/hooks/modules/security/approval_grants.py +506 -1103
  44. package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +24 -1
  45. package/dist/gaia-security/hooks/modules/session/pending_scanner.py +150 -90
  46. package/dist/gaia-security/hooks/modules/session/session_manifest.py +257 -28
  47. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +19 -0
  48. package/dist/gaia-security/hooks/user_prompt_submit.py +20 -0
  49. package/gaia/approvals/__init__.py +2 -1
  50. package/gaia/approvals/store.py +165 -15
  51. package/gaia/store/schema.sql +38 -1
  52. package/gaia/store/writer.py +400 -0
  53. package/hooks/adapters/claude_code.py +92 -86
  54. package/hooks/elicitation_result.py +20 -75
  55. package/hooks/modules/agents/handoff_persister.py +13 -2
  56. package/hooks/modules/context/context_injector.py +23 -7
  57. package/hooks/modules/events/event_writer.py +63 -96
  58. package/hooks/modules/security/__init__.py +0 -2
  59. package/hooks/modules/security/approval_cleanup.py +238 -69
  60. package/hooks/modules/security/approval_grants.py +506 -1103
  61. package/hooks/modules/security/mutative_verbs.py +24 -1
  62. package/hooks/modules/session/pending_scanner.py +150 -90
  63. package/hooks/modules/session/session_manifest.py +257 -28
  64. package/hooks/modules/tools/bash_validator.py +19 -0
  65. package/hooks/post_compact.py +1 -0
  66. package/hooks/pre_compact.py +1 -0
  67. package/hooks/user_prompt_submit.py +20 -0
  68. package/package.json +1 -1
  69. package/pyproject.toml +1 -1
  70. package/scripts/bootstrap_database.sh +66 -17
  71. package/scripts/migrations/README.md +26 -14
  72. package/scripts/migrations/schema.checksum +2 -2
  73. package/scripts/migrations/v18_to_v19.sql +36 -0
  74. package/scripts/migrations/v19_to_v20.sql +20 -0
  75. package/skills/agent-approval-protocol/SKILL.md +50 -14
  76. package/skills/agent-approval-protocol/reference.md +16 -9
  77. package/skills/agent-protocol/examples.md +12 -1
  78. package/skills/gaia-patterns/reference.md +2 -2
  79. package/skills/orchestrator-present-approval/SKILL.md +69 -22
  80. package/skills/orchestrator-present-approval/reference.md +16 -3
  81. package/skills/orchestrator-present-approval/template.md +20 -14
  82. package/skills/pending-approvals/SKILL.md +16 -11
  83. package/skills/subagent-request-approval/SKILL.md +28 -3
  84. package/skills/subagent-request-approval/reference.md +34 -8
  85. package/tools/migration/README.md +10 -12
  86. package/tools/scan/orchestrator.py +194 -10
  87. package/tools/scan/tests/test_integration.py +1 -2
  88. package/bin/cli/plans.py +0 -517
  89. package/dist/gaia-ops/tools/context/deep_merge.py +0 -159
  90. package/dist/gaia-ops/tools/migration/migrate_04_harness_events.py +0 -132
  91. package/dist/gaia-ops/tools/migration/migrate_04_harness_events.sh +0 -23
  92. package/dist/gaia-ops/tools/scan/merge.py +0 -213
  93. package/dist/gaia-ops/tools/scan/tests/test_merge.py +0 -269
  94. package/gaia/approvals/revert.py +0 -282
  95. package/tools/context/deep_merge.py +0 -159
  96. package/tools/migration/migrate_04_harness_events.py +0 -132
  97. package/tools/migration/migrate_04_harness_events.sh +0 -23
  98. package/tools/scan/merge.py +0 -213
  99. package/tools/scan/tests/test_merge.py +0 -269
@@ -15,11 +15,13 @@ names the specific action. No exceptions. No brevity shortcuts.
15
15
  ```
16
16
 
17
17
  `orchestrator-present-approval` is the discipline the orchestrator follows when
18
- a subagent emits `APPROVAL_REQUEST` with an `approval_id`: relay the
19
- `sealed_payload` into AskUserQuestion -- fingerprint check, mandatory fields in
20
- the question, mandatory nonce in the option label. For the subagent side that
21
- produced the payload see `subagent-request-approval`; for the data contract
22
- itself see `agent-approval-protocol`.
18
+ an approval needs the user's consent: relay the sealed fields into
19
+ AskUserQuestion -- mandatory fields in the question, mandatory nonce in the
20
+ option label. The orchestrator has no shell, so it never dispatches a subagent
21
+ to derive or verify an approval; it presents from a trusted source it already
22
+ holds. For the subagent side that produced the payload see
23
+ `subagent-request-approval`; for the data contract itself see
24
+ `agent-approval-protocol`.
23
25
 
24
26
  ## Mental Model
25
27
 
@@ -27,19 +29,53 @@ The orchestrator sits between the subagent and the user. The user cannot make
27
29
  an informed decision on data they have not seen -- a summary, a reference to
28
30
  "the plan above", or an offer to show details on request all push the decision
29
31
  without the data needed to decide. The job is **verbatim relay, not
30
- re-authoring**: rewriting any of the 7 sealed fields breaks the fingerprint and
31
- `verify_fingerprint` (`gaia/approvals/chain.py`) raises `ChainTamperError`.
32
-
33
- ## Step 0 -- Verify the approval against the DB (mandatory before SHOWN)
34
-
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.
32
+ re-authoring**: rewriting any of the sealed fields would change the consent
33
+ surface from what was recorded. Integrity of the payload is enforced at grant
34
+ **activation** (`verify_fingerprint` in `gaia/approvals/chain.py`, called when
35
+ the user selects the Approve label), not at presentation -- so presentation
36
+ itself never needs a verify-dispatch.
37
+
38
+ ## Step 0 -- Present from a trusted source; never dispatch to verify or derive
39
+
40
+ The orchestrator has no shell. It MUST NOT dispatch a subagent solely to derive
41
+ or verify an approval before presenting -- that dispatch is both unnecessary
42
+ (the integrity check runs at activation, below) and harmful (its SubagentStop
43
+ can sweep the very pending being verified). Instead, present from one of two
44
+ **trusted** sources:
45
+
46
+ 1. **Primary -- the injected `[PENDING-APPROVALS-VERIFIED]` block.** A per-turn
47
+ hook (`hooks/modules/session/session_manifest.py`) injects, on every
48
+ `UserPromptSubmit`, every pending that has survived >= 1 turn. Each row in
49
+ that block has already been DB-read and fingerprint-verified by the hook
50
+ (`build_verified_pending_approvals` -- only rows whose payload re-canonicalizes
51
+ to the fingerprint stored on their `REQUESTED` event appear, each marked
52
+ `verified: true`). **Present directly from this block** -- the fields, the
53
+ full `approval_id`, and (for batches) the whole `command_set` with its minted
54
+ id are all there. No DB query, no `derive-id`, no dispatch.
55
+ 2. **Fallback -- same-turn relay.** A pending a subagent emits during the
56
+ CURRENT turn will not be in this turn's block yet: the block is built at
57
+ `UserPromptSubmit`, before the subagent ran. For that case present from the
58
+ subagent's relayed `approval_request`. This is justified because the pending
59
+ was freshly minted in THIS session by a trusted dispatch, AND integrity is
60
+ enforced at grant **activation** (`verify_fingerprint` fires when the user
61
+ selects the Approve label), not at presentation. The old pre-presentation
62
+ verify was redundant belt-and-suspenders; it is removed.
63
+
64
+ Once the pending survives a turn it appears in the injected block, so the relay
65
+ is only ever needed for the same-turn case.
66
+
67
+ **For a `command_set` (plan-first batch) you do not derive the id -- you read it
68
+ from the block.** The hook mints the `approval_id` at SubagentStop
69
+ (`_intake_command_set_pending` -- see Rule 3) from the **content** of the
70
+ command_set (`derive_command_set_id` in `gaia/approvals/store.py`,
71
+ `P-<first 32 hex of sha256(canonical(command list))>`). Once that pending has
72
+ survived a turn, the `[PENDING-APPROVALS-VERIFIED]` block carries it with its
73
+ minted `approval_id` and all N commands already attached -- so you read the id
74
+ and the commands straight from the block. **No `gaia approvals derive-id`
75
+ dispatch is needed.** For a command_set emitted in the CURRENT turn (not yet in
76
+ the block), present from the subagent's relayed `approval_request`, which carries
77
+ the same `command_set`; the content-derived id reaches you when the pending
78
+ appears in the next turn's block.
43
79
 
44
80
  ## Mandatory presentation -- 5 labeled fields + nonce-suffixed label
45
81
 
@@ -66,7 +102,13 @@ whose `id` starts with `P-{prefix}`. Without the suffix no grant is created.
66
102
  See `template.md` for the canonical layout and `reference.md` -> "GOOD vs BAD
67
103
  Examples" for full presentations.
68
104
 
69
- Fields above are extracted from the DB-stored canonical payload (`payload_json` on the REQUESTED row), not from the subagent's relayed `approval_request` — that's why `rollback_hint` is the field name here while the subagent contract uses `rollback`.
105
+ Fields above are extracted from your trusted source. From the injected
106
+ `[PENDING-APPROVALS-VERIFIED]` block (the primary path) they appear under the
107
+ canonical names shown here (`operation`, `exact_content`, `scope`, `risk_level`,
108
+ `rationale`, `rollback_hint`). From a same-turn relayed `approval_request` (the
109
+ fallback) the rollback field arrives under the key `rollback` -- map it to
110
+ ROLLBACK the same way. Either way you copy values verbatim; you do not re-author
111
+ them.
70
112
 
71
113
  ## Rules
72
114
 
@@ -84,7 +126,12 @@ Fields above are extracted from the DB-stored canonical payload (`payload_json`
84
126
  `APPROVAL_REQUEST` carrying a `command_set` of >= 2 `{command, rationale}`
85
127
  items and **no** `approval_id`, the SubagentStop processor
86
128
  (`handoff_persister._intake_command_set_pending`) mints ONE pending
87
- `COMMAND_SET` with one `approval_id`. You present that single approval: list
129
+ `COMMAND_SET` with one content-derived `approval_id`. Once that pending has
130
+ survived a turn it appears in the injected `[PENDING-APPROVALS-VERIFIED]`
131
+ block with its minted `approval_id` and all N commands -- **read the id and
132
+ commands from the block; do not dispatch `gaia approvals derive-id`.** (A
133
+ command_set emitted in the current turn is presented from the subagent's
134
+ relayed `approval_request`.) You present that single approval: list
88
135
  **all N commands** in the question body, but use **one** Approve label with
89
136
  **one** `[P-{nonce8}]` suffix -- one consent covers the whole batch. On
90
137
  approval, `activate_db_pending_by_prefix` Step 3b creates a single
@@ -120,5 +167,5 @@ wording, see `reference.md` -> "GOOD vs BAD Examples", "Option Label Patterns",
120
167
  | "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. |
121
168
  | "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. |
122
169
  | "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. |
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. |
170
+ | "I can paraphrase a field before relaying" | The fingerprint covers all sealed fields and is checked at grant **activation** (`verify_fingerprint`, when the user selects the Approve label); a paraphrase there raises `ChainTamperError` and the grant never forms. Relay verbatim so activation succeeds. |
171
+ | **"I'll dispatch a subagent to verify or derive the approval before presenting"** | The orchestrator has no shell and must NEVER dispatch to verify or derive an approval. The pending arrives **already verified** in the injected `[PENDING-APPROVALS-VERIFIED]` block (DB-read + fingerprint-checked by the per-turn hook, `verified: true`) -- present from it. For a same-turn pending not yet in the block, present from the subagent's relayed `approval_request`. A verify/derive dispatch is unnecessary (integrity is enforced at activation) and harmful (its SubagentStop can sweep the very pending). For `command_set`, read the minted `approval_id` and all commands from the block -- do not run `gaia approvals derive-id`. |
@@ -151,6 +151,16 @@ commands** in the question body, with **one** Approve label carrying **one**
151
151
  `[P-{nonce8}]` suffix. The user gives one consent; each command then runs on its
152
152
  own retry within the 60-minute window. You do NOT issue N separate approvals.
153
153
 
154
+ **Reading the batch id and commands -- from the block, not by dispatch.** Once
155
+ the minted `COMMAND_SET` pending has survived a turn, it appears in the injected
156
+ `[PENDING-APPROVALS-VERIFIED]` block with its content-derived `approval_id` and
157
+ all N commands attached (`build_verified_pending_approvals` in
158
+ `hooks/modules/session/session_manifest.py`). Read the id and the commands
159
+ straight from that block -- the orchestrator has no shell and must NOT dispatch
160
+ `gaia approvals derive-id` or any verify command. For a command_set emitted in
161
+ the CURRENT turn (not yet in the block), present from the subagent's relayed
162
+ `approval_request`, which carries the same `command_set`.
163
+
154
164
  ## Grant Activation Mechanics
155
165
 
156
166
  When the hook blocks a T3 Bash command in subagent context,
@@ -161,9 +171,12 @@ generates a `P-{uuid4_hex}` `approval_id`, fingerprints the payload, inserts an
161
171
  message ends with `approval_id: P-{...}` (`build_t3_blocked_denial_message` in
162
172
  `hooks/modules/security/approval_messages.py`).
163
173
 
164
- The subagent relays that `approval_id` in its `approval_request`. The
165
- orchestrator presents via AskUserQuestion with the `[P-xxxxxxxx]` label. When
166
- the user selects the Approve label, the **ElicitationResult hook**
174
+ The orchestrator presents via AskUserQuestion with the `[P-xxxxxxxx]` label,
175
+ reading the `approval_id` and fields from the injected
176
+ `[PENDING-APPROVALS-VERIFIED]` block (primary) or, for a same-turn pending not
177
+ yet in the block, from the subagent's relayed `approval_request` (fallback). It
178
+ does not dispatch to verify or derive. When the user selects the Approve label,
179
+ the **ElicitationResult hook**
167
180
  (`hooks/elicitation_result.py`) fires and calls
168
181
  `activate_db_pending_by_prefix()`, which:
169
182
 
@@ -1,8 +1,12 @@
1
1
  # AskUserQuestion Template
2
2
 
3
3
  Use this layout verbatim when presenting an approval to the user. Replace
4
- `{...}` placeholders with values extracted from the subagent's `sealed_payload`
5
- and `approval_request`. Do not paraphrase, summarize, or omit any field.
4
+ `{...}` placeholders with values read from your trusted source -- the injected
5
+ `[PENDING-APPROVALS-VERIFIED]` block (primary; already DB-read and
6
+ fingerprint-verified by the per-turn hook) or, for a same-turn pending not yet
7
+ in the block, the subagent's relayed `approval_request` (fallback). Never
8
+ dispatch a subagent to derive or verify the approval. Do not paraphrase,
9
+ summarize, or omit any field.
6
10
 
7
11
  ## Standard Approval (single command)
8
12
 
@@ -23,19 +27,21 @@ AskUserQuestion(
23
27
  )
24
28
  ```
25
29
 
26
- Where `approval_id_prefix8` is the first 8 characters of the `approval_id`
27
- field from the subagent's `approval_request` (after the `P-` prefix).
30
+ Where `approval_id_prefix8` is the first 8 characters (after the `P-` prefix) of
31
+ the `approval_id` read from the `[PENDING-APPROVALS-VERIFIED]` block, or from the
32
+ subagent's `approval_request` for a same-turn pending.
28
33
 
29
- ## No batch template
34
+ ## Batch template (COMMAND_SET)
30
35
 
31
- There is no batch/multi-use approval in the current code. The `verb_family` grant
32
- was removed (see the module docstring of
33
- `hooks/modules/security/approval_grants.py`) and the `COMMAND_SET` replacement
34
- has no production activation path (`create_command_set_grant` has no production
35
- caller). The word "batch" in a label and a `batch_scope` field are both ignored.
36
- For a sweep of N commands, present each command with its own single-command
37
- approval (the template above), once per `approval_id`. See `reference.md` ->
38
- "On batch intents".
36
+ When the subagent emits a plan-first `APPROVAL_REQUEST` with a `command_set`
37
+ of >= 2 `{command, rationale}` items and **no** `approval_id`, the
38
+ SubagentStop intake mints ONE pending `COMMAND_SET` approval. Present it as
39
+ a single approval: list all N commands in the question body, one Approve
40
+ label with one `[P-{nonce8}]` suffix. See `reference.md` -> "On batch
41
+ intents" for the full layout.
42
+
43
+ A `batch_scope` field and the word "batch" in an option label are both
44
+ ignored -- the signal is the presence of `command_set` in the contract.
39
45
 
40
46
  ## Field Extraction Reference
41
47
 
@@ -46,4 +52,4 @@ approval (the template above), once per `approval_id`. See `reference.md` ->
46
52
  | SCOPE | `sealed_payload.scope` |
47
53
  | RIESGO | `sealed_payload.risk_level` + `sealed_payload.rationale` |
48
54
  | ROLLBACK | `sealed_payload.rollback_hint` (null -> "NOT REVERSIBLE") |
49
- | Option nonce suffix | `approval_request.approval_id` first 8 chars after `P-` |
55
+ | Option nonce suffix | `approval_id` first 8 chars after `P-` (from the `[PENDING-APPROVALS-VERIFIED]` block, or `approval_request.approval_id` for a same-turn pending) |
@@ -37,7 +37,7 @@ report "rejected" when nothing actually changed.
37
37
  | `gaia approvals list` | DB grants + filesystem pendings | `cmd_list` (mixed) |
38
38
  | `gaia approvals reject NONCE` | filesystem only | `reject_pending` in `hooks/modules/security/approval_grants.py` |
39
39
  | `gaia approvals reject-all` | filesystem only | loops `reject_pending` |
40
- | `gaia approvals clean` | filesystem only | `cleanup_expired_grants` |
40
+ | `gaia approvals clean` | DB (cross-session stale pendings) + filesystem | `cmd_clean` in `bin/cli/approvals.py`: calls `store.list_pending(all_sessions=True)`, transitions every pending older than `DEFAULT_PENDING_TTL_MINUTES` (24 h) to `revoked` via `store.revoke()`, then calls `cleanup_expired_grants` for filesystem files |
41
41
 
42
42
  The practical consequence: `revoke` is the DB-aware single-id verb; `reject` and
43
43
  `reject-all` only touch the legacy filesystem queue. If you need to mark a DB
@@ -105,15 +105,19 @@ Offer bulk cleanup when the user says "limpia todos los pendings", "borra los
105
105
  pendientes", or when SessionStart surfaces 5+ orphaned pendings the user has
106
106
  not engaged with.
107
107
 
108
- - `gaia approvals reject-all` -- bulk reject across the **filesystem** queue.
109
- Returns "0 rejected" when the queue is empty.
110
- - `gaia approvals clean` -- removes expired/stale **filesystem** files.
108
+ - `gaia approvals reject-all` -- bulk soft-reject across the **filesystem** queue.
109
+ Returns "0 rejected" when the queue is empty. Does not touch DB rows.
110
+ - `gaia approvals clean` -- the first-class cross-session bulk drain for stale
111
+ DB pendings: `cmd_clean` calls `store.list_pending(all_sessions=True)` and
112
+ transitions every pending older than 24 h (`DEFAULT_PENDING_TTL_MINUTES`) to
113
+ `revoked` via `store.revoke()`, then runs `cleanup_expired_grants` to clean
114
+ expired filesystem grant files. Runs without a T3 prompt (consent-reducing,
115
+ listed in `CONSENT_REDUCING_SUBCOMMAND_EXCEPTIONS`). Use this when
116
+ `gaia approvals pending --all-sessions` shows a backlog of stale rows.
111
117
 
112
- There is no first-class bulk-revoke for the DB queue. If `gaia approvals
113
- pending --all-sessions` shows rows that need clearing, either revoke each by id
114
- or call `store.revoke()` in a short Python loop. Do not report "bulk cleanup
115
- done" after `reject-all` if the DB queue still has pending rows -- check
116
- `gaia approvals pending --all-sessions` to confirm.
118
+ Do not report "bulk cleanup done" after `reject-all` alone -- it only clears
119
+ the filesystem queue. Run `gaia approvals clean` to drain the DB backlog, then
120
+ confirm with `gaia approvals pending --all-sessions`.
117
121
 
118
122
  Do not offer `reject-all` when there are active same-session pendings the user
119
123
  may still want to approve.
@@ -123,8 +127,9 @@ may still want to approve.
123
127
  - Approving without showing the exact COMANDO -- the user consents on the
124
128
  verbatim string, not a summary. The full presentation discipline lives in
125
129
  `orchestrator-present-approval`; this skill does not restate it.
126
- - Treating `gaia approvals reject-all` as a DB cleanup -- it operates on the
127
- filesystem queue only. DB rows survive the call.
130
+ - Treating `gaia approvals reject-all` as a full cleanup -- it operates on the
131
+ filesystem queue only; DB rows survive the call. Use `gaia approvals clean`
132
+ to drain the DB backlog.
128
133
  - Reporting "rechazado" without verifying the store -- `revoke` returns
129
134
  `not_found` for filesystem-only pendings; the inverse happens for `reject` on
130
135
  DB rows. Pick the verb by store, or be ready to fall back.
@@ -44,9 +44,20 @@ Add an `approval_request` to your `agent_contract_handoff`, copying the hook's f
44
44
 
45
45
  The `approval_request` schema is canonical in `agent-approval-protocol` — relay the sealed_payload fields verbatim (the hook built them) and add `verification` (your own success criteria) + `approval_id` (the literal token from the denial). See `agent-approval-protocol/SKILL.md` for the full field list and types.
46
46
 
47
- The `approval_id` is the `P-{...}` token the orchestrator uses to find the
48
- `REQUESTED` row in the DB and validate the fingerprint. Fields written only in
49
- prose are invisible to the presentation -- the user would approve blind.
47
+ The `approval_id` is the `P-{...}` token tying this request to its `REQUESTED`
48
+ row in the DB. Fields written only in prose are invisible to the presentation --
49
+ the user would approve blind.
50
+
51
+ **What your relay is for: same-turn immediacy.** Your `approval_request` is the
52
+ orchestrator's source only for the CURRENT turn. The orchestrator's primary
53
+ source is the per-turn `[PENDING-APPROVALS-VERIFIED]` block injected at
54
+ `UserPromptSubmit`, which carries every pending that has survived >= 1 turn,
55
+ already DB-read and fingerprint-verified. But that block was built before you
56
+ ran this turn, so a pending you mint now is not in it yet -- the orchestrator
57
+ presents it from your relay until the next turn's block picks it up. You emit the
58
+ same fields either way; nothing on your side changes. The orchestrator never
59
+ dispatches a subagent to verify or derive your request -- integrity is enforced
60
+ at grant activation, not at presentation.
50
61
 
51
62
  ## Non-negotiable rules
52
63
 
@@ -99,6 +110,20 @@ with one `approval_id` -- so a batch of N commands is **one consent, N
99
110
  commands**, not N approvals. A set of `<= 1` item is not a batch: it does not
100
111
  mint a COMMAND_SET (use the normal singular block path for a single command).
101
112
 
113
+ You still emit the `command_set` with **no `approval_id`** -- nothing changes on
114
+ your side. What changed underneath: the minted `approval_id` is now
115
+ **content-derived** from the command_set
116
+ (`derive_command_set_id` -> `P-<first 32 hex of sha256(canonical commands)>`),
117
+ not a random uuid4. You do not compute or emit it (you cannot hash reliably, and
118
+ you have nothing to attempt yet); the value is purely internal. The reason it
119
+ matters: the content-derived id is reproducible without a uuid4 that could be
120
+ lost across sessions. Once the minted pending has survived a turn, the
121
+ orchestrator reads it -- with all N commands -- straight from the injected
122
+ `[PENDING-APPROVALS-VERIFIED]` block (no DB search, no derive-dispatch); for the
123
+ turn you mint it in, the orchestrator presents from the `command_set` in your
124
+ relay. Your contract stays the same -- `command_set` of `{command, rationale}`
125
+ items, no `approval_id`.
126
+
102
127
  On the user's approval, that one pending activates into a single `COMMAND_SET`
103
128
  grant (60-minute TTL); each item is then consumed byte-for-byte on its own
104
129
  retry, with replay protection, until the whole set is `CONSUMED`. See
@@ -16,8 +16,12 @@ payload from the intercepted command and calls
16
16
  3. writes the `REQUESTED` event to the DB.
17
17
 
18
18
  The block message you receive (`[T3_BLOCKED] ...`) ends with `approval_id: P-{...}`.
19
- You relay that token plus the operation details; the orchestrator re-derives the
20
- fingerprint from the DB row.
19
+ You relay that token plus the operation details. For the current turn the
20
+ orchestrator presents from your relay; once the pending survives a turn it
21
+ appears in the per-turn `[PENDING-APPROVALS-VERIFIED]` block, already
22
+ fingerprint-verified by the hook. Payload integrity is enforced at grant
23
+ activation (`verify_fingerprint`), so the orchestrator never dispatches to
24
+ verify or derive your request.
21
25
 
22
26
  Source: `bash_validator._build_sealed_payload()`, the subagent block path in
23
27
  `bash_validator._validate_single_command()`; `gaia/approvals/store.py`
@@ -94,6 +98,24 @@ COMMAND_SET is ever minted for one command). The intake runs independently of
94
98
  the audit handoff-row write, so a batch consent is never lost to an unrelated
95
99
  DB failure.
96
100
 
101
+ **The COMMAND_SET `approval_id` is content-derived, not uuid4.** Unlike the
102
+ singular hook-block path (which mints `P-{uuid4hex}`), the intake derives the id
103
+ from the command_set content via `gaia.approvals.store.derive_command_set_id()`:
104
+ `P-<first 32 hex of sha256(canonical(post-filter command strings))>`. It then
105
+ passes that id to `insert_requested(..., approval_id=...)` as the pending row id.
106
+ The point is reproducibility without a fragile uuid4: a uuid4 minted at
107
+ SubagentStop could not be recovered by the parent (Claude Code #5812), but a
108
+ content-derived id needs no recovery -- the same canonicalization
109
+ (`chain.canonical_payload`) and mutative filter always yield the same id. Once
110
+ the minted pending survives a turn, the orchestrator reads that id (and all N
111
+ commands) straight from the injected `[PENDING-APPROVALS-VERIFIED]` block -- no
112
+ DB lookup and no `gaia approvals derive-id` dispatch; for the mint turn it
113
+ presents from the `command_set` in your relay. The id is
114
+ **order-sensitive** (the consume side matches positionally) and **content-only**
115
+ (rationale/session/agent are not folded in, so both sides agree from the command
116
+ list alone). Idempotency follows the existing fingerprint dedup: two identical
117
+ command sets map to one id.
118
+
97
119
  **Envelope shape.** The sealed_payload the intake writes carries a `command_set`
98
120
  key holding the verbatim list of `{command, rationale}` items, and `commands`
99
121
  listing every command string in the set:
@@ -138,13 +160,17 @@ single-use within the 60-minute window.
138
160
  Always `plan_status: "APPROVAL_REQUEST"`. The presence of `approval_id` tells the
139
161
  orchestrator which path:
140
162
 
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.
163
+ - **With `approval_id`** -- the hook blocked a single command; the orchestrator
164
+ presents from your relay (current turn) or the injected
165
+ `[PENDING-APPROVALS-VERIFIED]` block (later turns), and the single-use semantic
166
+ grant activates on user approval (fingerprint checked at activation).
144
167
  - **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.
168
+ batch. The SubagentStop intake processor mints ONE pending `COMMAND_SET` with a
169
+ **content-derived** id (`derive_command_set_id`). The orchestrator reads that
170
+ id and the N commands from the injected `[PENDING-APPROVALS-VERIFIED]` block
171
+ (no derive-dispatch), or, for the mint turn, from the `command_set` in your
172
+ relay, then presents the single approval (N commands, one nonce). See
173
+ "Batch / COMMAND_SET -- wired" above.
148
174
  - **Without `approval_id` and without a multi-item `command_set`** -- plan-first
149
175
  single (you are presenting one T3 plan before attempting); the orchestrator
150
176
  gates on user consent before any execution.
@@ -19,7 +19,12 @@ desde el filesystem hacia `~/.gaia/gaia.db`.
19
19
  | 01 | Episodes | `.claude/project-context/episodic-memory/episodes.jsonl` | `episodes` (+`episodes_fts`) |
20
20
  | 02 | Memory | `~/.claude/projects/-home-jorge-ws-me/memory/*.md` | `memory` (+`memory_fts`) |
21
21
  | 03 | Context contracts | `.claude/project-context/project-context.json` | `context_contracts` |
22
- | 04 | Harness events | `.claude/events/events.jsonl` | `harness_events` |
22
+ | 04 | Harness events | ~~`.claude/events/events.jsonl`~~ (ELIMINADO) | `harness_events` |
23
+
24
+ > **Dominio 04 completado y eliminado.** `events.jsonl` y su archivo `.lock` fueron
25
+ > retirados. El hook `event_writer` escribe directamente a `harness_events` en la DB.
26
+ > El script `migrate_04_harness_events.py` y su wrapper `.sh` fueron borrados una vez
27
+ > completada la absorción. Los datos vivos se leen desde `harness_events` en `~/.gaia/gaia.db`.
23
28
 
24
29
  Cada dominio tiene 2 archivos:
25
30
 
@@ -37,8 +42,8 @@ bootstrap.sh # crea/inicializa ~/.gaia/gaia.db con s
37
42
  ./migrate_01_episodes.sh # ~50-80 MB de SQL, batch 80
38
43
  ./migrate_02_memory.sh # 28 .md (MEMORY.md excluido)
39
44
  ./migrate_03_context_contracts.sh # 12 secciones
40
- ./migrate_04_harness_events.sh # ~5-10 MB de SQL, batch 200
41
- ./validate.sh # 5 aserciones read-only
45
+ # migrate_04_harness_events.sh ELIMINADO dominio 04 completado; eventos en DB-canonical
46
+ ./validate.sh # aserciones read-only (V4 eliminada junto con 04)
42
47
  ```
43
48
 
44
49
  Cada script imprime `[migrate_NN] OK` al terminar.
@@ -50,14 +55,7 @@ Cada script imprime `[migrate_NN] OK` al terminar.
50
55
  | 01 episodes | `INSERT OR IGNORE` (PK = `episode_id`) | sí |
51
56
  | 02 memory | `INSERT OR IGNORE` (PK = `(project, name)`) | sí |
52
57
  | 03 context_contracts | `INSERT OR IGNORE` (PK = `(project, section_name)`) | sí |
53
- | 04 harness_events | `INSERT` simple (sin PK natural) | **no duplica filas** |
54
-
55
- Para re-ejecutar 04 limpiamente:
56
-
57
- ```
58
- sqlite3 ~/.gaia/gaia.db "DELETE FROM harness_events WHERE project='me';"
59
- ./migrate_04_harness_events.sh
60
- ```
58
+ | 04 harness_events | N/A tool eliminado; escritura vía `event_writer` DB-direct | N/A |
61
59
 
62
60
  ## Validación
63
61
 
@@ -68,7 +66,7 @@ sqlite3 ~/.gaia/gaia.db "DELETE FROM harness_events WHERE project='me';"
68
66
  | V1 | `COUNT(*) FROM episodes` == líneas no vacías de `episodes.jsonl` |
69
67
  | V2 | `COUNT(*) FROM memory` == archivos `.md` (excluyendo `MEMORY.md`) |
70
68
  | V3 | `COUNT(*) FROM context_contracts` == 12 |
71
- | V4 | `COUNT(*) FROM harness_events` == líneas no vacías de `events.jsonl` |
69
+ | ~~V4~~ | ~~`COUNT(*) FROM harness_events` == líneas no vacías de `events.jsonl`~~ — eliminado junto con el dominio 04 |
72
70
  | V5 | `COUNT(*) FROM episodes_fts` == `COUNT(*) FROM episodes` (FTS sync) |
73
71
 
74
72
  Exit code: 0 si todas pasan, 1 si alguna falla.