diff --git a/docs/evidence/framey-tailscale-path-2026-10-26.md b/docs/evidence/framey-tailscale-path-2026-10-26.md new file mode 100644 index 0000000..4e37baf --- /dev/null +++ b/docs/evidence/framey-tailscale-path-2026-10-26.md @@ -0,0 +1,33 @@ +# Tailscale Control read-only backend and Decky PATH, 26 October 2026 + +The audited archive was official Tailscale Control 0.1.3, store ID 43, +SHA-256 `06411711b27101d438339000da3d91202d682862710b02ef3ddf2462e642448a`. +Earlier local Chromium evidence showed its settings panel. This check focused +on the Frame's CLI lookup; it did not run a live Framey panel RPC. + +Each real Frame check acquired `/tmp/frame-test.lock` atomically with its own +random owner, read SteamVR activity and proximity, and released only that +owner's lock. The headset was in standby. The installed Tailscale CLI wrapper +resides in `~/.local/bin`, which the systemd user manager did not put in the +Decky service's inherited `PATH`. + +| Check | Result | +| --- | --- | +| Exact official backend with the current service PATH | Three read-only status methods returned `None`; `tailscale` was not found. | +| Same backend in a disposable process with `~/.local/bin` prepended | The read-only methods returned active state, two exit-node entries and 16 device entries. | +| Bounded transient systemd user unit with the proposed service PATH | Found `tailscale` and completed `tailscale status`. The unit ended inactive, PID 0, `LoadState=not-found`. | + +The Frame Control managed Decky unit now sets `PATH` to `~/.local/bin` plus +standard system directories when installed or updated. The install transaction +rolls its prior unit back on failure. No Tailscale `up`, `down`, routing or +exit-node change was run. No persistent test service, headset wake, controller +input or settings write was used. + +This supports CLI discovery and read-only backend data on the real Frame. It +does not prove the production Framey-to-Decky RPC, connection controls, QR +asset, worn panel visibility or physical controller usability. Tailscale +Control remains **partial** in the digest-scoped catalog. + +A later independent hourly read-only check found SteamVR activity 3 +(standby), proximity raw 3.0625 and battery 93%. It released its owned lock; +no worn test was possible and the hourly schedule remains enabled. diff --git a/docs/framey-compatibility.md b/docs/framey-compatibility.md index dfa65df..ac13094 100644 --- a/docs/framey-compatibility.md +++ b/docs/framey-compatibility.md @@ -59,7 +59,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say | 37: Discord Status 1.5.0 | 1 / 2 | partial | Disconnected panel renders; running-activity construction accepts a synthetic game in local Chromium. Exact official is_connected returned false in real Decky 3.3.0 ARM64. Empty live running-list reads and lifetime registration/cleanup passed. Real game transitions, Discord shortcut detection/connection/activity, local custom artwork and worn VR remain unverified/unsupported. | static archive, local browser, fake-Frame, real Frame SSH | | 38: Volume Mixer 1.2.1-1 | legacy / legacy | works | Per-app stream discovery and slider/keyboard volume changes pass in local Chromium through real Decky 3.3.0 ARM64 on the Frame audio session, with independent Pulse readback of a silent test stream. Bluetooth playback, audible output and worn-VR controls unverified. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH | | 40: Controller Tools 2.0.2 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: ControllerTools/bin/backend. Frontend audit failed: Audit blocks WebSocket. | static archive, local browser, fake-Frame | -| 43: Tailscale Control 0.1.3 | legacy / legacy | partial | Settings render locally after the SuspensefulImage fallback. Tailscale daemon/state, connection and exit-node changes unverified; bundled localhost QR asset remains unavailable. No network settings were changed. | static archive, local browser, fake-Frame | +| 43: Tailscale Control 0.1.3 | legacy / legacy | partial | Settings render locally after the SuspensefulImage fallback. On the real Frame, exact official read-only backend status calls returned live state, exit-node and device entries when ~/.local/bin was in PATH; a transient user unit found the CLI with the proposed managed-service PATH. Live Framey Decky RPC, connection/exit-node changes and worn VR remain unverified; the bundled localhost QR asset is unavailable. No network settings were changed. | static archive, local browser, fake-Frame, real Frame read-only backend | | 44: DeckMTP 1.0.5 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: DeckMTP/bin/umtprd. | static archive, local browser, fake-Frame | | 46: Steamback 1.0.0-1 | legacy / legacy | partial | Panel opens using the real Frame current-user/library snapshot. Account setup and empty save-list reads pass through actual Decky 3.3.0 amd64 in isolation. ARM64 backend, supported-save discovery, automatic backups and restore remain unverified; no backup/restore ran. | static archive, local browser, fake-Frame, real Frame SSH | | 48: Quick Launch 1.2.1 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame | diff --git a/tests/test_frame_decky.py b/tests/test_frame_decky.py index c507c21..841e56d 100644 --- a/tests/test_frame_decky.py +++ b/tests/test_frame_decky.py @@ -76,17 +76,22 @@ class DeckyServiceScript(unittest.TestCase): self.assertEqual((self.base / 'PluginLoader').read_bytes(), first) receipt = self.base / '.frame-control/receipt.json' self.assertEqual(json.loads(receipt.read_text())['version'], 'v3.3.0') - self.assertTrue((Path(self.tmp.name) / '.config/systemd/user/frame-control-decky.service').is_file()) + service = Path(self.tmp.name) / '.config/systemd/user/frame-control-decky.service' + first_unit = service.read_text() + self.assertIn('Environment=PATH='+str(Path(self.tmp.name) / '.local/bin')+ + ':/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\n', first_unit) second = arm_binary(b'B') result = self.apply('update', second, version='v3.3.1', fail=True) self.assertNotEqual(result.returncode, 0) self.assertEqual((self.base / 'PluginLoader').read_bytes(), first) self.assertEqual(json.loads(receipt.read_text())['version'], 'v3.3.0') + self.assertEqual(service.read_text(), first_unit) result = self.apply('update', second, version='v3.3.1') self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual((self.base / 'PluginLoader').read_bytes(), second) + self.assertIn('Environment=PATH=', service.read_text()) result = self.apply('remove') self.assertEqual(result.returncode, 0, result.stderr) self.assertFalse((self.base / 'PluginLoader').exists()) diff --git a/ui/frame_decky.py b/ui/frame_decky.py index d463ddd..cc775a5 100644 --- a/ui/frame_decky.py +++ b/ui/frame_decky.py @@ -123,6 +123,12 @@ try: next_binary.write_bytes(data) os.replace(next_binary, binary) binary.chmod(0o700) + # The Frame's user-installed tools (including its Tailscale CLI) + # live here, while the systemd user manager omits this directory. + # Keep standard system paths for plugins that invoke OS tools. + plugin_path = ':'.join((str(home / '.local/bin'), '/usr/local/sbin', + '/usr/local/bin', '/usr/sbin', '/usr/bin', + '/sbin', '/bin')) text = ('[Unit]\nDescription=Decky Loader (Frame Control experimental user install)\n' 'After=default.target\n[Service]\nType=simple\nRestart=on-failure\n' 'RestartSec=5\nExecStart='+str(binary)+'\n' @@ -130,6 +136,7 @@ try: 'Environment=PRIVILEGED_PATH='+str(base)+'\n' 'Environment=CHOWN_PLUGIN_PATH=0\nEnvironment=LIVE_RELOAD=0\n' 'Environment=USE_CEF_CLOSE_WORKAROUND=0\nEnvironment=KEEP_SYSTEMD_SERVICE=1\n' + 'Environment=PATH='+plugin_path+'\n' '[Install]\nWantedBy=default.target\n') service.write_text(text) receipt.write_text(json.dumps({'managedBy':'Frame Control','version':version,'sha256':digest})+'\n') diff --git a/ui/framey-compatibility.json b/ui/framey-compatibility.json index 953dbb2..975b34e 100644 --- a/ui/framey-compatibility.json +++ b/ui/framey-compatibility.json @@ -3185,12 +3185,13 @@ "https://github.com/tailscale-dev/deck-tailscale" ], "status": "partial", - "reason": "Settings render locally after the SuspensefulImage fallback. Tailscale daemon/state, connection and exit-node changes unverified; bundled localhost QR asset remains unavailable. No network settings were changed.", + "reason": "Settings render locally after the SuspensefulImage fallback. On the real Frame, exact official read-only backend status calls returned live state, exit-node and device entries when ~/.local/bin was in PATH; a transient user unit found the CLI with the proposed managed-service PATH. Live Framey Decky RPC, connection/exit-node changes and worn VR remain unverified; the bundled localhost QR asset is unavailable. No network settings were changed.", "blockInstall": false, "verification": [ "static archive", "local browser", - "fake-Frame" + "fake-Frame", + "real Frame read-only backend" ], "browser": { "id": 43,