Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions hooks-gt/_shared/hestia_governance_closure.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# hestia-gt-sha256: f32a4241f27f3f89f7d7391d9c02bbe1e1965c0d876c0ffd03377abb6e58687e (published ground truth; manifest: hooks-gt)
# hestia-gt-sha256: 1c5c413502e82fe3e84e19959f32ce378f69a3a08661172df9e9e020407359c7 (published ground truth; manifest: hooks-gt)
"""hestia_governance_closure — ONE governance-access classifier for every harness shim.

PRD gate-consolidation §5 / §6.B / §7.3(8): the effective governance CLOSURE — the files that
Expand Down Expand Up @@ -1713,6 +1713,11 @@ def _bash_write_targets(command: str) -> list:
# destination. Filing records a petition; it does not write the
# requested file (cbp-being, 2026-10-19).
"request_scope"})
# A being's COMPOSED reads (SAGE being_gate_client, #1361): the seat builds a grep / git read from
# {pattern, path}, so their `path` is the read target, never a destination. The NAME buys nothing
# else (review hold on #1362): a `command` such an event carries is classified for write positions
# exactly as a Bash command is, and file_path / notebook_path keep the unknown-tool reading.
_COMPOSED_READ_TOOLS = frozenset({"search", "git_read"})


def _write_position_targets(tool_name: str, tool_input: Any) -> tuple:
Expand All @@ -1722,6 +1727,13 @@ def _write_position_targets(tool_name: str, tool_input: Any) -> tuple:
return [], None
if tool_name in _READ_PATH_TOOLS:
return [], None
if tool_name in _COMPOSED_READ_TOOLS:
targets, note = [], None
if isinstance(tool_input.get("command"), str) and tool_input["command"].strip():
targets, note = _write_position_targets("Bash", {"command": tool_input["command"]})
rest = [v for k in _PATH_KEYS
if k != "path" and isinstance((v := tool_input.get(k)), str) and v]
return list(targets) + rest, note
if tool_name in _BASH_TOOLS:
cmd = tool_input.get("command")
if not isinstance(cmd, str) or not cmd.strip():
Expand Down Expand Up @@ -1752,7 +1764,7 @@ def _read_position_mentions(tool_name: str, tool_input: Any) -> list:
if not isinstance(tool_input, dict):
return []
out = []
if tool_name in _BASH_TOOLS:
if tool_name in _BASH_TOOLS or tool_name in _COMPOSED_READ_TOOLS:
cmd = tool_input.get("command")
if isinstance(cmd, str) and cmd.strip():
try:
Expand Down
4 changes: 2 additions & 2 deletions hooks-gt/_shared/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
{
"path": "hestia_governance_closure.py",
"sha256": "f32a4241f27f3f89f7d7391d9c02bbe1e1965c0d876c0ffd03377abb6e58687e",
"sha256": "1c5c413502e82fe3e84e19959f32ce378f69a3a08661172df9e9e020407359c7",
"source": "plugins/_shared/hestia_governance_closure.py"
},
{
Expand All @@ -38,5 +38,5 @@
],
"engine": null,
"requires": [],
"gt_version": "0582b82b2310e67e6e121174327bf3d6f50b01b14f9d621ec1fff89d004a74c5"
"gt_version": "f97e63f0a21def86534f0aa486a6f32a6e16159bd3be8c3cd81202d61541093b"
}
6 changes: 3 additions & 3 deletions hooks-gt/claude-code/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
],
"engine": {
"unit": "_shared",
"gt_version": "0582b82b2310e67e6e121174327bf3d6f50b01b14f9d621ec1fff89d004a74c5",
"gt_version": "f97e63f0a21def86534f0aa486a6f32a6e16159bd3be8c3cd81202d61541093b",
"files": [
{
"path": "hestia_gate_core.py",
Expand All @@ -45,7 +45,7 @@
},
{
"path": "hestia_governance_closure.py",
"sha256": "f32a4241f27f3f89f7d7391d9c02bbe1e1965c0d876c0ffd03377abb6e58687e"
"sha256": "1c5c413502e82fe3e84e19959f32ce378f69a3a08661172df9e9e020407359c7"
},
{
"path": "hestia_shell_classifier.py",
Expand All @@ -62,5 +62,5 @@
]
},
"requires": [],
"gt_version": "369734a7467ee2e381c947aec5d3733f8c9e97e57d92f878d7893549c85e2000"
"gt_version": "b1662c83d9c4cc12fd7d23ee192586802880388316821076bb0bbab1bf8e4e28"
}
6 changes: 3 additions & 3 deletions hooks-gt/codex/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
],
"engine": {
"unit": "_shared",
"gt_version": "0582b82b2310e67e6e121174327bf3d6f50b01b14f9d621ec1fff89d004a74c5",
"gt_version": "f97e63f0a21def86534f0aa486a6f32a6e16159bd3be8c3cd81202d61541093b",
"files": [
{
"path": "hestia_gate_core.py",
Expand All @@ -45,7 +45,7 @@
},
{
"path": "hestia_governance_closure.py",
"sha256": "f32a4241f27f3f89f7d7391d9c02bbe1e1965c0d876c0ffd03377abb6e58687e"
"sha256": "1c5c413502e82fe3e84e19959f32ce378f69a3a08661172df9e9e020407359c7"
},
{
"path": "hestia_shell_classifier.py",
Expand All @@ -62,5 +62,5 @@
]
},
"requires": [],
"gt_version": "e71a8d2fbbf130ef19240296c23ea9c98b308ff39fd638325faadff38c32f425"
"gt_version": "726f554213431a316ebc331fa7bdd545f363aeb332a350fbc8c2f022031f7615"
}
6 changes: 3 additions & 3 deletions hooks-gt/gemini/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
],
"engine": {
"unit": "_shared",
"gt_version": "0582b82b2310e67e6e121174327bf3d6f50b01b14f9d621ec1fff89d004a74c5",
"gt_version": "f97e63f0a21def86534f0aa486a6f32a6e16159bd3be8c3cd81202d61541093b",
"files": [
{
"path": "hestia_gate_core.py",
Expand All @@ -45,7 +45,7 @@
},
{
"path": "hestia_governance_closure.py",
"sha256": "f32a4241f27f3f89f7d7391d9c02bbe1e1965c0d876c0ffd03377abb6e58687e"
"sha256": "1c5c413502e82fe3e84e19959f32ce378f69a3a08661172df9e9e020407359c7"
},
{
"path": "hestia_shell_classifier.py",
Expand All @@ -62,5 +62,5 @@
]
},
"requires": [],
"gt_version": "23a31fd6e2ffe46e6617c1ccd04dd47247c77a7e806ec327c9663b4b86de092e"
"gt_version": "75f580643a40dbc06dc9849c95dd2d07aee9a6756b44ecb393a967b32ddfe9b5"
}
6 changes: 3 additions & 3 deletions hooks-gt/kimi/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
],
"engine": {
"unit": "_shared",
"gt_version": "0582b82b2310e67e6e121174327bf3d6f50b01b14f9d621ec1fff89d004a74c5",
"gt_version": "f97e63f0a21def86534f0aa486a6f32a6e16159bd3be8c3cd81202d61541093b",
"files": [
{
"path": "hestia_gate_core.py",
Expand All @@ -45,7 +45,7 @@
},
{
"path": "hestia_governance_closure.py",
"sha256": "f32a4241f27f3f89f7d7391d9c02bbe1e1965c0d876c0ffd03377abb6e58687e"
"sha256": "1c5c413502e82fe3e84e19959f32ce378f69a3a08661172df9e9e020407359c7"
},
{
"path": "hestia_shell_classifier.py",
Expand All @@ -62,5 +62,5 @@
]
},
"requires": [],
"gt_version": "70c7b764ce150a6c8b599e5f8bfb4fc318c800b513521a1f213998e03e6b1516"
"gt_version": "4cc3378842c36bc9d8a17c319ceab5713c8a2282255bbf302285a384b60bf1f0"
}
14 changes: 13 additions & 1 deletion plugins/_shared/hestia_governance_closure.py
Original file line number Diff line number Diff line change
Expand Up @@ -1712,6 +1712,11 @@ def _bash_write_targets(command: str) -> list:
# destination. Filing records a petition; it does not write the
# requested file (cbp-being, 2026-10-19).
"request_scope"})
# A being's COMPOSED reads (SAGE being_gate_client, #1361): the seat builds a grep / git read from
# {pattern, path}, so their `path` is the read target, never a destination. The NAME buys nothing
# else (review hold on #1362): a `command` such an event carries is classified for write positions
# exactly as a Bash command is, and file_path / notebook_path keep the unknown-tool reading.
_COMPOSED_READ_TOOLS = frozenset({"search", "git_read"})


def _write_position_targets(tool_name: str, tool_input: Any) -> tuple:
Expand All @@ -1721,6 +1726,13 @@ def _write_position_targets(tool_name: str, tool_input: Any) -> tuple:
return [], None
if tool_name in _READ_PATH_TOOLS:
return [], None
if tool_name in _COMPOSED_READ_TOOLS:
targets, note = [], None
if isinstance(tool_input.get("command"), str) and tool_input["command"].strip():
targets, note = _write_position_targets("Bash", {"command": tool_input["command"]})
rest = [v for k in _PATH_KEYS
if k != "path" and isinstance((v := tool_input.get(k)), str) and v]
return list(targets) + rest, note
if tool_name in _BASH_TOOLS:
cmd = tool_input.get("command")
if not isinstance(cmd, str) or not cmd.strip():
Expand Down Expand Up @@ -1751,7 +1763,7 @@ def _read_position_mentions(tool_name: str, tool_input: Any) -> list:
if not isinstance(tool_input, dict):
return []
out = []
if tool_name in _BASH_TOOLS:
if tool_name in _BASH_TOOLS or tool_name in _COMPOSED_READ_TOOLS:
cmd = tool_input.get("command")
if isinstance(cmd, str) and cmd.strip():
try:
Expand Down
31 changes: 31 additions & 0 deletions tools/closure_read_tool_names_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,35 @@ def test_a_scope_request_naming_a_gate_file_is_not_a_write():
check("request_scope_is_not_write", v.classification != "write", str(v))


def test_a_being_composed_read_verb_is_a_read():
# SAGE's search and git_read reach the gate under their own names with {pattern, path} /
# {op, rev, path}; the seat composes a grep / git read from them. Both were refused as
# governance WRITES on the snapshot (cbp-being, 2026-10-21: 08f7aa13417d5c50, #1361).
for tool, args in (("search", {"pattern": "verdict_available", "path": GATE_FILE}),
("search", {"pattern": "x", "path": os.path.dirname(GATE_FILE)}),
("git_read", {"op": "log", "path": GATE_FILE})):
v = g.classify(tool, args, cwd=NEUTRAL_CWD)
check(f"{tool}_is_read", v.classification == "read", str(v))
# A composed command that only READS keeps it a read.
for tool, cmd in (("search", f"grep -r -n -I -E -s --max-count=20 -e verdict_available -- {GATE_FILE}"),
("git_read", f"git -C /tmp/wt log --oneline -- {GATE_FILE}")):
v = g.classify(tool, {"path": GATE_FILE, "command": cmd}, cwd=NEUTRAL_CWD)
check(f"{tool}_reading_command_is_read", v.classification == "read", str(v))


def test_a_composed_read_name_does_not_launder_a_write():
# Review hold on #1362: the NAME must not be what makes it a read. A command such an event
# carries is classified for write positions as Bash is, and the other destination keys keep
# the unknown-tool reading. Each of these was a read at a95d0577.
for tool in ("search", "git_read"):
for cmd in (f"tee {GATE_FILE}", f"sed -i s/a/b/ {GATE_FILE}", f"cat x > {GATE_FILE}",
f"grep -r x -- /tmp && cp /tmp/evil {GATE_FILE}"):
v = g.classify(tool, {"path": GATE_FILE, "command": cmd}, cwd=NEUTRAL_CWD)
check(f"{tool}_command_write::{cmd}", v.classification == "write", str(v))
v = g.classify(tool, {"pattern": "x", "file_path": GATE_FILE}, cwd=NEUTRAL_CWD)
check(f"{tool}_file_path_is_write", v.classification == "write", str(v))


def test_writers_and_unknown_tools_still_write():
# Controls: the fix must not widen into the write names, and an unknown tool keeps the
# conservative default (its path keys are destinations).
Expand Down Expand Up @@ -97,6 +126,8 @@ def test_witness_read_names_are_closure_readers():
test_a_being_read_of_a_gate_file_is_a_read,
test_every_lowercase_reader_is_a_read,
test_a_scope_request_naming_a_gate_file_is_not_a_write,
test_a_being_composed_read_verb_is_a_read,
test_a_composed_read_name_does_not_launder_a_write,
test_writers_and_unknown_tools_still_write,
test_gemini_readers_are_closure_readers,
test_witness_read_names_are_closure_readers,
Expand Down
Loading