Skip to content

fix(guards): anchor the production root on the OS account home, not $HOME (t_c64b9d44) - #865

Closed
Kyzcreig wants to merge 1 commit into
daedalus-opus/t_2f909ab6-live-board-gatefrom
daedalus/t_c64b9d44-production-root-anchor
Closed

Kyzcreig wants to merge 1 commit into
daedalus-opus/t_2f909ab6-live-board-gatefrom
daedalus/t_c64b9d44-production-root-anchor

Conversation

@Kyzcreig

@Kyzcreig Kyzcreig commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes the FALSE POSITIVE in #853's connect() gate. Found by argus reviewing
#858; the defect is #853's, so it lands on #853's branch and #858 inherits it.

The bug

Both live-system guards decide "is this production?" from
hermes_state._real_platform_state_root(), which read os.path.expanduser("~")
— on POSIX, just $HOME.

But monkeypatch.setenv("HOME", tmp_path) is THE hermetic-isolation idiom in
this repo (51 test files use it). Under that idiom the test's own tmpdir IS the
platform-default production root for the process, so a hermetic board at
<tmp>/.hermes/kanban.db IS the live board — and the gate refused it.

Measured red, 2 files / 24 tests:

file before
tests/gateway/test_kanban_dispatcher_standby.py 21 errors, all LiveBoardWriteRefused at setup
tests/tools/test_execute_code_session_provenance.py 3 failed (child process exits 1 on the same guard)

The fix — 4 functional lines

Anchor on the passwd entry instead of $HOME. It is a property of the UID,
so it does not move when a process rewrites $HOME — which is exactly the
discriminator between "the operator's real root" and "a test redirected HOME
here".

This is a structural discriminator, not a per-test marker. A
@pytest.mark.live_system_guard_bypass equivalent was the obvious seam (the
state.db guard has one) but it is a remember-to-do-it mitigation — the same
shape as the opt-in HERMES_KANBAN_SANDBOX=1 that already failed twice
(2026-08-08, 2026-09-21). A guard that fires on the standard isolation idiom
teaches people to disarm it globally; keeping it precise is what keeps it armed.

The old docstring's actual requirement — not reachable through a monkeypatchable
Path.home() — is preserved. $HOME only ever aliased the passwd entry when
nothing had redirected it.

Falls back to expanduser when pwd is unavailable (Windows) or names a
non-existent dir (service accounts, some container images): a bogus anchor would
point the deny-list at a root nothing resolves to and disarm the guard silently.

Fixed at the shared definition, so both members of the class get it — the
kanban board gate and hermes_state's state.db guard read the same function.

Verification

acceptance result
1. standby 21 passed (was 21 errors)
2. provenance 8 passed (was 3 failed)
3. gate still refuses the real leak two-door control on the #858 stack: 6/6 live doors REFUSED, effect-free (no .init.lock, no .bak, no ghost db), non-live scratch control still ALLOWED
4. fleet-brick ALLOWED against the real ~/.hermes/kanban.db from a live worker env — production not bricked
5. measured FP trip set 2 files → 0 (harness re-run over all 10 $HOME-redirecting kanban files, not grepped)
6. mutation-proof 4/4 KILLED
7. test_kanban*.py 1019 passed / 5 failed / 3 skipped — byte-identical failure set to the base tree

Acceptance 3 note: argus's argus_two_door.py declares its synthetic live board
production by faking $HOME — the exact classification this PR removes, so it
dies on its own seed assert. Rather than weaken its assertions I re-ran the same
six doors with the root declared through
hermes_state._STATE_DB_GUARD_EXTRA_DENY_ROOTS, the documented injection point
tests/conftest.py and the guard's own test suite already use.

Acceptance 7 note: the card predicted 1023 passed. The base tree measures the
same 1019 / 5 / 3
— that count was taken against a different tree state, not
regressed by this PR. The failure sets diff clean, so the delta this change
causes is zero.

Mutation battery (each a different way to get the anchor wrong):

M1  revert to os.path.expanduser("~")   -> 8 failed, 21 errors   KILLED
M2  anchor on Path.home()               -> 5 failed, 24 errors   KILLED
M3  account home returns $HOME          -> 9 failed, 21 errors   KILLED
M4  account home always None            -> 8 failed, 21 errors   KILLED
baseline before/after                   -> 37 passed

M4 matters on its own: it proves the fallback is gated, not load-bearing by
accident.

New tests (tests/hermes_state/test_production_root_anchor.py, 8 tests) pin
both directions. A test that only pinned "the tmpdir isn't production" would
pass just as well if the guard went blind entirely — the failure mode that is
far worse than the false positive — so the real account root is pinned as
still-production, including while $HOME is redirected.

Card t_c64b9d44.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…HOME

The live-system guards decide "is this production?" from
_real_platform_state_root(), which read os.path.expanduser("~") — on POSIX
just $HOME. But monkeypatch.setenv("HOME", tmp_path) is THE hermetic
isolation idiom here (51 test files). So under that idiom a test's own
tmpdir WAS the production root, and a hermetic board at
<tmp>/.hermes/kanban.db WAS the live board: #853's connect() gate refused
it.

Measured red, 2 files / 24 tests:
  tests/gateway/test_kanban_dispatcher_standby.py      21 errors
  tests/tools/test_execute_code_session_provenance.py   3 failed (child proc)

Anchor on the passwd entry instead. It is a property of the UID, so it does
not move when a process rewrites $HOME — which is exactly the discriminator
between "the operator's real root" and "a test redirected HOME here". The
old docstring's requirement (not reachable via a monkeypatchable Path.home)
is preserved; $HOME only ever aliased the passwd entry when nothing had
redirected it. Falls back to expanduser when pwd is unavailable (Windows) or
names a non-existent dir, because a bogus anchor would point the deny-list
at a root nothing resolves to and disarm the guard silently.

Fixed at the shared definition, so both members of the class get it: the
kanban board gate and hermes_state's state.db guard read the same function.

Verified:
  standby 21 passed (was 21 errors); provenance 8 passed (was 3 failed)
  measured FP trip set: 2 files -> 0 (harness re-run, not grepped)
  two-door control on the #858 stack: 6/6 live doors REFUSED, effect-free
    (no .init.lock, no .bak, no ghost db), non-live control still ALLOWED
  real ~/.hermes still refused from a test context, incl. under a
    redirected $HOME; fleet-brick check ALLOWED (production not bricked)
  mutation battery 4/4 KILLED (revert to expanduser; Path.home(); account
    home = $HOME; account home always None)
  tests/hermes_state/ + kanban guard suite: 294 passed, 1 skipped

Card t_c64b9d44. Found by argus reviewing #858; the defect is #853's.
@Kyzcreig

Copy link
Copy Markdown
Collaborator Author

FleetReview

Reviewed with 2 of 3 model families — openai unavailable.

Confidence: 3/5

Findings

  • P1 hermes_state.py:618 — Production anchor now disagrees with how the app actually resolves its home ($HOME), so the guard goes blind when HOME != passwd pw_dir
  • P2 hermes_state.py:582 — Uncached NSS lookup (pwd.getpwuid) + stat on every DB connect, including the dispatcher's tick loop
  • P2 tests/hermes_state/test_production_root_anchor.py:96 — Tests assume home
  • P2 tests/hermes_state/test_production_root_anchor.py:43 — _account_home() skips the implementation's isdir fallback, so 6 of 8 tests fail on exactly the hosts the fallback exists for

FleetReview provenance · models: C=claude-code-opus-5, D=grok-4.6, G=grok-4.6 · cost: $5.87 · duration: 22m 36s · rounds: 1 · files examined: 2

Kyzcreig added a commit that referenced this pull request Sep 22, 2026
fork/main landed multi-repository operator survivors (#865 family): both
claim flags became `action="append"` and each value may carry a
`<workspace-relative-repo>=` qualifier, and `_verified_explicit` changed
shape from "return one ref" to "return {repository-key-or-None: ref}".
This branch had changed the SAME function to bind each claim to the card.
Neither side is droppable, so both are kept:

  * `_verified_explicit(task_id, ...)` keeps the per-claim loop and the
    qualifier split from fork/main, and applies the binding, the
    RemoteUnavailable split and the unbound/weak-corroboration recording
    to EACH claim before it is filed under its repository key.
  * `_external` keeps fork/main's multi-ref explicit branch and this
    branch's `_reusable(previous)` gate on the cleanup path -- an unbound
    claim still never becomes standing delete authority.
  * `complete_task` takes `Sequence[str]` claims AND `survivor_unbound`.
  * The `--survivor-ref`/`--survivor-pr` help carries both contracts: the
    task-naming requirement and the repeatable `<repo>=` qualifier.

Test-side: fork/main added 17 happy-path tests driving `survivor_pr=PR`
against the `remote` fixture, whose `headRefName` is UNRELATED by design
so this file's refusal tests keep their teeth. Under the binding those
claims are correctly refused, so each now calls the existing
`names_card(remote, tid)` helper -- the same declawing r2 applied to the
three pre-existing happy paths. No production behaviour is relaxed for
them; the refusal tests are untouched. `test_the_help_documents_the_task_naming_requirement`
tracks the new `[REPO=]` metavars.

Verified (python3.14, PYTEST_ADDOPTS cleared, private --basetemp, serial):
  226 passed -- survivor + external-survivor + binding + stale-bases +
  authority + tools/test_kanban_tool_survivor + pr-gate

Refs: kanban t_de2e348e.
@Kyzcreig

Copy link
Copy Markdown
Collaborator Author

Landed by content on main via stack tip #874 (this head is an ancestor of #874's merged tip). Closing the ghost PR — apollo/fr-pause-0922.

@Kyzcreig Kyzcreig closed this Sep 23, 2026
@Kyzcreig

Copy link
Copy Markdown
Collaborator Author

Reopened — closed one step early: #874 is still in the merge queue, not merged. Will close by content once it lands. — apollo

@Kyzcreig Kyzcreig reopened this Sep 23, 2026
@Kyzcreig

Copy link
Copy Markdown
Collaborator Author

Landed by content on main via stack tip #874 (merged 2026-09-23T03:27:59Z; this head is an ancestor of it). Closing the ghost PR — apollo/fr-pause-0922.

@Kyzcreig Kyzcreig closed this Sep 23, 2026
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.

1 participant