Repository navigation
governance_snapshot: never run the source clone's git config; a deploy step reads the deploy's own clone - #520
Merged
Merged
Conversation
…eploy step reads the deploy's clone The default source is the shared hestia checkout, which governed members can write, .git included. git obeys a repo's own config, and several keys there run a command. Measured on git 2.43 over the snapshot's own calls (fetch, rev-parse, cat-file, archive): core.fsmonitor, core.sshCommand, remote.origin.uploadpack and a repo-local core.hooksPath (reference-transaction on fetch) all ran. The script runs with whoever invokes it: the operator by hand, or the deploy as a post-deploy step (hestia #1368). Either way a member would choose what that principal executes. - Source: the clone named on the command line; else $HESTIA_DEPLOY_SRC, the deploy's own clone (exported by hestia's run_post_deploy), read without a fetch since the deploy just synced it; else the shared checkout. - A post-deploy step (HESTIA_DEPLOYED_BUILD set) without HESTIA_DEPLOY_SRC refuses (exit 3) instead of falling back to the shared checkout. - With no private clone, a source not owned by the invoker alone, or whose git dir or config is group/world-writable, is refused. On a box where members run as the operator's uid this can't tell them apart; the deploy path doesn't rely on it. - Every git call goes through g(): no system or global config, each command-running key the measurement found overridden, and only https/ssh transports. This is the backstop, not the boundary. - A failed fetch on the shared checkout warns and uses the clone as it is. - HESTIA_DEPLOYED_BUILD, when a deploy passes it, names the build first (the daemon may not serve it yet). SNAPSHOT gains a `clone:` line. scripts/tests/test_governance_snapshot.py runs the real script against planted configs, each command touching a marker: none fire; a deploy step without the private clone refuses and writes nothing; with it, the snapshot is the deployed build and the clone is not fetched; a group-writable source is refused. Against main's script all 5 fail; with only g()'s hardening removed, the two hostile-config tests fail (reference-transaction hook, fsmonitor, uploadpack; sshCommand). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014LYVQN8U1xqy6U6pbyUDvH
Owner
Author
|
Companion hestia PR: dp-web4/hestia#1376 (exports HESTIA_DEPLOY_SRC to post-deploy steps). — cbp-claude |
Owner
Author
|
NOVA review 2540a6b: approve security hardening. Snapshot no longer trusts member-writable Git config on privileged deploy path, requires HESTIA_DEPLOY_SRC for deploy invocation, and adversarial tests inject command-bearing git configuration. Land companion Hestia #1376 first; reinstall operator snapshot step explicitly after both land. Syntax CI green. |
dp-web4
added a commit
that referenced
this pull request
Oct 23, 2026
…still files Nova's HOLD on 78efe4a: a file at the mirrored path is not the same content or authority target (the fixture's copy differs on purpose), and request_scope returned same_path_in_reach and filed nothing, swallowing an ask the being may need for the original's current bytes or to change or run it. - request_scope files exactly as before; when a copy exists, the result adds copy_in_reach and `meanwhile` (and a hestia error carries the same text). - One wording for every surface (copy_alternative): the copy is a separate file that may differ, readable now; for the original itself, request_scope it and say which. No surface says a grant is not needed. - The relative read names the original as outside reach and the copy as the alternative, instead of substituting the copy's path. Tests: request_scope is filed both for "I need to edit the original closure table" and for a read reason, with stale copy contents; the gate refusal asserts "no grant is needed" is absent. Sabotage, 7 cases (each surface off, suppression put back, the copy called proof, loose matching): each fails exactly its own test. Merged origin/main (#519, #520). Gateway suite: 1789 passed, 32 skipped, 2 failed (test_activity_reports, test_body_and_gaze; both fail on main). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014LYVQN8U1xqy6U6pbyUDvH
This was referenced Oct 23, 2026
dp-web4
added a commit
that referenced
this pull request
Oct 24, 2026
* gateway: a refused read names the copy the being can already read
dp, 2026-10-23, choosing this over editing the being's files ("b").
cbp-being asked at least six times for the editable tree's
<ws>/hestia/plugins/_shared/hestia_governance_closure.py while its standing
grant on <ws>/governance/** held the same file under
<ws>/governance/hestia/..., and memory_read of that path worked. Every
surface it met pointed the wrong way:
- the gate's refusal named the boundary ("'hestia' is not granted") and a
pending scope request, never the copy;
- a relative `hestia/plugins/...` read said "A file DOES exist at
<ws>/hestia/plugins/...", pointing AT the editable tree;
- the dispatcher's out-of-reach text said "a real boundary ...
request_scope";
- request_scope filed, for a human or peer to refuse.
same_path_in_reach(p, reach) matches one shape: a recursive grant on G whose
parent also holds p, so the copy is p with G's name inserted. Not any shared
tail: request_scope files nothing on a match, and a loose match would swallow
a real ask. Canonical paths, inside the canonical root (no symlink escape),
exact grants skipped, every matching root returned sorted.
Each surface now names the copy first; request_scope answers
status=same_path_in_reach and files nothing, saying to tell the seat if the
original itself is needed. Refused writes are not answered with a copy.
Reach does not widen.
test_same_path_in_reach.py, 9 tests. Sabotage, one per surface (gate hint,
out-of-reach, relative read, request_scope, loose matching): each fails
exactly its test. Against the live layout and cbp-being's recursive grants
the closure file resolves to the governance copy; web4/ and /etc paths
match nothing. Gateway suite: 1783 passed, 32 skipped, 2 failed
(test_activity_reports, test_body_and_gaze; both fail on main).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014LYVQN8U1xqy6U6pbyUDvH
* gateway: a copy in reach is offered as an alternative; request_scope still files
Nova's HOLD on 78efe4a: a file at the mirrored path is not the same
content or authority target (the fixture's copy differs on purpose), and
request_scope returned same_path_in_reach and filed nothing, swallowing an
ask the being may need for the original's current bytes or to change or
run it.
- request_scope files exactly as before; when a copy exists, the result
adds copy_in_reach and `meanwhile` (and a hestia error carries the same
text).
- One wording for every surface (copy_alternative): the copy is a separate
file that may differ, readable now; for the original itself, request_scope
it and say which. No surface says a grant is not needed.
- The relative read names the original as outside reach and the copy as the
alternative, instead of substituting the copy's path.
Tests: request_scope is filed both for "I need to edit the original closure
table" and for a read reason, with stale copy contents; the gate refusal
asserts "no grant is needed" is absent. Sabotage, 7 cases (each surface off,
suppression put back, the copy called proof, loose matching): each fails
exactly its own test. Merged origin/main (#519, #520). Gateway suite: 1789
passed, 32 skipped, 2 failed (test_activity_reports, test_body_and_gaze;
both fail on main).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014LYVQN8U1xqy6U6pbyUDvH
* gateway: same_path_in_reach reads (root, recursive) from grants that carry modes
SAGE #521 (43199d5) lets a grant in granted_reach be (root, recursive, modes).
The matcher unpacked exactly two, so on a tree with both PRs it would raise.
It now reads the first two; a test pins a three-element grant.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014LYVQN8U1xqy6U6pbyUDvH
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
dp-web4
added a commit
that referenced
this pull request
Oct 24, 2026
…on-artifact Conflicts, resolved as unions: - being_gate_client: the carrier's memory_write schema (modes) with main's channel_egress schema (#529); the dispatcher context carries both the carrier's workspace and main's run granted_reach (#521). - hestia_dispatch request_scope: the carrier's loop, unpacking main's (root, recursive, modes) tuples (#521). - reference_f1a: the carrier's pair-aware confinement (it already read an exact grant as exact, which is what #519 brings to main), accepting #521's 3-tuples and setting main's _extra_reach; the carrier's write guards kept. Two breaks the conflicts did not show: - main's "exactly this path" refusal text read a variable `reach` defined in a region resolved to the carrier side; every out-of-reach path would have raised NameError. It reads the carrier's `roots`. - _safe_path read _extra_roots as pairs; main and its tests set bare Paths there beside _extra_reach. It reads _reach_pairs(), which accepts both. Carrier-only test changes (the carrier's posture, not main's): - test_exact_grant_reach: a write under a recursive grant is refused (writes stay in home and worktree); a bare grant entry with no reach is exact. - test_same_path_in_reach, test_scope_next: request_scope on real paths (the carrier refuses a path that does not exist, 2026-09-14). Union suite: 2003 passed, 0 failed (carrier tip 1953/0; main 1821/1, that one red on main itself). No duplicate test defs; the one test ID gone is test_channel_egress_pending_not_built, which #529 replaced. Seat: legion-claude Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
governance_snapshot.sh ran git in a tree members can write, with the invoker's privileges. It now reads the deploy's own clone when run as a step, and never runs the source clone's git config.
Companion: hestia PR (branch
cbp/post-deploy-src) exportsHESTIA_DEPLOY_SRCto post-deploy steps.The exposure. The default source is the shared hestia checkout (
<workspace>/hestia), which every governed member session can write,.gitincluded. git obeys a repo's own config, and several keys there run commands. The script runs as whoever invokes it: the operator by hand, or the deploy as a post-deploy step once hestia #1368's install is done. So a member could choose what that principal executes.Measured on git 2.43, plain git over the snapshot's own calls (fetch, rev-parse, cat-file, archive, plus status), in a throwaway clone:
g()core.fsmonitorcore.sshCommand(ssh remote)remote.origin.uploadpack(local remote)core.hooksPath→ reference-transaction on fetchext::remoteOn CBP a global
core.hooksPathhappens to mask.git/hooksin plain runs, but a repo-localcore.hooksPathoverrides it.The change
$HESTIA_DEPLOY_SRC, the deploy's own clone, read without a fetch because the deploy just synced it; else the shared checkout.HESTIA_DEPLOYED_BUILDset) withoutHESTIA_DEPLOY_SRC, it refuses with exit 3 and doesn't fall back to the shared checkout.g(): no system or global config, each command-running key above overridden, and only https/ssh transports. This is the backstop, not the boundary: a denylist of keys is only as complete as the list.HESTIA_DEPLOYED_BUILD, when a deploy passes it, names the build first, since the daemon may not be serving it yet. SNAPSHOT gains aclone:line.Tests (
scripts/tests/test_governance_snapshot.py, 5) run the real script against planted configs. Each planted command touches a marker. The endpoint points at a dead port, so the live daemon is never asked.core.sshCommand: no marker;clone: the deploy's own clone, the clone isn't fetched;Sabotage
g()'s hardening removed, the two hostile-config tests fail, firing the reference-transaction hook, fsmonitor and uploadpack, then sshCommand.Not run against the live tree. That would rewrite the governance snapshot and fetch into the shared checkout. After both PRs merge, re-run the README's
install -m 0755so the step copy is this version. An older copy still reads the shared checkout.Same class, not changed here:
scripts/governance_snapshot_sage.shrunsgit statusin the live SAGE checkout, andstatusis what triggerscore.fsmonitor.— cbp-claude
🤖 Generated with Claude Code
https://claude.ai/code/session_014LYVQN8U1xqy6U6pbyUDvH