fix(apodex): prevent saved Bash allow from bypassing typed confirm via substitution - #42
Conversation
…a substitution Root cause: PermissionStore._matches compared raw segments, ignoring unquoted command substitution, and assess_with_rules dropped the danger label on downgrade. Validation: repro script confirms git push --force and echo pip install payloads stay confirm with danger; 5 permission tests pass; ruff and pyright clean. Closes ApodexAI#39
|
Hey @dq-ai-dev just put this one up, would love your eyes on it when you get a sec! This was a sneaky one: a saved I tightened both layers nested shell now needs its own allow, and anything with a Happy to tweak the approach if you'd rather handle the nested case differently! |
Summary
PermissionStore._matchesagainst unquoted$(...)/backtick substitution: nested payloads must independently match a savedBash(...)prefix, otherwise the command is not authorized.assess_with_rulesfrom downgradingCONFIRMtoSAFEwhen the call carries adangerlabel, so dep-installs and force-pushes still require typedyes.echo $(pip install x), backticks,git push --force, and single-quoted literals.Validation
git push --forcestaysconfirmwithdanger='git force-push';echo $(pip install evil-pkg)staysconfirmwithdanger='installs dependencies'; single-quoted stays allowed.uv run pytest apodex/tests/test_features.py -k "saved_allow or single_quoted or permission_store or assess_with_rules": 5 passed.test_features.py: 84 passed, 4 failed — identical 4 fail on unmodifiedmain(Windows path expectations), no new failures.uv run ruff check apodex/permissions.py apodex/agent_tools.py apodex/tests/test_features.py: passes.uv run pyright apodex/permissions.py apodex/agent_tools.py: 0 errors.uv run python tools/import_smoke.py --stage 1: 288/289 (onlyfcntlmissing on Windows, pre-existing).Root cause
_matchescompared raw&&/|/;segments by prefix, never unwrapping substitution, soecho $(pip install x)looked like "just an echo".assess_with_rulesreturnedSAFEwith thedangerlabel dropped. Sinceobservers.pyskipsconfirm()forSAFE, the typed gate never fired. The hard denylist did not help because both repros areconfirm, notdeny.Closes #39