@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +13 -0
- package/bin/README.md +6 -1
- package/bin/cli/approvals.py +486 -474
- package/bin/cli/brief.py +13 -0
- package/bin/cli/doctor.py +1 -1
- package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-ops/hooks/adapters/claude_code.py +92 -86
- package/dist/gaia-ops/hooks/modules/agents/handoff_persister.py +13 -2
- package/dist/gaia-ops/hooks/modules/context/context_injector.py +23 -7
- package/dist/gaia-ops/hooks/modules/events/event_writer.py +63 -96
- package/dist/gaia-ops/hooks/modules/security/__init__.py +0 -2
- package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +238 -69
- package/dist/gaia-ops/hooks/modules/security/approval_grants.py +506 -1103
- package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +24 -1
- package/dist/gaia-ops/hooks/modules/session/pending_scanner.py +150 -90
- package/dist/gaia-ops/hooks/modules/session/session_manifest.py +257 -28
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +19 -0
- package/dist/gaia-ops/hooks/post_compact.py +1 -0
- package/dist/gaia-ops/hooks/pre_compact.py +1 -0
- package/dist/gaia-ops/hooks/user_prompt_submit.py +20 -0
- package/dist/gaia-ops/skills/agent-approval-protocol/SKILL.md +50 -14
- package/dist/gaia-ops/skills/agent-approval-protocol/reference.md +16 -9
- package/dist/gaia-ops/skills/agent-protocol/examples.md +12 -1
- package/dist/gaia-ops/skills/gaia-patterns/reference.md +2 -2
- package/dist/gaia-ops/skills/orchestrator-present-approval/SKILL.md +69 -22
- package/dist/gaia-ops/skills/orchestrator-present-approval/reference.md +16 -3
- package/dist/gaia-ops/skills/orchestrator-present-approval/template.md +20 -14
- package/dist/gaia-ops/skills/pending-approvals/SKILL.md +16 -11
- package/dist/gaia-ops/skills/subagent-request-approval/SKILL.md +28 -3
- package/dist/gaia-ops/skills/subagent-request-approval/reference.md +34 -8
- package/dist/gaia-ops/tools/migration/README.md +10 -12
- package/dist/gaia-ops/tools/scan/orchestrator.py +194 -10
- package/dist/gaia-ops/tools/scan/tests/test_integration.py +1 -2
- package/dist/gaia-security/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-security/hooks/adapters/claude_code.py +92 -86
- package/dist/gaia-security/hooks/modules/agents/handoff_persister.py +13 -2
- package/dist/gaia-security/hooks/modules/context/context_injector.py +23 -7
- package/dist/gaia-security/hooks/modules/events/event_writer.py +63 -96
- package/dist/gaia-security/hooks/modules/security/__init__.py +0 -2
- package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +238 -69
- package/dist/gaia-security/hooks/modules/security/approval_grants.py +506 -1103
- package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +24 -1
- package/dist/gaia-security/hooks/modules/session/pending_scanner.py +150 -90
- package/dist/gaia-security/hooks/modules/session/session_manifest.py +257 -28
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +19 -0
- package/dist/gaia-security/hooks/user_prompt_submit.py +20 -0
- package/gaia/approvals/__init__.py +2 -1
- package/gaia/approvals/store.py +165 -15
- package/gaia/store/schema.sql +38 -1
- package/gaia/store/writer.py +400 -0
- package/hooks/adapters/claude_code.py +92 -86
- package/hooks/elicitation_result.py +20 -75
- package/hooks/modules/agents/handoff_persister.py +13 -2
- package/hooks/modules/context/context_injector.py +23 -7
- package/hooks/modules/events/event_writer.py +63 -96
- package/hooks/modules/security/__init__.py +0 -2
- package/hooks/modules/security/approval_cleanup.py +238 -69
- package/hooks/modules/security/approval_grants.py +506 -1103
- package/hooks/modules/security/mutative_verbs.py +24 -1
- package/hooks/modules/session/pending_scanner.py +150 -90
- package/hooks/modules/session/session_manifest.py +257 -28
- package/hooks/modules/tools/bash_validator.py +19 -0
- package/hooks/post_compact.py +1 -0
- package/hooks/pre_compact.py +1 -0
- package/hooks/user_prompt_submit.py +20 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/bootstrap_database.sh +66 -17
- package/scripts/migrations/README.md +26 -14
- package/scripts/migrations/schema.checksum +2 -2
- package/scripts/migrations/v18_to_v19.sql +36 -0
- package/scripts/migrations/v19_to_v20.sql +20 -0
- package/skills/agent-approval-protocol/SKILL.md +50 -14
- package/skills/agent-approval-protocol/reference.md +16 -9
- package/skills/agent-protocol/examples.md +12 -1
- package/skills/gaia-patterns/reference.md +2 -2
- package/skills/orchestrator-present-approval/SKILL.md +69 -22
- package/skills/orchestrator-present-approval/reference.md +16 -3
- package/skills/orchestrator-present-approval/template.md +20 -14
- package/skills/pending-approvals/SKILL.md +16 -11
- package/skills/subagent-request-approval/SKILL.md +28 -3
- package/skills/subagent-request-approval/reference.md +34 -8
- package/tools/migration/README.md +10 -12
- package/tools/scan/orchestrator.py +194 -10
- package/tools/scan/tests/test_integration.py +1 -2
- package/bin/cli/plans.py +0 -517
- package/dist/gaia-ops/tools/context/deep_merge.py +0 -159
- package/dist/gaia-ops/tools/migration/migrate_04_harness_events.py +0 -132
- package/dist/gaia-ops/tools/migration/migrate_04_harness_events.sh +0 -23
- package/dist/gaia-ops/tools/scan/merge.py +0 -213
- package/dist/gaia-ops/tools/scan/tests/test_merge.py +0 -269
- package/gaia/approvals/revert.py +0 -282
- package/tools/context/deep_merge.py +0 -159
- package/tools/migration/migrate_04_harness_events.py +0 -132
- package/tools/migration/migrate_04_harness_events.sh +0 -23
- package/tools/scan/merge.py +0 -213
- package/tools/scan/tests/test_merge.py +0 -269
|
@@ -90,6 +90,11 @@ class BashValidationResult:
|
|
|
90
90
|
# plain error string (exit 2). Used for structured block responses that
|
|
91
91
|
# should correct the agent rather than terminate execution.
|
|
92
92
|
block_response: Optional[Dict[str, Any]] = None
|
|
93
|
+
# When a T3 command is allowed because it matched (and consumed) an active
|
|
94
|
+
# grant, this carries the approval_id of that grant. The adapter stashes it
|
|
95
|
+
# in HookState so PostToolUse can append an EXECUTED/FAILED event to the
|
|
96
|
+
# approval_events chain for this approval. None for non-T3 / no-grant paths.
|
|
97
|
+
consumed_approval_id: Optional[str] = None
|
|
93
98
|
|
|
94
99
|
def __post_init__(self):
|
|
95
100
|
if self.suggestions is None:
|
|
@@ -667,6 +672,7 @@ class BashValidator:
|
|
|
667
672
|
allowed=True,
|
|
668
673
|
tier=SecurityTier.T3_BLOCKED,
|
|
669
674
|
reason="Command-set grant matched",
|
|
675
|
+
consumed_approval_id=cs_approval_id,
|
|
670
676
|
)
|
|
671
677
|
|
|
672
678
|
# DB-primary + filesystem-fallback grant check.
|
|
@@ -720,6 +726,7 @@ class BashValidator:
|
|
|
720
726
|
allowed=True,
|
|
721
727
|
tier=SecurityTier.T3_BLOCKED,
|
|
722
728
|
reason="Grant confirmed",
|
|
729
|
+
consumed_approval_id=db_approval_id,
|
|
723
730
|
)
|
|
724
731
|
else:
|
|
725
732
|
# Filesystem grant exists, not yet confirmed -- GAIA approved,
|
|
@@ -733,6 +740,7 @@ class BashValidator:
|
|
|
733
740
|
allowed=True,
|
|
734
741
|
tier=SecurityTier.T3_BLOCKED,
|
|
735
742
|
reason="Grant active, pending confirmation",
|
|
743
|
+
consumed_approval_id=db_approval_id,
|
|
736
744
|
)
|
|
737
745
|
else:
|
|
738
746
|
# Converge on the single T3 decision point. When there is an
|
|
@@ -808,6 +816,7 @@ class BashValidator:
|
|
|
808
816
|
allowed=True,
|
|
809
817
|
tier=SecurityTier.T3_BLOCKED,
|
|
810
818
|
reason="Command-set grant matched",
|
|
819
|
+
consumed_approval_id=cs_approval_id,
|
|
811
820
|
)
|
|
812
821
|
|
|
813
822
|
grant = check_approval_grant(command, session_id=session_id)
|
|
@@ -859,6 +868,7 @@ class BashValidator:
|
|
|
859
868
|
allowed=True,
|
|
860
869
|
tier=SecurityTier.T3_BLOCKED,
|
|
861
870
|
reason="Grant confirmed",
|
|
871
|
+
consumed_approval_id=db_approval_id,
|
|
862
872
|
)
|
|
863
873
|
else:
|
|
864
874
|
logger.info(
|
|
@@ -870,6 +880,7 @@ class BashValidator:
|
|
|
870
880
|
allowed=True,
|
|
871
881
|
tier=SecurityTier.T3_BLOCKED,
|
|
872
882
|
reason="Grant active, pending confirmation",
|
|
883
|
+
consumed_approval_id=db_approval_id,
|
|
873
884
|
)
|
|
874
885
|
|
|
875
886
|
# No grant matched -- converge on the single T3 decision
|
|
@@ -939,10 +950,18 @@ class BashValidator:
|
|
|
939
950
|
key=lambda t: tier_order.index(t.value),
|
|
940
951
|
)
|
|
941
952
|
|
|
953
|
+
# Propagate the consumed approval_id from whichever component matched a
|
|
954
|
+
# grant, so PostToolUse can append EXECUTED/FAILED for that approval.
|
|
955
|
+
consumed_approval_id = next(
|
|
956
|
+
(r.consumed_approval_id for r in component_results if r.consumed_approval_id),
|
|
957
|
+
None,
|
|
958
|
+
)
|
|
959
|
+
|
|
942
960
|
return BashValidationResult(
|
|
943
961
|
allowed=True,
|
|
944
962
|
tier=highest_tier,
|
|
945
963
|
reason=f"All {len(components)} components validated",
|
|
964
|
+
consumed_approval_id=consumed_approval_id,
|
|
946
965
|
)
|
|
947
966
|
|
|
948
967
|
def _phase4_check_composition(
|
package/hooks/post_compact.py
CHANGED
package/hooks/pre_compact.py
CHANGED
|
@@ -194,6 +194,26 @@ if __name__ == "__main__":
|
|
|
194
194
|
else:
|
|
195
195
|
logger.info("Could not extract user prompt from stdin, skipping routing")
|
|
196
196
|
|
|
197
|
+
# Per-turn VERIFIED pending approvals. Lets the orchestrator present
|
|
198
|
+
# a pending approval for consent directly from injected context,
|
|
199
|
+
# WITHOUT dispatching a subagent to derive/verify it (that dispatch's
|
|
200
|
+
# SubagentStop caused a pending-revocation bug). Emits "" when there
|
|
201
|
+
# are no verified pendings, so a turn with nothing pending injects
|
|
202
|
+
# nothing -- this is what keeps the per-turn injection quiet, unlike
|
|
203
|
+
# the one-shot SessionStart summary it deliberately does not re-emit.
|
|
204
|
+
try:
|
|
205
|
+
from modules.session.session_manifest import (
|
|
206
|
+
build_per_turn_pending_approvals_block,
|
|
207
|
+
)
|
|
208
|
+
pending_block = build_per_turn_pending_approvals_block()
|
|
209
|
+
if pending_block:
|
|
210
|
+
context_parts.append(pending_block)
|
|
211
|
+
except Exception as _pa_exc:
|
|
212
|
+
logger.debug(
|
|
213
|
+
"per-turn pending approvals injection failed (non-fatal): %s",
|
|
214
|
+
_pa_exc,
|
|
215
|
+
)
|
|
216
|
+
|
|
197
217
|
additional_context = "\n\n".join(context_parts)
|
|
198
218
|
logger.info("Context injected: %s mode (%d chars)", mode, len(additional_context))
|
|
199
219
|
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -211,9 +211,13 @@ fi
|
|
|
211
211
|
# EXPECTED_SCHEMA_VERSION en doctor.py en el mismo commit.
|
|
212
212
|
# - Para una DB en el FLOOR, esa migración corre directo (la DB está en el
|
|
213
213
|
# estado source de la migración). No se necesitan variantes _fresh: un
|
|
214
|
-
# fresh install
|
|
215
|
-
#
|
|
216
|
-
#
|
|
214
|
+
# fresh install sella el ledger en el FLOOR (Section 3b) y, cuando hay
|
|
215
|
+
# migraciones forward (EXPECTED > FLOOR), ESTE loop SÍ se replaya en cada
|
|
216
|
+
# fresh install desde FLOOR+1 hasta EXPECTED contra una DB cuyos objetos
|
|
217
|
+
# schema.sql ya creó -- por eso las migraciones DEBEN ser idempotentes
|
|
218
|
+
# (CREATE ... IF NOT EXISTS; ADD COLUMN neutralizado por el guard runner).
|
|
219
|
+
# El guard-probe por-versión del modelo viejo desaparece junto con la
|
|
220
|
+
# cadena histórica.
|
|
217
221
|
#
|
|
218
222
|
# Cada migración corre en su propia transacción BEGIN/COMMIT. Si falla, abort
|
|
219
223
|
# -- el ledger NO avanza y el próximo bootstrap retry ve la misma pendiente.
|
|
@@ -245,13 +249,58 @@ echo "[bootstrap] schema_version: current=${CURRENT_VERSION}, expected=${EXPECTE
|
|
|
245
249
|
|
|
246
250
|
MIG_DIR="${SCRIPT_DIR}/migrations"
|
|
247
251
|
|
|
252
|
+
# --- Idempotent ADD COLUMN guard (runner-level) ------------------------------
|
|
253
|
+
#
|
|
254
|
+
# Forward migrations are applied on EVERY fresh install: schema.sql produces the
|
|
255
|
+
# EXPECTED shape, the ledger is stamped at the FLOOR, and Section 3c walks
|
|
256
|
+
# FLOOR+1..EXPECTED (this is what test_fresh_install_stamps_floor and
|
|
257
|
+
# test_bootstrap_idempotent_at_floor require). Because schema.sql already
|
|
258
|
+
# carries each migration's target DDL (see migrations/README.md section 1:
|
|
259
|
+
# "add the DDL to schema.sql AND create the migration"), a migration is always
|
|
260
|
+
# replayed against a DB that already has its objects.
|
|
261
|
+
#
|
|
262
|
+
# CREATE ... IF NOT EXISTS makes CREATE statements idempotent under that replay
|
|
263
|
+
# (v18_to_v19 relies on it). But SQLite has NO `ADD COLUMN IF NOT EXISTS`, so a
|
|
264
|
+
# bare `ALTER TABLE t ADD COLUMN c` aborts with "duplicate column name" when the
|
|
265
|
+
# column already exists from schema.sql. This guard restores idempotency for
|
|
266
|
+
# ADD COLUMN at the RUNNER level (not by putting invalid SQL in the .sql file):
|
|
267
|
+
# for each `ALTER TABLE <t> ADD COLUMN <c> ...` line, if column <c> already
|
|
268
|
+
# exists on table <t> (PRAGMA table_info), the line is neutralised (commented
|
|
269
|
+
# out) before the migration runs. Every other statement passes through verbatim.
|
|
270
|
+
#
|
|
271
|
+
# Pure bash + sqlite3, no python3 -- consistent with this script's principles.
|
|
272
|
+
_filter_add_column_idempotent() {
|
|
273
|
+
# $1 = path to the migration .sql file. Emits the (possibly filtered) SQL on
|
|
274
|
+
# stdout. Lines that are `ALTER TABLE t ADD COLUMN c` for an existing column
|
|
275
|
+
# are replaced by a comment; all other lines are passed through unchanged.
|
|
276
|
+
local mig_file="$1"
|
|
277
|
+
local line lower table col exists
|
|
278
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
279
|
+
# Normalise whitespace for matching only (emit the ORIGINAL line).
|
|
280
|
+
lower="$(printf '%s' "$line" | tr '[:upper:]' '[:lower:]')"
|
|
281
|
+
if [[ "$lower" =~ alter[[:space:]]+table[[:space:]]+([a-z0-9_]+)[[:space:]]+add[[:space:]]+column[[:space:]]+([a-z0-9_]+) ]]; then
|
|
282
|
+
table="${BASH_REMATCH[1]}"
|
|
283
|
+
col="${BASH_REMATCH[2]}"
|
|
284
|
+
exists="$(sqlite3 "$GAIA_DB" "SELECT COUNT(*) FROM pragma_table_info('${table}') WHERE name='${col}';")"
|
|
285
|
+
if [ "$exists" -gt 0 ]; then
|
|
286
|
+
printf -- '-- [bootstrap] skipped (column %s.%s already present): %s\n' "$table" "$col" "$line"
|
|
287
|
+
continue
|
|
288
|
+
fi
|
|
289
|
+
fi
|
|
290
|
+
printf '%s\n' "$line"
|
|
291
|
+
done < "$mig_file"
|
|
292
|
+
}
|
|
293
|
+
|
|
248
294
|
if [ "$CURRENT_VERSION" -lt "$EXPECTED_VERSION" ]; then
|
|
249
|
-
# Forward-only loop.
|
|
250
|
-
#
|
|
251
|
-
#
|
|
252
|
-
# FLOOR
|
|
253
|
-
#
|
|
254
|
-
#
|
|
295
|
+
# Forward-only loop. Runs whenever the live DB is behind EXPECTED, which
|
|
296
|
+
# INCLUDES a fresh install: Section 3b stamps the ledger at the FLOOR, and
|
|
297
|
+
# when forward migrations exist (EXPECTED > FLOOR) a fresh DB sits at the
|
|
298
|
+
# FLOOR while EXPECTED is higher, so it enters here and replays FLOOR+1..
|
|
299
|
+
# EXPECTED. That replay runs against a DB whose objects schema.sql already
|
|
300
|
+
# created, which is exactly why these migrations MUST be idempotent (CREATE
|
|
301
|
+
# ... IF NOT EXISTS; ADD COLUMN neutralised by the runner guard above).
|
|
302
|
+
# Any DB below the FLOOR was already rejected in Section 3b, so
|
|
303
|
+
# CURRENT_VERSION here is always >= FLOOR.
|
|
255
304
|
for N in $(seq $((CURRENT_VERSION + 1)) "$EXPECTED_VERSION"); do
|
|
256
305
|
PREV=$((N - 1))
|
|
257
306
|
MIG_FILE="${MIG_DIR}/v${PREV}_to_v${N}.sql"
|
|
@@ -263,15 +312,15 @@ if [ "$CURRENT_VERSION" -lt "$EXPECTED_VERSION" ]; then
|
|
|
263
312
|
exit 1
|
|
264
313
|
fi
|
|
265
314
|
|
|
266
|
-
#
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
#
|
|
270
|
-
#
|
|
271
|
-
#
|
|
272
|
-
#
|
|
315
|
+
# Apply the migration inside an explicit transaction. The SQL is passed
|
|
316
|
+
# through _filter_add_column_idempotent first so that `ADD COLUMN`
|
|
317
|
+
# statements for columns schema.sql already created are skipped (SQLite
|
|
318
|
+
# lacks `ADD COLUMN IF NOT EXISTS`). CREATE ... IF NOT EXISTS statements
|
|
319
|
+
# are already idempotent and pass through unchanged. This is what lets a
|
|
320
|
+
# fresh install (where schema.sql produced the EXPECTED shape) replay the
|
|
321
|
+
# FLOOR+1..EXPECTED migrations without aborting on duplicate columns.
|
|
273
322
|
echo "[bootstrap] migration v${PREV}->v${N}: applying ${MIG_FILE}"
|
|
274
|
-
MIG_SQL="$(
|
|
323
|
+
MIG_SQL="$(_filter_add_column_idempotent "$MIG_FILE")"
|
|
275
324
|
if ! sqlite3 "$GAIA_DB" <<EOF
|
|
276
325
|
BEGIN;
|
|
277
326
|
${MIG_SQL}
|
|
@@ -17,25 +17,31 @@ The floor is **v18**. It is declared in three places that must agree:
|
|
|
17
17
|
|
|
18
18
|
| Location | What it holds |
|
|
19
19
|
|----------|---------------|
|
|
20
|
-
| `gaia/store/schema.sql` | Produces the
|
|
21
|
-
| `scripts/bootstrap_database.sh` Section 3b (`SCHEMA_FLOOR=18`) |
|
|
22
|
-
| `bin/cli/doctor.py` (`EXPECTED_SCHEMA_VERSION`) | The version the CLI expects; equals the floor
|
|
20
|
+
| `gaia/store/schema.sql` | Produces the **latest** (EXPECTED) shape directly -- fresh installs land here, not at the floor. |
|
|
21
|
+
| `scripts/bootstrap_database.sh` Section 3b (`SCHEMA_FLOOR=18`) | Stamps the fresh ledger at the floor; rejects DBs below it. |
|
|
22
|
+
| `bin/cli/doctor.py` (`EXPECTED_SCHEMA_VERSION`) | The version the CLI expects; equals the floor when no forward migration exists, and the highest migration target once they do. |
|
|
23
23
|
|
|
24
24
|
How bootstrap treats each case:
|
|
25
25
|
|
|
26
26
|
* **Fresh install** (no `schema_version` rows): `schema.sql` already produced
|
|
27
|
-
the
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
the EXPECTED shape, and Section 3b stamps the ledger at the **floor** (not
|
|
28
|
+
EXPECTED). Section 3c then replays every forward migration from `floor+1` to
|
|
29
|
+
EXPECTED against that already-current DB. It does **not** seed v1 and walk the
|
|
30
|
+
historical chain. Because the migrations run against objects `schema.sql`
|
|
31
|
+
already created, they **must be idempotent** (see section 1).
|
|
32
|
+
* **DB at or above the floor** (the common case, e.g. `~/.gaia/gaia.db`):
|
|
33
|
+
Section 3c applies any forward migrations the DB is still behind on, up to
|
|
34
|
+
EXPECTED.
|
|
31
35
|
* **DB below the floor** (`1 <= version < 18`): **no longer supported** for
|
|
32
36
|
in-place upgrade. Bootstrap aborts with a clear message asking you to
|
|
33
37
|
recreate the DB (back up, delete `~/.gaia/gaia.db`, re-run `gaia install`).
|
|
34
38
|
|
|
35
39
|
There are no `_fresh` / `_merge` variants under the floor model. Those existed
|
|
36
40
|
only because the old baseline was v1 and the whole chain was walked on every
|
|
37
|
-
fresh install.
|
|
38
|
-
|
|
41
|
+
fresh install. Under the floor model the forward-migration loop is still
|
|
42
|
+
replayed on every fresh install (from `floor+1` to EXPECTED) -- so the single
|
|
43
|
+
forward migration file per bump must be idempotent rather than split into
|
|
44
|
+
`_fresh` / `_merge` variants.
|
|
39
45
|
|
|
40
46
|
---
|
|
41
47
|
|
|
@@ -48,14 +54,19 @@ version) to `N`:
|
|
|
48
54
|
1. Add the new DDL to `gaia/store/schema.sql` so fresh installs land in the
|
|
49
55
|
target shape.
|
|
50
56
|
2. Create exactly one `scripts/migrations/v{N-1}_to_v{N}.sql` containing the
|
|
51
|
-
full DDL delta applied to a DB at version `N-1`.
|
|
57
|
+
full DDL delta applied to a DB at version `N-1`. It **must be idempotent**
|
|
58
|
+
(`CREATE ... IF NOT EXISTS`; for `ADD COLUMN`, rely on the runner's
|
|
59
|
+
existence guard -- SQLite has no `ADD COLUMN IF NOT EXISTS`), because it is
|
|
60
|
+
replayed on fresh installs against a DB that already has those objects.
|
|
52
61
|
3. Bump `EXPECTED_SCHEMA_VERSION` to `N` in `bin/cli/doctor.py` **in the same
|
|
53
62
|
commit**.
|
|
54
63
|
|
|
55
64
|
`bootstrap_database.sh` Section 3c then applies `v{N-1}_to_v{N}.sql` inside a
|
|
56
65
|
single `BEGIN/COMMIT` transaction for any DB behind `N`, and stamps the ledger
|
|
57
|
-
only on success. A fresh install
|
|
58
|
-
|
|
66
|
+
only on success. A fresh install stamps the ledger at the floor (Section 3b)
|
|
67
|
+
and then replays `floor+1 .. N` here too -- since `schema.sql` already produced
|
|
68
|
+
the `N` shape, the migration runs against objects that already exist, which is
|
|
69
|
+
exactly why it must be idempotent (no `_fresh` variant is used).
|
|
59
70
|
|
|
60
71
|
`tests/cli/test_schema_version_lockstep.py` enforces that
|
|
61
72
|
`EXPECTED_SCHEMA_VERSION` equals the floor when no forward migrations exist,
|
|
@@ -93,8 +104,9 @@ as the ledger grows.
|
|
|
93
104
|
| `vN_to_vN+1.sql` | Applied to an existing DB at version N. Contains the full DDL delta, applied inside a `BEGIN/COMMIT` transaction by bootstrap Section 3c. |
|
|
94
105
|
|
|
95
106
|
The historical `_fresh` and `_merge` variants are no longer used: under the
|
|
96
|
-
floor model a
|
|
97
|
-
|
|
107
|
+
floor model a single idempotent `vN_to_vN+1.sql` covers both an in-place
|
|
108
|
+
upgrade and the fresh-install replay (Section 3c walks `floor+1 .. EXPECTED`
|
|
109
|
+
on every fresh install), so one idempotent file replaces the old split.
|
|
98
110
|
|
|
99
111
|
---
|
|
100
112
|
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
# corresponds to (EXPECTED_SCHEMA_VERSION in bin/cli/doctor.py).
|
|
5
5
|
# Do NOT edit by hand: bump EXPECTED_SCHEMA_VERSION + add a migration,
|
|
6
6
|
# then re-run the guard to refresh this file.
|
|
7
|
-
version=
|
|
8
|
-
sha256=
|
|
7
|
+
version=20
|
|
8
|
+
sha256=027c8a61e8217b40cbdb07d0b83e7fb18f1ec671d3069dc0a0ce6bb4cc0e8ee9
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- Migration v18 -> v19: audit-immutability gap closure (Task B).
|
|
2
|
+
--
|
|
3
|
+
-- Adds BEFORE UPDATE trigger bu_approvals_status_has_event on the approvals
|
|
4
|
+
-- table to enforce that every approvals.status transition is accompanied by a
|
|
5
|
+
-- preceding event row in the append-only approval_events chain.
|
|
6
|
+
--
|
|
7
|
+
-- The trigger fires when status changes to 'approved', 'rejected', or 'revoked'.
|
|
8
|
+
-- It checks that an event row with the matching event_type exists for the
|
|
9
|
+
-- approval_id within the same transaction. If no matching event is found it
|
|
10
|
+
-- raises ABORT, rolling back the UPDATE.
|
|
11
|
+
--
|
|
12
|
+
-- This closes the gap where a direct UPDATE approvals SET status = 'approved'
|
|
13
|
+
-- could flip the status column without leaving an auditable event, violating
|
|
14
|
+
-- the "auditable + immutable" invariant of the approval_events chain.
|
|
15
|
+
--
|
|
16
|
+
-- Bootstrap note: a fresh install (schema.sql) already includes this trigger
|
|
17
|
+
-- via CREATE TRIGGER IF NOT EXISTS; this migration only adds it to existing DBs
|
|
18
|
+
-- that were initialized before v19.
|
|
19
|
+
|
|
20
|
+
CREATE TRIGGER IF NOT EXISTS bu_approvals_status_has_event
|
|
21
|
+
BEFORE UPDATE OF status ON approvals
|
|
22
|
+
WHEN NEW.status != OLD.status AND NEW.status IN ('approved', 'rejected', 'revoked')
|
|
23
|
+
BEGIN
|
|
24
|
+
SELECT CASE
|
|
25
|
+
WHEN (
|
|
26
|
+
SELECT COUNT(*) FROM approval_events
|
|
27
|
+
WHERE approval_id = NEW.id
|
|
28
|
+
AND event_type = CASE NEW.status
|
|
29
|
+
WHEN 'approved' THEN 'APPROVED'
|
|
30
|
+
WHEN 'rejected' THEN 'REJECTED'
|
|
31
|
+
WHEN 'revoked' THEN 'REVOKED'
|
|
32
|
+
END
|
|
33
|
+
) = 0
|
|
34
|
+
THEN RAISE(ABORT, 'approvals: status change requires a preceding event in approval_events')
|
|
35
|
+
END;
|
|
36
|
+
END;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- Migration v19 -> v20: add multi_use and confirmed columns to approval_grants.
|
|
2
|
+
--
|
|
3
|
+
-- These two columns support the upcoming FS-grant-plane migration:
|
|
4
|
+
-- multi_use INTEGER NOT NULL DEFAULT 0 -- 1 = multi-use grant, 0 = single-use (BOOLEAN)
|
|
5
|
+
-- confirmed INTEGER NOT NULL DEFAULT 0 -- 1 = grant confirmed by user, 0 = pending (BOOLEAN)
|
|
6
|
+
--
|
|
7
|
+
-- Both columns use the established boolean-as-INTEGER convention (DEFAULT 0)
|
|
8
|
+
-- already in use across this schema (e.g. allow_write, can_read, can_write).
|
|
9
|
+
--
|
|
10
|
+
-- SQLite ALTER TABLE ADD COLUMN is safe and additive: existing rows receive the
|
|
11
|
+
-- DEFAULT value and the table is NOT rebuilt. Zero data loss is guaranteed by
|
|
12
|
+
-- the SQLite specification (https://www.sqlite.org/lang_altertable.html).
|
|
13
|
+
--
|
|
14
|
+
-- Bootstrap note: migrations run once via the version chain. A fresh install
|
|
15
|
+
-- seeds from schema.sql (which already includes both columns) and then skips
|
|
16
|
+
-- this migration file by version-gating; this file runs only against existing
|
|
17
|
+
-- DBs initialized before v20 (which do not yet have these columns).
|
|
18
|
+
|
|
19
|
+
ALTER TABLE approval_grants ADD COLUMN multi_use INTEGER NOT NULL DEFAULT 0;
|
|
20
|
+
ALTER TABLE approval_grants ADD COLUMN confirmed INTEGER NOT NULL DEFAULT 0;
|
|
@@ -14,6 +14,20 @@ through the hook layer, to the orchestrator when a T3 command is blocked: the
|
|
|
14
14
|
the status and event vocabularies, and how to confirm a grant is active. The
|
|
15
15
|
tables below are the canonical schema -- relay them verbatim, do not author them.
|
|
16
16
|
|
|
17
|
+
The orchestrator presents this contract to the user from a **trusted source**,
|
|
18
|
+
never by dispatching a subagent to verify or derive it (it has no shell). The
|
|
19
|
+
primary source is the per-turn `[PENDING-APPROVALS-VERIFIED]` block injected at
|
|
20
|
+
`UserPromptSubmit` (`build_verified_pending_approvals` in
|
|
21
|
+
`hooks/modules/session/session_manifest.py`), which carries every pending that
|
|
22
|
+
has survived >= 1 turn, each already DB-read and fingerprint-verified
|
|
23
|
+
(`verified: true`). For a pending emitted in the current turn -- not yet in the
|
|
24
|
+
block -- the fallback is the subagent's relayed `approval_request`. The
|
|
25
|
+
**integrity boundary is grant activation**, not presentation:
|
|
26
|
+
`verify_fingerprint` (`gaia/approvals/chain.py`) runs when the user selects the
|
|
27
|
+
Approve label, so a tampered payload fails to form a grant regardless of how it
|
|
28
|
+
was presented. See `Skill('orchestrator-present-approval')` for the presentation
|
|
29
|
+
discipline.
|
|
30
|
+
|
|
17
31
|
For the universal response envelope (`plan_status` states, `evidence_report`),
|
|
18
32
|
see `agent-protocol`. For the deep mechanics -- fingerprint canonicalization,
|
|
19
33
|
the hash chain, grant activation, reading a granted approval from Python -- see
|
|
@@ -21,10 +35,20 @@ the hash chain, grant activation, reading a granted approval from Python -- see
|
|
|
21
35
|
|
|
22
36
|
## approval_id format
|
|
23
37
|
|
|
38
|
+
For a **singular** T3 approval (the hook-block path),
|
|
24
39
|
`store._generate_approval_id()` returns `P-{uuid4().hex}` (e.g.
|
|
25
|
-
`P-b1bdfbb0b9474bf5b3f86b1f6a213f7a`)
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
`P-b1bdfbb0b9474bf5b3f86b1f6a213f7a`) -- a random, unique id the subagent relays
|
|
41
|
+
verbatim. For a **plan-first `COMMAND_SET`** the id is instead **content-derived**
|
|
42
|
+
by `store.derive_command_set_id()`: `P-<first 32 hex of
|
|
43
|
+
sha256(canonical(command strings))>`. The two share the `P-` prefix and 32-hex
|
|
44
|
+
length but differ in origin -- the command_set id is deterministic (minted at
|
|
45
|
+
SubagentStop intake), and once the pending has survived a turn the orchestrator
|
|
46
|
+
reads that id directly from the injected `[PENDING-APPROVALS-VERIFIED]` block
|
|
47
|
+
(no derive-dispatch, no DB search); the singular id is random and the subagent
|
|
48
|
+
relays it directly for the same-turn case. The `P-` prefix is mandatory in both
|
|
49
|
+
cases: without it the PostToolUse
|
|
50
|
+
hook cannot do targeted grant activation. The first 8 hex chars after `P-` are
|
|
51
|
+
the nonce prefix shown in option labels: `[P-b1bdfbb0]`.
|
|
28
52
|
|
|
29
53
|
## APPROVAL_REQUEST contract shape
|
|
30
54
|
|
|
@@ -55,8 +79,11 @@ becomes `rollback` in the contract; `commands` (`[exact_content]`) and
|
|
|
55
79
|
}
|
|
56
80
|
```
|
|
57
81
|
|
|
58
|
-
There is no `batch_scope` field: the `verb_family` grant was removed
|
|
59
|
-
blocked command gets its own single-use
|
|
82
|
+
There is no `batch_scope` field: the `verb_family` grant was removed. For a
|
|
83
|
+
single blocked command, each gets its own single-use `SCOPE_SEMANTIC_SIGNATURE`
|
|
84
|
+
grant. For a batch of >= 2 T3 commands known up-front, emit a `command_set`
|
|
85
|
+
list and **no** `approval_id` -- the SubagentStop intake mints a single
|
|
86
|
+
`COMMAND_SET` grant (one consent covers all). See
|
|
60
87
|
`Skill('orchestrator-present-approval')` for the orchestrator side.
|
|
61
88
|
|
|
62
89
|
## Status vocabularies -- distinct columns, opposite casing, never collapse
|
|
@@ -69,8 +96,8 @@ blocked command gets its own single-use grant. See
|
|
|
69
96
|
## Event chain
|
|
70
97
|
|
|
71
98
|
The `approval_events.event_type` CHECK admits nine values: `REQUESTED` `SHOWN`
|
|
72
|
-
`APPROVED` `REJECTED` `EXECUTED` `FAILED` `NOOP` `REVOKED` `REVERTED`.
|
|
73
|
-
|
|
99
|
+
`APPROVED` `REJECTED` `EXECUTED` `FAILED` `NOOP` `REVOKED` `REVERTED`. These are
|
|
100
|
+
written by production code today:
|
|
74
101
|
|
|
75
102
|
| Event | Who writes it | When |
|
|
76
103
|
|-------|--------------|------|
|
|
@@ -78,11 +105,16 @@ are written by production code today:
|
|
|
78
105
|
| `SHOWN` | ElicitationResult hook via `activate_db_pending_by_prefix()` | User selects an Approve `[P-xxx]` label |
|
|
79
106
|
| `APPROVED` | ElicitationResult hook (same call as `SHOWN`) | Immediately after `SHOWN` |
|
|
80
107
|
| `REJECTED` / `REVOKED` | `gaia approvals` CLI via `store.reject()` / `store.revoke()` | User rejects or admin cancels |
|
|
108
|
+
| `EXECUTED` / `FAILED` | PostToolUse adapter (`_record_t3_outcome_event`) via `store.record_event()` | An approved T3 command runs under a consumed grant -- `EXECUTED` on clean exit, `FAILED` otherwise |
|
|
81
109
|
|
|
82
|
-
|
|
83
|
-
`
|
|
84
|
-
|
|
85
|
-
|
|
110
|
+
The PostToolUse path closes the audit cycle: PreToolUse stashes the consumed
|
|
111
|
+
grant's `approval_id` in `HookState`, and PostToolUse appends `EXECUTED` or
|
|
112
|
+
`FAILED` for that approval, continuing the hash chain through `record_event()`.
|
|
113
|
+
`store.get_executed_payload()` and `gaia approvals replay` read the `EXECUTED`
|
|
114
|
+
payload to re-present the commands that ran. `NOOP` and `REVERTED` remain valid
|
|
115
|
+
in the CHECK but are **inert** -- no production code writes them (the revert
|
|
116
|
+
feature was removed). Do not assume an `EXECUTED` event exists for an approval
|
|
117
|
+
whose command never ran, or that ran through the redirect-sanitized path.
|
|
86
118
|
|
|
87
119
|
## Key invariants
|
|
88
120
|
|
|
@@ -90,9 +122,13 @@ not assume an `EXECUTED` event exists after a command runs.
|
|
|
90
122
|
- `SHOWN` precedes `APPROVED`; the activation path writes them together.
|
|
91
123
|
- `approval_events` is append-only -- the `bu_approval_events_immutable` and
|
|
92
124
|
`bd_approval_events_immutable` triggers `RAISE(ABORT)` on UPDATE/DELETE.
|
|
93
|
-
- The
|
|
94
|
-
`chain.verify_fingerprint(approval_id, payload_json, con)`
|
|
95
|
-
|
|
125
|
+
- The payload's integrity is enforced at grant **activation**, not at
|
|
126
|
+
presentation: `chain.verify_fingerprint(approval_id, payload_json, con)` runs
|
|
127
|
+
when the user selects the Approve label, and a mismatch raises
|
|
128
|
+
`ChainTamperError` so the grant never forms. The orchestrator presents from a
|
|
129
|
+
trusted source (the injected `[PENDING-APPROVALS-VERIFIED]` block, already
|
|
130
|
+
fingerprint-verified by the hook; or a same-turn relayed `approval_request`)
|
|
131
|
+
and never dispatches a subagent to verify or derive the approval.
|
|
96
132
|
|
|
97
133
|
For the grant activation walk-through, fingerprint internals, reading a granted
|
|
98
134
|
approval from Python, and the retry-blocked-again diagnosis, see `reference.md`.
|
|
@@ -12,12 +12,17 @@ canonical string. `store.insert_requested()` stores both the canonical JSON
|
|
|
12
12
|
(`payload_json`) and the hex fingerprint on the `approvals` row and on the
|
|
13
13
|
`REQUESTED` event.
|
|
14
14
|
|
|
15
|
-
The
|
|
16
|
-
`chain.verify_fingerprint(approval_id, payload_json, con)`
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
The fingerprint is verified at grant **activation**, not at presentation.
|
|
16
|
+
`chain.verify_fingerprint(approval_id, payload_json, con)` re-parses and
|
|
17
|
+
re-canonicalizes the payload, recomputes the fingerprint, and compares it
|
|
18
|
+
against the fingerprint stored on the `REQUESTED` event; a mismatch raises
|
|
19
|
+
`ChainTamperError` and the grant never forms -- a security boundary, not a
|
|
20
|
+
recoverable UX issue. The per-turn `[PENDING-APPROVALS-VERIFIED]` builder
|
|
21
|
+
(`build_verified_pending_approvals`) applies the same check when assembling the
|
|
22
|
+
injected block, so only fingerprint-clean pendings reach the orchestrator marked
|
|
23
|
+
`verified: true`. The orchestrator therefore presents from that already-verified
|
|
24
|
+
block (or a same-turn relayed `approval_request`) and never dispatches to verify
|
|
25
|
+
the payload itself.
|
|
21
26
|
|
|
22
27
|
## Hash chain
|
|
23
28
|
|
|
@@ -27,9 +32,11 @@ Each event links to the previous via `prev_hash` -> `this_hash`
|
|
|
27
32
|
Because `approval_events` is append-only (UPDATE/DELETE blocked by the
|
|
28
33
|
`bu_approval_events_immutable` and `bd_approval_events_immutable` triggers),
|
|
29
34
|
`this_hash` is computed in the application layer before INSERT, inside
|
|
30
|
-
`chain.insert_event()` -- not by a DB trigger. `
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
`chain.insert_event()` -- not by a DB trigger. `EXECUTED` / `FAILED` events,
|
|
36
|
+
appended by the PostToolUse adapter through `store.record_event()` after an
|
|
37
|
+
approved T3 command runs, extend the same chain. `REVERTED` remains a valid
|
|
38
|
+
CHECK value but is **inert** -- the revert feature was removed, so no code
|
|
39
|
+
writes it.
|
|
33
40
|
|
|
34
41
|
## Grant activation walk-through
|
|
35
42
|
|
|
@@ -330,4 +330,15 @@ The agent discovered a project fact a section it owns did not yet hold, and writ
|
|
|
330
330
|
|
|
331
331
|
## Notes on multi-command APPROVAL_REQUEST sweeps
|
|
332
332
|
|
|
333
|
-
|
|
333
|
+
**Just-in-time (unknown batch):** when T3 commands appear one at a time as the
|
|
334
|
+
agent works, each blocked command produces its own `APPROVAL_REQUEST` with an
|
|
335
|
+
`approval_id` (shape identical to example 4 above). Do not emit `batch_scope`
|
|
336
|
+
-- it is ignored.
|
|
337
|
+
|
|
338
|
+
**Plan-first (known batch):** when the agent knows >= 2 T3 commands up-front,
|
|
339
|
+
emit ONE `APPROVAL_REQUEST` carrying a `command_set` list of `{command,
|
|
340
|
+
rationale}` items and **no** `approval_id`. The SubagentStop intake
|
|
341
|
+
(`handoff_persister._intake_command_set_pending`) mints a single `COMMAND_SET`
|
|
342
|
+
approval; the orchestrator presents it as one consent covering all N commands.
|
|
343
|
+
Each command then runs on its own retry, byte-for-byte matched and consumed
|
|
344
|
+
individually.
|
|
@@ -109,7 +109,7 @@ The package ships a single `gaia` binary (`bin/gaia.js`) that dispatches to Pyth
|
|
|
109
109
|
| `gaia memory` | `bin/cli/memory.py` | Episodic memory: FTS5 search, show episode, health checks |
|
|
110
110
|
| `gaia metrics` | `bin/cli/metrics.py` | Usage analytics: tier classification, agent invocations, anomaly counters |
|
|
111
111
|
| `gaia paths` | `bin/cli/paths.py` | Inspect canonical Gaia storage paths (DB, plugin root, workspace) |
|
|
112
|
-
| `gaia
|
|
112
|
+
| `gaia plan` | `bin/cli/plan.py` | Manage plans (one per brief, DB-canonical): save, show, list, status |
|
|
113
113
|
| `gaia workspace` | `bin/cli/workspace.py` | Workspace identity and consolidate operations |
|
|
114
114
|
| `gaia scan` | `bin/cli/scan.py` | In-process project scan: detect stack, sync results to ~/.gaia/gaia.db (DB-canonical; no project-context.json written) |
|
|
115
115
|
| `gaia status` | `bin/cli/status.py` | Quick installation snapshot: version, mode, DB path, registered workspace, last scan |
|
|
@@ -289,7 +289,7 @@ After `npm install -g @jaguilar87/gaia` (or via the local symlink) the dispatche
|
|
|
289
289
|
| `gaia history` | Session history viewer | Debugging past sessions |
|
|
290
290
|
| `gaia memory` | Episodic memory inspect/search | Recall past episodes, memory health |
|
|
291
291
|
| `gaia approvals` | List/accept/reject pending T3 approvals | Approval workflow |
|
|
292
|
-
| `gaia brief` / `gaia
|
|
292
|
+
| `gaia brief` / `gaia plan` | Brief and plan management against the DB substrate | Planning, brief lifecycle |
|
|
293
293
|
| `gaia context` | Display and refresh project context | Audit context state |
|
|
294
294
|
| `gaia paths` | Print resolved storage paths | Path debugging |
|
|
295
295
|
| `gaia workspace` | Workspace identity and consolidate operations | Multi-workspace setups |
|