Tests did monkeypatch.setattr(<facade module>, name) where name is now defined in a
sibling module and the production path reads the sibling's binding. Where production
reads through BOTH bindings the setattr is duplicated onto the defining module (import
added next to the existing alias import); where only the sibling reads it the target is
repointed. Seams whose production readers go through the facade are left alone.
tools/approval.py no longer re-exports sibling names (approval_context/prompt/floors/detection/
human_wait/smart/gateway_wait); it imports only what it uses. Siblings reference sibling-defined
names directly (module-attribute reads on tools.approval_context so patching the defining module
still works); only facade-owned state (_lock, _gateway_queues, _permanent_approved, _denied,
_denial_breaker_addendum, _gateway_notify_cb) is still read back through tools.approval.
approval_detection calls its own _command_detection_variants instead of late-binding through the facade.
Answers "what would the approval system do with this command?" without
executing it, prompting anyone, or persisting anything. Composes the
REAL runtime evaluators from tools/approval.py in the same order as
check_all_command_guards: container-skip gate, hardline blocklist,
sudo-stdin guard, user approvals.deny rules, yolo/mode-off bypass,
permanent command_allowlist, dangerous-pattern detection. Because the
same functions run — including _command_detection_variants's
normalization/de-obfuscation path — an obfuscated command gets exactly
the verdict its plain form would get at runtime, and the output shows
the normalized-variant trace the detectors actually evaluated.
- hermes_cli/approvals_test.py: evaluate_command() + text/JSON output.
Script-friendly exit codes: 0 allow, 1 usage, 2 ask-approval, 3 deny
(hardline / sudo-stdin / user deny rule).
- hermes_cli/subcommands/approvals.py: `test` subparser with --env-type
(default local), --json, and a REMAINDER command (dest command_words —
NOT "command", which main.py's startup path reads as the top-level
subcommand name).
- hermes_cli/approvals_suggest.py: dispatch `test` and mention it in the
bare-`hermes approvals` usage text.
- tests/hermes_cli/test_approvals_test.py: verdict matrix (benign /
hardline / dangerous / user-deny from config / container skip /
mode=off vs hardline), obfuscated==plain verdict parity with
normalized trace, spy proof that the real runtime detectors are the
ones invoked, read-only invariants (nothing executed; prompt and
persistence paths rigged to explode), JSON shape, dispatcher and
parser wiring.
Read-only by construction: only detection/matching functions are
called; the approval gate, prompts, gateway notify, and allowlist
writers are never reached.
Inspired by: Amp `permissions test` (idea-level, proprietary — zero code)