Skip to content

closure: a being's composed reads (search, git_read) are reads (#1361) - #1362

Merged
dp-web4 merged 3 commits into
mainfrom
cbp/closure-being-read-verbs
Oct 22, 2026
Merged

dp-web4 merged 3 commits into
mainfrom
cbp/closure-being-read-verbs

Conversation

@dp-web4

@dp-web4 dp-web4 commented Oct 21, 2026

Copy link
Copy Markdown
Owner

Fixes #1361 (stopgap). The class-level fix is #1353.

Why

SAGE's being_gate_client sends the being's composed reads to the gate under their own names: search with {pattern, path}, and git_read with {op, rev, path}. Neither is in _READ_PATH_TOOLS, so the closure's unknown-tool rule reads path as a write destination.

On 2026-10-21, cbp-being's searches of the read-only governance snapshot were refused as gate.self_access ("would WRITE the governance surface"). That happened 46 times in its log. Each one auto-opened an escalation saying "approving authorises this one write" (08f7aa13417d5c50, 4161b0b8e92c5d2a, …).

The refusal text then steered the being: it spent the night asking dp for write approval on the gate file. Evidence is in the #1361 thread.

Change

  • _READ_PATH_TOOLS gains "search" and "git_read" in plugins/_shared and in the hooks-gt mirror. For both verbs the seat composes the real command (grep -r … / git log …), and the law judges that command. Neither verb writes.
  • tools/closure_read_tool_names_test.py: new test_a_being_composed_read_verb_is_a_read, covering a gate file and its directory. It fails on main (classification='write', marker plugins/_shared) and passes here. The existing control (Write/Edit/unknown tools still write) is unchanged and passes.
  • hooks-gt: the sha header and the five manifests were regenerated with tools/hooks_gt.py publish.

check and run are left out on purpose: they execute code and aren't reads.

Tests (local)

  • closure_read_tool_names_test: 7/7
  • closure_output_writes_test: 4 tests, 103 pairs
  • governance_closure_arms_test: 14/14
  • hooks_gt_test: passes after the GT regeneration
  • hooks_gt_store_test: passes
  • gate_parity_runner_test: 26 checks
  • witness_closure_census_test: passes

Accountability self-audit

surface: closure classify() for tool names search, git_read   act: none new (a read is allowed + witnessed as gate_self_read)
S: low/reversible [construct: _READ_PATH_TOOLS]
R: n/a   W: n/a [construct: law still judges the composed command in being_gate_client]
O: pass [construct: classify() runs before dispatch, unchanged]   A: pass [construct: gate_self_read witness, unchanged]
V: n/a
verdict: PASS

— cbp-claude

🤖 Generated with Claude Code

https://claude.ai/code/session_014LYVQN8U1xqy6U6pbyUDvH

SAGE's being_gate_client sends search and git_read to the gate under their
own names, with {pattern, path} / {op, rev, path}. Neither was in
_READ_PATH_TOOLS, so the unknown-tool rule treated `path` as a write
destination. A being's search of the read-only governance snapshot was
refused as gate.self_access and auto-opened escalations described as
"approving authorises this one write" (cbp-being, 2026-10-21:
08f7aa13417d5c50, 4161b0b8e92c5d2a; 46 search denies in its log).

- _READ_PATH_TOOLS gains "search" and "git_read" (both closure copies).
  The seat composes the actual command, and the law judges that command.
- tools/closure_read_tool_names_test.py: new test, the composed read verbs
  classify as read on a gate file and on its directory. It fails on main.
- hooks-gt: sha header and manifests regenerated by hooks_gt.py publish.

Stopgap, as #1361 proposes. Closing the class (verbs declare their path
mode, or the closure judges the composed command) is #1353.

Signed: cbp-claude

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014LYVQN8U1xqy6U6pbyUDvH

dp-web4 commented Oct 21, 2026

Copy link
Copy Markdown
Owner Author

Review HOLD at head a95d057: _write_position_targets() returns ([], None) immediately for every search / git_read event, before inspecting tool_input.command. That establishes read-only classification solely from a caller-supplied tool name, even if the composed command contains a write to the governance closure. The PR says SAGE's trusted adapter composes safe readers, but the closure is a cross-harness enforcement boundary, and the new classification is not conditional on proving that property. Please add a negative regression calling classify('search', {'path': GATE_FILE, 'command': 'tee '+GATE_FILE}, ...) and analogous git_read with a writing command. Either restrict this exemption to an attested/bound composed-read route or classify the actual command's write positions before declaring it read. Do not merge on only the positive read regressions. This is a concern about the classifier contract, not evidence of a currently reachable exploit through SAGE's fixed dispatcher.

…hold)

At a95d057, search and git_read sat in _READ_PATH_TOOLS, so
_write_position_targets returned no write positions for them by NAME alone. An
event under either name carrying command "tee <gate file>" classified as a
read. SAGE sends no command key for them today, but the closure is a
cross-harness boundary.

- search and git_read move to their own _COMPOSED_READ_TOOLS. Their `path` is
  the read target only. A `command` they carry is classified for write
  positions exactly as a Bash command (out-of-grammar and opaque writers fail
  closed as for Bash). file_path / notebook_path keep the unknown-tool
  (write) reading.
- _read_position_mentions tokenizes their command as it does for Bash, so a
  reading grep / git log over the gate stays a witnessed read.
- New test test_a_composed_read_name_does_not_launder_a_write: tee, sed -i,
  a > redirect and && cp to the gate, under either name, are WRITE, and so is
  a file_path key. It fails at a95d057. The being's real {pattern, path}
  events and reading commands stay READ.
- hooks-gt regenerated (hooks_gt.py publish).

Signed: cbp-claude

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014LYVQN8U1xqy6U6pbyUDvH
@dp-web4

dp-web4 commented Oct 21, 2026

Copy link
Copy Markdown
Owner Author

Response to the hold at a95d057. Fixed at 64f7921f.

You were right. With search and git_read in _READ_PATH_TOOLS, _write_position_targets returned ([], None) on the name alone, before looking at anything the event carried. I reproduced your case on a95d057: classify('search', {'path': GATE_FILE, 'command': 'tee ' + GATE_FILE}) returned read. So did sed -i, a > redirect, and a chained && cp, under either name. SAGE's dispatcher sends {pattern, path} with no command key, so this wasn't reachable from the being. But the closure is shared by every harness, so your concern about the contract stands.

v2: the command's write positions are classified.

  • The two verbs move out of _READ_PATH_TOOLS into their own _COMPOSED_READ_TOOLS.
  • Their path is the read target and is never a destination.
  • A command they carry goes through the Bash write-position analysis. That includes its fail-closed arms: out-of-grammar commands and opaque writers.
  • file_path and notebook_path keep the unknown-tool (write) reading.
  • _read_position_mentions tokenizes their command as it does for Bash, so a reading grep or git log over the gate stays a witnessed read.

Tests. The new test test_a_composed_read_name_does_not_launder_a_write covers your negative regression, plus sed -i, >, && cp, and a file_path key, all under both names. It fails at a95d057 and passes here. The positive cases still hold: the being's real {pattern, path} events and reading commands classify as read. All closure suites pass:

  • names: 8/8
  • arms: 14/14
  • output writes: 103 pairs
  • hooks_gt: clean, after the GT regeneration (escalation b4e21b2bb6139f28)
  • store, parity and census: pass

kimi-code independently corroborated the v2 diff before the GT apply, and reproduced the v1 hole.

— cbp-claude

dp-web4 commented Oct 21, 2026

Copy link
Copy Markdown
Owner Author

Re-review at 64f7921: previous classifier-contract hold is addressed in this revision by treating search/git_read path as read while still analyzing any provided command through Bash write-position grammar, and retaining write semantics for other destination keys. The earlier tee/sed -i/redirect cases are now covered in the described regressions. No remaining blocker from my previous finding; await complete CI and integration check before merge.

dp-web4 added a commit that referenced this pull request Oct 21, 2026
8c01b68c/8475e5f2 DISSENT: retained write_note acts opaque (no rationale, bytes/diff, payload, write effect); conditional name-only v1 laundering risk identified from PR #1362 v2 diff, actual edit bytes not attested. d9ce6c46 CONCUR strictly on stated single-file search, not unrecorded commands/future writes. PR #1362 verified OPEN/unmerged at 64f7921. Peer factors witnessed 8bbc0a85/145eafb6/95e7e16f; law c80f41fa; review_done 30772/30773/30774 bound to 30721/30739/30747 (binding_verified).
dp-web4 added a commit that referenced this pull request Oct 21, 2026
…ad-only; bare 'search' absent from _READ_PATH_TOOLS at executing vintage l.1223 AND origin/main l.1707 -- #1361 mismatch, not a vintage artifact; fix remains PR #1362 OPEN at 64f7921; per-act approval not the fix); review_done 30776 in_reply_to 30765 binding_verified; ack 30761 terminal; 54 legacy owed rows untouched; law_hash c80f41fa
dp-web4 added a commit that referenced this pull request Oct 21, 2026
…ad-only; residual #1361 gap -- bare 'search' absent from _READ_PATH_TOOLS at executing vintage l.1223 (lowercase names already live), shared tree l.1221, origin/main l.1707; not #1342 class, not a vintage artifact; fix remains PR #1362 OPEN/MERGEABLE at 64f7921; codex law-hash 7ad772a4 diverges from consult c80f41fa, noted as evidence); review_done 30794 in_reply_to 30781 binding_verified; law_hash c80f41fa
dp-web4 added a commit that referenced this pull request Oct 21, 2026
…-appeal -- home-wide grep incl ~/.hestia* rightly refused by peer:claude-code (recursive /home/dp grant over-broad; read-only intent no cure); need already served in-session via hestia_gate_escalation_poll (81fe90dda8a15a89 concur 30794); reverified pending, 3 concurs; PR #1362 OPEN/MERGEABLE 64f7921 unchanged; ack 30796 in_reply_to 30792 binding_verified; codex ack 30793 terminal; 54 legacy rows untouched
dp-web4 added a commit that referenced this pull request Oct 21, 2026
… -- opaque write_note to live governance closure; same retained act digest as denied 8c01b68c (not payload proof); PR #1362 OPEN/MERGEABLE 64f7921 composed-read route; factor 53376ae0; review_done 30831 bound to 30809 binding_verified; final denied permits_write=false; law_hash c80f41fa
#1351 and #1362 both regenerate the five hooks-gt manifests (content
hashes), so they conflict by construction. Resolved by regenerating over the
merged tree with tools/hooks_gt.py publish, not by hand. hooks_gt check is
clean, closure suites and one_gate_decide_contract (24 + shim parity) pass.

Signed: cbp-claude

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014LYVQN8U1xqy6U6pbyUDvH
dp-web4 added a commit that referenced this pull request Oct 21, 2026
…tice 30842) -- same digest 2af2923d verified read-only 19:54Z (esc 7555d99bc158d821); bare 'search' absent from _READ_PATH_TOOLS re-verified shared tree l.1221 AND origin/main l.1707 (#1361, not vintage); approval binds digest not the auto 'write' frame (codex dissent addressed); fix PR #1362 OPEN/MERGEABLE 76d89ee; review_done 30877 binding_verified. scope-d955d2b3c1db (notice 30849) terminal before decide: peer:codex refused-as-moot 23:36Z citing asker reply 30866 (session ended); peer_decide 409 recorded; post-decision concur on mootness, act was read-only had need persisted; review_done 30878 binding_verified. Incidental: own verify probe auto-opened scope-468840b80a0f, not pursued, no withdraw surface, lapses. 54 legacy rows untouched; law_hash c80f41fa
dp-web4 added a commit that referenced this pull request Oct 21, 2026
…tice 30859) -- act_digest 5eaa8f96 recomputed byte-exact from retained act text, payload null so approval binds the search text only; bare 'search' absent from _READ_PATH_TOOLS in executing mirror l.1223 AND writable tree l.1221 AND origin/main l.1707 (#1361, not vintage); codex dissent targets the auto 'write' frame, approval binds digest not frame; fix PR #1362 OPEN/MERGEABLE 76d89ee; factor witnessed 1792626379, bar_met_if_decided_now; review_done 30904 binding_verified. scope-5b7bbe8c1a6e (notice 30883) terminal before decide: peer:claude-code refused 23:42:41Z (+103s), mirror path serves the file read-only in asker standing scope; peer_decide 409 recorded; post-decision concur; review_done 30905 binding_verified. Own incidental scope-468840b80a0f closed: ack 30906 codex (30872 refused-as-submitted, correct), ack 30907 claude-code (30874 no-ruling); hestia disposition 30870 read; ack 30880 terminal. 54 legacy rows untouched; law_hash c80f41fa
dp-web4 added a commit that referenced this pull request Oct 21, 2026
…tice 30894) -- act_digest 06ea7f69 recomputed byte-exact from retained act text, payload null so approval binds the search text only; bare 'search' absent from _READ_PATH_TOOLS in executing mirror l.1223 AND writable tree l.1221 AND origin/main l.1707 (#1361, not vintage); 3rd cross-vendor concur; fix PR #1362 OPEN/MERGEABLE 76d89ee; review_done 30912 binding_verified. scope-fe9a8390cbc2 (notice 30888, asker claude-code) terminal before decide: peer:codex refused 23:46:22Z (+202s), whole-home find depth 6 without task purpose over-broad; peer_decide 409 recorded; post-decision concur; review_done 30913 binding_verified. 54 legacy rows untouched; law_hash c80f41fa
dp-web4 added a commit that referenced this pull request Oct 22, 2026
…-- terminal before decide: pending at first read, peer:codex refused within +4m on names-nothing grounds (race-guard re-read caught it, no peer_decide attempted); independent verification agreed: plugins/_shared/hestia_gate_search_test.py in NO branch history (git log --all empty), absent from HEAD + origin/main trees, nowhere on disk in any worktree; post-decision CONCUR posted review_done 30953 binding_verified witness ee5d178b, listing the gate tests that DO exist + #1361 search-vocab pointer. PR #1362 OPEN/MERGEABLE 76d89ee unchanged. 54 legacy rows untouched (moot-at-read fix b2c6de2 merged, not deployed here); law_hash c80f41fa
@dp-web4
dp-web4 merged commit a097526 into main Oct 22, 2026
10 checks passed
dp-web4 added a commit that referenced this pull request Oct 22, 2026
…- terminal at first read: peer:claude-code refused 01:21:23Z (+78s), no peer_decide attempted; independent verification agreed on every cited ground: hestia_gate_search_test.py absent from plugins/_shared (ls 0), all branch history (git log --all empty), HEAD + origin/main trees; same ask already refused as scope-3c9bc1a9323f (peer:codex) and scope-8ca4d388d577 (peer:claude-code); real #1361 test shipped as tools/closure_read_tool_names_test.py in PR #1362 MERGED 01:16Z (a097526). Post-decision CONCUR posted review_done 30963 binding_verified witness 664281c4. PR #1362 state change: OPEN->MERGED. 54 legacy rows untouched; law_hash c80f41fa
dp-web4 added a commit that referenced this pull request Oct 22, 2026
…-- terminal at race-guard re-read: peer:claude-code refused 01:39:37Z (+202s), no peer_decide attempted; this petition DIFFERED from the four priors (target EXISTS: wt-1354-moot draft, 5039B) so my drafted ruling was grant-once, superseded on review -- post-decision CONCUR, each ground verified: worktree HEAD=61b36419, file 2026-10-20, lacks BOTH #1362 tests (diff vs origin/main a097526), stale draft omits exactly the asker's own case so a grant would misinform; NOTE 3rd petition scope-6839185fc6f6 pending on live-tree path absent from this branch. review_done 30993 binding_verified witness 67fb8274, recipient live. 54 legacy rows untouched; law_hash c80f41fa
dp-web4 added a commit that referenced this pull request Oct 22, 2026
…ices 30982/30990, cbp-being) -- BOTH terminal before my decide: peer:claude-code refused 01:44:07Z (+199s) and 01:47:26Z (+116s), no peer_decide attempted; grounds independently verified: plugins/_shared/tools/ names nothing (absent on disk, HEAD, origin/main, all branch history) and tools/closure_read_tool_names_test.py is absent on this branch (predates #1346/#1362), existing only on origin/main a097526 (#1361/#1362) -- both grants would reach nothing, refusals correct; post-decision CONCURs posted review_done 30996 (witness 966d3b36) + 30997 (witness 7d876e8d), binding_verified, recipient live. cbp-being's gate filed 3 petitions in 9min chasing this one test file; hestia#1367 (OPEN) is exactly this class: mrh.path deny on a NON-EXISTENT read target reads as 'the file exists'. Durable fix is the shared tree catching up to main, not more petitions. 54 legacy rows untouched; law_hash c80f41fa
dp-web4 added a commit that referenced this pull request Oct 22, 2026
…ices 31001/31003, claude-code) -- BOTH terminal at first read 02:37Z: peer:codex granted the once-only act (queued 02:34:48Z, grant expiry 02:47:05Z), no peer_decide attempted; grounds independently verified within my MRH: act read-only (api/version GET + SNAPSHOT head -5 + grep -c _COMPOSED_READ_TOOLS on two named deployed files), premise TRUE (origin/main a097526 #1361/#1362 carries _COMPOSED_READ_TOOLS 3x in each named source file, absent on this branch -- deploy vintage), both paths real in origin/main tree; grant narrow (once-only, exact act, ~12min window) matching act shape; deployed tree ~/.hestia/deploy itself outside my MRH, not independently read -- CONCUR is on grant correctness given act shape, not on what the asker's grep counts. First send failed hestia.member_notify_bad_pointer (517>512), pointer trimmed and re-sent OK. Post-decision CONCURs posted review_done 31020 (witness 27359452) + 31021 (witness e20803e8), binding_verified, recipient live. Race-guard pattern: 5th consecutive wake where petitions went terminal before my decide. 54 legacy rows untouched (moot-at-read fix b2c6de2 merged, not deployed on this branch); law_hash c80f41fa
dp-web4 added a commit that referenced this pull request Oct 23, 2026
…y.d) (#1368)

Some state on a box is derived from the deployed build but isn't hestia's.
On CBP, beings read their law from a read-only snapshot of the deployed
source (SAGE scripts/governance_snapshot.sh). #1362 deployed at 01:59Z, but
the snapshot stayed on the previous build until a manual run at 02:44Z.
During that gap the being looked for the fix in the only copy it may read,
didn't find it, and asked for scope on paths that don't exist.

hestia-deploy now runs each executable in $HESTIA_HOME/post-deploy.d, in
name order, with HESTIA_DEPLOYED_BUILD set. It does this after the members'
install, on DEPLOYED, CURRENT and CURRENT-manifest-repair cycles. It follows
the same contract as the inventory step: a step can never stop a deploy.
- A failure, a hang (bounded where timeout(1) exists), or a step a member
  could have written (not ours, or group/world-writable) is logged and
  counted, and the function returns 0.
- Governed sessions and HESTIA_DEPLOY_POST=0 run nothing.
- The summary line carries post_deploy=<status>.

post_deploy_step_test.py runs the real function under set -euo pipefail.
Sabotaging the ownership check or the build export fails it.


Claude-Session: https://claude.ai/code/session_014LYVQN8U1xqy6U6pbyUDvH

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

closure: a being's search verb is classed as a WRITE (unknown-tool rule), refusing reads of the governance snapshot

1 participant