From 5a2a4bcbda74c058ef2a447aa71d31637702b1d2 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 3 Oct 2026 19:21:54 +0200 Subject: [PATCH] phone_guard: pin phone_vd.sh record as a lease-free read of the session's own VD Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WXd5qap8DWqZCS9mjh3CzH --- .../phone-vd-record-guard-2026-10-03.json | 29 +++++++++++ ...structure-step0-safety-net-2026-08-23.json | 51 ------------------- python_pkg/phone_guard/tests/test_hook.py | 15 ++++++ 3 files changed, 44 insertions(+), 51 deletions(-) create mode 100644 docs/superpowers/evidence/phone-vd-record-guard-2026-10-03.json delete mode 100644 docs/superpowers/evidence/restructure-step0-safety-net-2026-08-23.json diff --git a/docs/superpowers/evidence/phone-vd-record-guard-2026-10-03.json b/docs/superpowers/evidence/phone-vd-record-guard-2026-10-03.json new file mode 100644 index 00000000..03ac0c68 --- /dev/null +++ b/docs/superpowers/evidence/phone-vd-record-guard-2026-10-03.json @@ -0,0 +1,29 @@ +{ + "intent": "Pin that `phone_vd.sh record start|stop` (new in ~/.claude: real MP4 video of a session's own virtual display via a read-only scrcpy) passes the phone_guard PreToolUse hook as a lease-free read, while the adb input that drives the recorded app still takes that app's lease.", + "scope": [ + "python_pkg/phone_guard/tests/test_hook.py", + "Non-goal: changing phone_guard classification -- the hook only inspects commands containing 'adb', so `phone_vd.sh record` never reached it and needed no change" + ], + "changes": [ + "TestLeases.test_recording_own_display_is_a_read: record start/stop take no lease and are not blocked; `phone_vd.sh record start a.mp4 && adb shell input -d 9 swipe ...` on the session's own display takes exactly pkg:" + ], + "verification": [ + { + "command": "capped.sh python3 -m pytest python_pkg/phone_guard -q --cov-branch --cov-report=term-missing", + "result": "pass", + "evidence": "68 passed; every python_pkg/phone_guard module 100% statements and branches" + }, + { + "command": "phone_vd.sh record start ~/data/claude-scratch/phone-record/vd37_final.mp4; 5 x adb shell input -d 37 swipe; phone_vd.sh record stop (Pixel 6a 23181JEGR08034, VD 37 running com.android.settings)", + "result": "pass", + "evidence": "ffprobe: h264 1080x2400, duration 10.511 s, 307 frames (~29 fps avg, ~35 fps while scrolling); the hook let every call through (the unit test pins that the swipe maps to pkg:; not observed live)" + } + ], + "risks": [ + "A raw `scrcpy --display-id=` still bypasses the guard (no 'adb' in the command); phone_vd.sh only ever records the caller's own display" + ], + "rollback": [ + "git revert this commit; it only adds a test and this evidence file", + "Run python3 -m pytest python_pkg/phone_guard afterwards" + ] +} diff --git a/docs/superpowers/evidence/restructure-step0-safety-net-2026-08-23.json b/docs/superpowers/evidence/restructure-step0-safety-net-2026-08-23.json deleted file mode 100644 index 408afd64..00000000 --- a/docs/superpowers/evidence/restructure-step0-safety-net-2026-08-23.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "intent": "Establish the safety net for the testsAndMisc restructure: a script that inventories every live reference into the repo, and the directory-depth gate written early as a dry run so the flattening work can be sized before any file moves.", - "scope": [ - "meta/scripts/report_live_state.sh (new)", - "meta/scripts/check_directory_depth.sh (new, NOT yet wired into pre-commit)", - "docs/restructure-live-state.md (new)", - "Non-goal: no archives, extractions or moves in this commit", - "Non-goal: the depth gate is deliberately not enforced yet - the tree does not conform" - ], - "changes": [ - "report_live_state.sh enumerates systemd units, /etc config and $HOME symlinks that name an absolute path inside the repo; --check exits 1 while any remain, making it the post-move gate.", - "check_directory_depth.sh enforces a 2-directory cap, modelled on check_file_length.sh (--all/per-file, EXCLUDED_PREFIXES). Exempts third_party/, docs/superpowers/, .github/skills/ by prefix and any lib/ or tests/ path segment, matched as whole segments so libvirt/ and lib.sh are unaffected.", - "docs/restructure-live-state.md records the baseline inventory (6 systemd units, 2 /etc guard-lib targets, 19 symlinks) plus the depth dry-run sizing." - ], - "verification": [ - { - "command": "./meta/scripts/report_live_state.sh", - "result": "pass", - "evidence": "Reported 6 systemd units (media-organizer, dns-blocklist-refresh, endurain-import, kcd2-dice-build, morning-routine, syncyomi-guard), 2 /etc/guard-lib/targets/*.conf plugin paths, and 19 symlinks under ~/.config/i3blocks (13), ~/.local/bin (3), ~/.oh-my-zsh/custom (3)." - }, - { - "command": "./meta/scripts/report_live_state.sh --check; echo $?", - "result": "pass", - "evidence": "Exit code 1 with 'live references into /home/kuhy/src/testsAndMisc remain', which is correct at baseline since nothing has moved yet." - }, - { - "command": "14 hand-built path cases through check_directory_depth.sh", - "result": "pass", - "evidence": "a/b/c.sh=0, a/b/c/d.sh=1, boot_recovery/tests/lib/x.sh=0, focus_policy/tests/test_model.py=0, meta/scripts/lib/trace_stubs.sh=0, meta/scripts/fixtures/mutations/monitor.json=1, third_party+docs/superpowers+.github/skills prefixes=0. False-positive guards: a/b/libvirt/c.sh=1 and a/b/c/lib.sh=1 both correctly NOT exempt." - }, - { - "command": "./meta/scripts/check_directory_depth.sh --all", - "result": "fail (expected - dry run only, gate not wired)", - "evidence": "556 violations at baseline. 349 disappear with the planned archives/extractions; 207 remain as the step D worklist, dominated by linux_configuration/scripts/single_use (~130)." - }, - { - "command": "pre-commit run --files meta/scripts/check_directory_depth.sh meta/scripts/report_live_state.sh docs/restructure-live-state.md", - "result": "pass", - "evidence": "All hooks passed, including shellcheck, the 250-line file cap, and the shell-coverage ratchet." - } - ], - "risks": [ - "report_live_state.sh reads /etc without sudo, so a root-only-readable reference would be missed; every reference found so far is world-readable.", - "The symlink scan is depth-limited to 4 under $HOME, which covers all known locations but would miss a deeper one.", - "check_directory_depth.sh is intentionally not wired into pre-commit yet; wiring it now would block every unrelated commit, exactly as the file-length-cap comment warns." - ], - "rollback": [ - "git revert the commit; all three files are new and nothing sources them yet.", - "Confirm pre-commit run --all-files still passes, since no existing hook or workflow references these scripts." - ] -} diff --git a/python_pkg/phone_guard/tests/test_hook.py b/python_pkg/phone_guard/tests/test_hook.py index b65ffdd1..de838132 100644 --- a/python_pkg/phone_guard/tests/test_hook.py +++ b/python_pkg/phone_guard/tests/test_hook.py @@ -75,6 +75,21 @@ class TestLeases: assert run("adb shell input -d 9 tap 5 5") == 0 assert acquire.call_args.kwargs["scope"] == "pkg:dev.kuhy.todo" + def test_recording_own_display_is_a_read( + self, acquire: MagicMock, isolated_state: Path + ) -> None: + # `phone_vd.sh record` mirrors this session's VD read-only (scrcpy + # --no-control), like `shot`: never blocked, no lease. Only the input + # that drives the recorded app takes that app's lease. + make_display(isolated_state, "S", "claude_1", "9", "dev.kuhy.todo") + assert run("~/.claude/scripts/phone_vd.sh record start out/a.mp4") == 0 + assert run("~/.claude/scripts/phone_vd.sh record stop") == 0 + acquire.assert_not_called() + both = "phone_vd.sh record start a.mp4 && adb shell input -d 9 swipe 1 2 3 4" + assert run(both) == 0 + scopes = [call.kwargs["scope"] for call in acquire.call_args_list] + assert scopes == ["pkg:dev.kuhy.todo"] + def test_another_sessions_display_is_refused( self, acquire: MagicMock,