@jaguilar87/gaia 5.0.8 → 5.0.10
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 +10 -3
- package/bin/cli/_install_helpers.py +0 -3
- package/bin/cli/approvals.py +341 -238
- package/bin/cli/brief.py +45 -4
- package/bin/cli/cleanup.py +304 -4
- package/bin/cli/doctor.py +1 -5
- package/bin/cli/uninstall.py +20 -0
- package/dist/gaia-ops/.claude-plugin/plugin.json +1 -1
- package/dist/gaia-ops/hooks/adapters/claude_code.py +19 -85
- package/dist/gaia-ops/hooks/modules/context/context_injector.py +23 -7
- package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +0 -5
- 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/capability_classes.py +83 -6
- package/dist/gaia-ops/hooks/modules/security/inline_ast_analyzer.py +237 -0
- package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +434 -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 +177 -20
- 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 +27 -7
- package/dist/gaia-ops/skills/agent-approval-protocol/reference.md +11 -6
- package/dist/gaia-ops/skills/gaia-patterns/reference.md +2 -2
- package/dist/gaia-ops/skills/orchestrator-present-approval/SKILL.md +69 -28
- package/dist/gaia-ops/skills/orchestrator-present-approval/reference.md +16 -3
- package/dist/gaia-ops/skills/orchestrator-present-approval/template.md +10 -5
- package/dist/gaia-ops/skills/pending-approvals/SKILL.md +16 -11
- package/dist/gaia-ops/skills/security-tiers/SKILL.md +1 -1
- package/dist/gaia-ops/skills/subagent-request-approval/SKILL.md +20 -6
- package/dist/gaia-ops/skills/subagent-request-approval/reference.md +23 -15
- 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 +19 -85
- package/dist/gaia-security/hooks/modules/context/context_injector.py +23 -7
- package/dist/gaia-security/hooks/modules/core/plugin_setup.py +0 -5
- 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/capability_classes.py +83 -6
- package/dist/gaia-security/hooks/modules/security/inline_ast_analyzer.py +237 -0
- package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +434 -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 +177 -20
- package/dist/gaia-security/hooks/user_prompt_submit.py +20 -0
- package/gaia/approvals/store.py +87 -9
- package/gaia/briefs/__init__.py +4 -0
- package/gaia/briefs/store.py +91 -0
- package/gaia/store/schema.sql +38 -1
- package/gaia/store/writer.py +400 -0
- package/hooks/adapters/claude_code.py +19 -85
- package/hooks/elicitation_result.py +20 -75
- package/hooks/modules/context/context_injector.py +23 -7
- package/hooks/modules/core/plugin_setup.py +0 -5
- 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/capability_classes.py +83 -6
- package/hooks/modules/security/inline_ast_analyzer.py +237 -0
- package/hooks/modules/security/mutative_verbs.py +434 -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 +177 -20
- 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 +20 -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 +27 -7
- package/skills/agent-approval-protocol/reference.md +11 -6
- package/skills/gaia-patterns/reference.md +2 -2
- package/skills/orchestrator-present-approval/SKILL.md +69 -28
- package/skills/orchestrator-present-approval/reference.md +16 -3
- package/skills/orchestrator-present-approval/template.md +10 -5
- package/skills/pending-approvals/SKILL.md +16 -11
- package/skills/security-tiers/SKILL.md +1 -1
- package/skills/subagent-request-approval/SKILL.md +20 -6
- package/skills/subagent-request-approval/reference.md +23 -15
- 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/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
|
@@ -911,6 +911,57 @@ class BashValidator:
|
|
|
911
911
|
reason="Safe by elimination (not blocked, not mutative)",
|
|
912
912
|
)
|
|
913
913
|
|
|
914
|
+
def _is_ungranted_t3_component(
|
|
915
|
+
self, component: str, session_id: str
|
|
916
|
+
) -> bool:
|
|
917
|
+
"""Classify a chain component as ungranted-T3 WITHOUT minting or consuming.
|
|
918
|
+
|
|
919
|
+
Returns True when the component is a T3 (mutative-verb or
|
|
920
|
+
flag-dependent) operation for which NO active grant exists -- i.e. the
|
|
921
|
+
component would, on its own, be blocked pending approval. This is a
|
|
922
|
+
read-only probe used by the chain COMMAND_SET intake (AC-8) to decide
|
|
923
|
+
whether >= 2 sub-commands need grouping under ONE consent, BEFORE any
|
|
924
|
+
per-component minting happens.
|
|
925
|
+
|
|
926
|
+
It deliberately does NOT call decide_t3_outcome (no pending minted) and
|
|
927
|
+
does NOT consume any grant (match_command_set_grant /
|
|
928
|
+
check_approval_grant are pure lookups; consumption happens later in the
|
|
929
|
+
real _validate_single_command pass at retry). A component that already
|
|
930
|
+
matches a COMMAND_SET or semantic grant is treated as NOT ungranted, so
|
|
931
|
+
it is excluded from a fresh batch.
|
|
932
|
+
"""
|
|
933
|
+
component = component.strip()
|
|
934
|
+
if not component:
|
|
935
|
+
return False
|
|
936
|
+
|
|
937
|
+
# Is this T3 (mutative verb or flag-dependent mutation)?
|
|
938
|
+
detect = detect_mutative_command(component)
|
|
939
|
+
is_t3 = detect.is_mutative
|
|
940
|
+
if not is_t3:
|
|
941
|
+
flag_result = classify_by_flags(component)
|
|
942
|
+
if (
|
|
943
|
+
flag_result is not None
|
|
944
|
+
and flag_result.outcome == FLAG_MUTATIVE
|
|
945
|
+
and not flag_result.command_family.startswith("git_")
|
|
946
|
+
):
|
|
947
|
+
is_t3 = True
|
|
948
|
+
if not is_t3:
|
|
949
|
+
return False
|
|
950
|
+
|
|
951
|
+
# Already covered by an active grant? Then it is NOT ungranted -- exclude
|
|
952
|
+
# it from a fresh batch (pure lookups, no consumption).
|
|
953
|
+
try:
|
|
954
|
+
if match_command_set_grant(component) is not None:
|
|
955
|
+
return False
|
|
956
|
+
except Exception:
|
|
957
|
+
pass
|
|
958
|
+
try:
|
|
959
|
+
if check_approval_grant(component, session_id=session_id) is not None:
|
|
960
|
+
return False
|
|
961
|
+
except Exception:
|
|
962
|
+
pass
|
|
963
|
+
return True
|
|
964
|
+
|
|
914
965
|
def _validate_compound_command(
|
|
915
966
|
self,
|
|
916
967
|
components: List[str],
|
|
@@ -918,9 +969,68 @@ class BashValidator:
|
|
|
918
969
|
session_id: str = "",
|
|
919
970
|
agent_type: str = "",
|
|
920
971
|
) -> BashValidationResult:
|
|
921
|
-
"""Validate a compound command (multiple components).
|
|
972
|
+
"""Validate a compound command (multiple components).
|
|
973
|
+
|
|
974
|
+
Chain COMMAND_SET intake (AC-8): when a chain ``a && b && c`` has TWO OR
|
|
975
|
+
MORE sub-commands that are ungranted T3, classifying them one-at-a-time
|
|
976
|
+
mints a single-signature pending for the FIRST and short-circuits -- so
|
|
977
|
+
one approval covers only the first sub-command and the next re-blocks
|
|
978
|
+
(the double-approval the user hit). To group them, a NON-MINTING
|
|
979
|
+
classification pass runs FIRST (``_is_ungranted_t3_component``); if >= 2
|
|
980
|
+
sub-commands are ungranted-T3 (and we are a subagent under the
|
|
981
|
+
orchestrator), ONE COMMAND_SET pending is minted over exactly those T3
|
|
982
|
+
sub-commands via ``decide_t3_outcome(command_set=...)``. One approval
|
|
983
|
+
then covers the chain; each sub-command is still consumed byte-for-byte
|
|
984
|
+
by its own signature at retry (no consent is widened -- the commands are
|
|
985
|
+
only grouped). Critically, the per-component minting path
|
|
986
|
+
(_validate_single_command) is NEVER entered for the batch, so no stray
|
|
987
|
+
single pendings are minted alongside the COMMAND_SET.
|
|
988
|
+
|
|
989
|
+
For every other shape (0 or 1 ungranted-T3, no orchestrator above, or a
|
|
990
|
+
component that is hard-blocked) the original per-component pass runs
|
|
991
|
+
unchanged: a hard block fails the chain fast, a lone T3 keeps the
|
|
992
|
+
singular grant path, and an all-granted/safe chain is allowed.
|
|
993
|
+
"""
|
|
922
994
|
logger.info(f"Compound command detected with {len(components)} components")
|
|
923
995
|
|
|
996
|
+
# NON-MINTING pre-pass: which components are ungranted T3? (AC-8)
|
|
997
|
+
if is_subagent and is_ops_mode():
|
|
998
|
+
ungranted_t3_idx = [
|
|
999
|
+
idx
|
|
1000
|
+
for idx, comp in enumerate(components)
|
|
1001
|
+
if self._is_ungranted_t3_component(comp, session_id)
|
|
1002
|
+
]
|
|
1003
|
+
if len(ungranted_t3_idx) >= 2:
|
|
1004
|
+
chain_set = [
|
|
1005
|
+
{"command": components[idx].strip(), "rationale": ""}
|
|
1006
|
+
for idx in ungranted_t3_idx
|
|
1007
|
+
]
|
|
1008
|
+
first_cmd = chain_set[0]["command"]
|
|
1009
|
+
first_detect = detect_mutative_command(first_cmd)
|
|
1010
|
+
verb = first_detect.verb or "command"
|
|
1011
|
+
category = first_detect.category or "MUTATIVE"
|
|
1012
|
+
native_ask_reason = (
|
|
1013
|
+
f"[T3_APPROVAL_REQUIRED] Chain of {len(chain_set)} T3 commands.\n"
|
|
1014
|
+
f"Commands:\n"
|
|
1015
|
+
+ "\n".join(f" - {it['command']}" for it in chain_set)
|
|
1016
|
+
)
|
|
1017
|
+
logger.info(
|
|
1018
|
+
"Chain COMMAND_SET intake: %d T3 sub-commands grouped under "
|
|
1019
|
+
"one consent (chain=%s)",
|
|
1020
|
+
len(chain_set),
|
|
1021
|
+
" && ".join(it["command"][:30] for it in chain_set),
|
|
1022
|
+
)
|
|
1023
|
+
return decide_t3_outcome(
|
|
1024
|
+
first_cmd,
|
|
1025
|
+
verb=verb,
|
|
1026
|
+
category=category,
|
|
1027
|
+
has_orchestrator_above=True,
|
|
1028
|
+
native_ask_reason=native_ask_reason,
|
|
1029
|
+
session_id=session_id,
|
|
1030
|
+
agent_type=agent_type,
|
|
1031
|
+
command_set=chain_set,
|
|
1032
|
+
)
|
|
1033
|
+
|
|
924
1034
|
component_results: List[BashValidationResult] = []
|
|
925
1035
|
for i, component in enumerate(components, 1):
|
|
926
1036
|
result = self._validate_single_command(
|
|
@@ -1385,6 +1495,7 @@ def decide_t3_outcome(
|
|
|
1385
1495
|
native_ask_reason: str,
|
|
1386
1496
|
session_id: str = "",
|
|
1387
1497
|
agent_type: str = "",
|
|
1498
|
+
command_set: list | None = None,
|
|
1388
1499
|
) -> BashValidationResult:
|
|
1389
1500
|
"""Single decision point for the outcome of a T3 (state-mutating) command.
|
|
1390
1501
|
|
|
@@ -1416,34 +1527,68 @@ def decide_t3_outcome(
|
|
|
1416
1527
|
native_ask_reason: Reason text for the native-ask fallback branch.
|
|
1417
1528
|
session_id: Session ID for pending-approval scoping.
|
|
1418
1529
|
agent_type: Originating agent name (for the sealed payload).
|
|
1530
|
+
command_set: Optional list of ``{command, rationale}`` dicts. When it
|
|
1531
|
+
carries MORE THAN ONE item, this T3 decision covers a chain
|
|
1532
|
+
(``a && b && c``) whose sub-commands are all T3, and the pending is
|
|
1533
|
+
minted as ONE COMMAND_SET envelope (the chain-intake path, AC-8)
|
|
1534
|
+
instead of a single semantic-signature pending. ONE user approval
|
|
1535
|
+
then covers the whole chain; each sub-command is still consumed
|
|
1536
|
+
byte-for-byte by its own signature at retry. A None / single-item
|
|
1537
|
+
set keeps the singular behaviour. Only honoured in the
|
|
1538
|
+
subagent-under-orchestrator branch (the native-ask branch has no
|
|
1539
|
+
COMMAND_SET concept).
|
|
1419
1540
|
|
|
1420
1541
|
Returns:
|
|
1421
1542
|
A blocked BashValidationResult (allowed=False, tier T3) whose
|
|
1422
1543
|
block_response is either a "deny" (with approval_id) or an "ask".
|
|
1423
1544
|
"""
|
|
1545
|
+
# A genuine multi-command chain is a set of >= 2 items. Anything else
|
|
1546
|
+
# collapses to the singular path so we never mint a COMMAND_SET for one
|
|
1547
|
+
# command (mirrors _build_sealed_payload's is_command_set guard).
|
|
1548
|
+
_normalized_set: list = []
|
|
1549
|
+
if command_set:
|
|
1550
|
+
for _item in command_set:
|
|
1551
|
+
if isinstance(_item, dict) and _item.get("command"):
|
|
1552
|
+
_normalized_set.append(
|
|
1553
|
+
{
|
|
1554
|
+
"command": _item["command"],
|
|
1555
|
+
"rationale": _item.get("rationale", ""),
|
|
1556
|
+
}
|
|
1557
|
+
)
|
|
1558
|
+
is_chain_command_set = len(_normalized_set) > 1
|
|
1559
|
+
|
|
1424
1560
|
if has_orchestrator_above:
|
|
1425
1561
|
# Subagent-under-orchestrator: deny + persisted approval_id so the
|
|
1426
1562
|
# orchestrator can run the approval cycle. Reuse an existing pending
|
|
1427
1563
|
# approval on retry to avoid generating duplicates while the user reviews.
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1564
|
+
#
|
|
1565
|
+
# For a COMMAND_SET chain the pending id is CONTENT-derived (matching the
|
|
1566
|
+
# plan-first intake), so a retry of the same chain produces the same id
|
|
1567
|
+
# and the fingerprint-dedup in insert_requested reuses the pending. The
|
|
1568
|
+
# singular reuse probe (_find_pending_in_db) matches a SINGLE command's
|
|
1569
|
+
# signature and must NOT be consulted for the chain -- it would match one
|
|
1570
|
+
# leftover single pending of a sub-command and degrade the chain back to
|
|
1571
|
+
# a single grant. So the chain path skips it entirely.
|
|
1572
|
+
if not is_chain_command_set:
|
|
1573
|
+
approval_id = _find_pending_in_db(session_id or "", command)
|
|
1574
|
+
if approval_id:
|
|
1575
|
+
logger.info(
|
|
1576
|
+
"Reusing pending approval_id=%s for retry: %s",
|
|
1577
|
+
approval_id, command[:80],
|
|
1578
|
+
)
|
|
1579
|
+
reason = build_t3_blocked_denial_message(
|
|
1580
|
+
approval_id=approval_id,
|
|
1581
|
+
command=command,
|
|
1582
|
+
verb=verb,
|
|
1583
|
+
category=category,
|
|
1584
|
+
)
|
|
1585
|
+
hook_deny = build_hook_permission_response("deny", reason)
|
|
1586
|
+
return BashValidationResult(
|
|
1587
|
+
allowed=False,
|
|
1588
|
+
tier=SecurityTier.T3_BLOCKED,
|
|
1589
|
+
reason=f"T3 {category.lower()} command: {command[:60]}",
|
|
1590
|
+
block_response=hook_deny,
|
|
1591
|
+
)
|
|
1447
1592
|
|
|
1448
1593
|
# No existing pending -- insert via DB (D16: exclusive path).
|
|
1449
1594
|
sealed_payload = _build_sealed_payload(
|
|
@@ -1451,13 +1596,25 @@ def decide_t3_outcome(
|
|
|
1451
1596
|
verb=verb,
|
|
1452
1597
|
category=category,
|
|
1453
1598
|
agent_type=agent_type,
|
|
1599
|
+
command_set=_normalized_set if is_chain_command_set else None,
|
|
1454
1600
|
)
|
|
1455
1601
|
try:
|
|
1456
1602
|
from gaia.approvals.store import insert_requested
|
|
1603
|
+
# COMMAND_SET chains use a CONTENT-derived id (deterministic over the
|
|
1604
|
+
# sub-command list) so a retry of the same chain reproduces the same
|
|
1605
|
+
# id and reuses the pending via fingerprint dedup -- identical to the
|
|
1606
|
+
# plan-first intake in handoff_persister. Singular T3 keeps uuid4.
|
|
1607
|
+
supplied_id = None
|
|
1608
|
+
if is_chain_command_set:
|
|
1609
|
+
from gaia.approvals.store import derive_command_set_id
|
|
1610
|
+
supplied_id = derive_command_set_id(
|
|
1611
|
+
[it["command"] for it in _normalized_set]
|
|
1612
|
+
)
|
|
1457
1613
|
approval_id = insert_requested(
|
|
1458
1614
|
sealed_payload,
|
|
1459
1615
|
agent_id=agent_type or None,
|
|
1460
1616
|
session_id=session_id or None,
|
|
1617
|
+
approval_id=supplied_id,
|
|
1461
1618
|
)
|
|
1462
1619
|
except Exception as _store_err:
|
|
1463
1620
|
logger.warning(
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -27,10 +41,12 @@ For a **singular** T3 approval (the hook-block path),
|
|
|
27
41
|
verbatim. For a **plan-first `COMMAND_SET`** the id is instead **content-derived**
|
|
28
42
|
by `store.derive_command_set_id()`: `P-<first 32 hex of
|
|
29
43
|
sha256(canonical(command strings))>`. The two share the `P-` prefix and 32-hex
|
|
30
|
-
length but differ in origin -- the command_set id is deterministic
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
34
50
|
hook cannot do targeted grant activation. The first 8 hex chars after `P-` are
|
|
35
51
|
the nonce prefix shown in option labels: `[P-b1bdfbb0]`.
|
|
36
52
|
|
|
@@ -106,9 +122,13 @@ whose command never ran, or that ran through the redirect-sanitized path.
|
|
|
106
122
|
- `SHOWN` precedes `APPROVED`; the activation path writes them together.
|
|
107
123
|
- `approval_events` is append-only -- the `bu_approval_events_immutable` and
|
|
108
124
|
`bd_approval_events_immutable` triggers `RAISE(ABORT)` on UPDATE/DELETE.
|
|
109
|
-
- The
|
|
110
|
-
`chain.verify_fingerprint(approval_id, payload_json, con)`
|
|
111
|
-
|
|
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.
|
|
112
132
|
|
|
113
133
|
For the grant activation walk-through, fingerprint internals, reading a granted
|
|
114
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
|
|
|
@@ -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 |
|
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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,25 +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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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.
|
|
49
79
|
|
|
50
80
|
## Mandatory presentation -- 5 labeled fields + nonce-suffixed label
|
|
51
81
|
|
|
@@ -72,7 +102,13 @@ whose `id` starts with `P-{prefix}`. Without the suffix no grant is created.
|
|
|
72
102
|
See `template.md` for the canonical layout and `reference.md` -> "GOOD vs BAD
|
|
73
103
|
Examples" for full presentations.
|
|
74
104
|
|
|
75
|
-
Fields above are extracted from
|
|
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.
|
|
76
112
|
|
|
77
113
|
## Rules
|
|
78
114
|
|
|
@@ -90,7 +126,12 @@ Fields above are extracted from the DB-stored canonical payload (`payload_json`
|
|
|
90
126
|
`APPROVAL_REQUEST` carrying a `command_set` of >= 2 `{command, rationale}`
|
|
91
127
|
items and **no** `approval_id`, the SubagentStop processor
|
|
92
128
|
(`handoff_persister._intake_command_set_pending`) mints ONE pending
|
|
93
|
-
`COMMAND_SET` with one `approval_id`.
|
|
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
|
|
94
135
|
**all N commands** in the question body, but use **one** Approve label with
|
|
95
136
|
**one** `[P-{nonce8}]` suffix -- one consent covers the whole batch. On
|
|
96
137
|
approval, `activate_db_pending_by_prefix` Step 3b creates a single
|
|
@@ -126,5 +167,5 @@ wording, see `reference.md` -> "GOOD vs BAD Examples", "Option Label Patterns",
|
|
|
126
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. |
|
|
127
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. |
|
|
128
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. |
|
|
129
|
-
| "I can paraphrase a field before relaying" | The fingerprint covers all
|
|
130
|
-
| **"
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
5
|
-
|
|
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,8 +27,9 @@ AskUserQuestion(
|
|
|
23
27
|
)
|
|
24
28
|
```
|
|
25
29
|
|
|
26
|
-
Where `approval_id_prefix8` is the first 8 characters
|
|
27
|
-
|
|
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
34
|
## Batch template (COMMAND_SET)
|
|
30
35
|
|
|
@@ -47,4 +52,4 @@ ignored -- the signal is the presence of `command_set` in the contract.
|
|
|
47
52
|
| SCOPE | `sealed_payload.scope` |
|
|
48
53
|
| RIESGO | `sealed_payload.risk_level` + `sealed_payload.rationale` |
|
|
49
54
|
| ROLLBACK | `sealed_payload.rollback_hint` (null -> "NOT REVERSIBLE") |
|
|
50
|
-
| Option nonce suffix | `
|
|
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) |
|