Compare commits

...
Author SHA1 Message Date
Alex Southwell af8c0a533f Merge pull request #404 from saphid/fix/framey-storage-cleaner-delete
fix(framey): make Storage Cleaner deletion bounded and reliable
2026-10-29 02:53:12 +00:00
SAPHID 081ce61adb fix(framey): make Storage Cleaner deletion bounded and reliable 2026-10-29 02:45:45 +00:00
Alex Southwell 7c30f3311b Merge pull request #403 from saphid/test/framey-deck-shelves-native
test(framey): verify Deck Shelves settings in native Decky
2026-10-29 02:37:36 +00:00
SAPHID 81c2e86f20 Merge remote-tracking branch 'origin/main' into test/framey-deck-shelves-native
# Conflicts:
#	tests/decky-runtime/check.py
2026-10-29 02:27:17 +00:00
Alex Southwell ddbaff8f6a Merge pull request #402 from saphid/fix/framey-radiyo-tls
fix(framey): verify RadiYo HTTPS certificates
2026-10-29 02:26:24 +00:00
SAPHID 250f2b9674 test(framey): verify Deck Shelves settings in native Decky 2026-10-29 02:23:57 +00:00
10 changed files with 194 additions and 7 deletions

No files matched your search

@@ -0,0 +1,22 @@
# Deck Shelves 2.0.0 native settings probe, 29 October 2026
The unchanged official archive for store ID 135 has SHA-256
`b566280ad38fd3ecd584b5b46a855f301539eb9a86063834c64b7045f7063f91`.
The earlier [browser evidence](framey-deck-shelves-browser-2026-10-26.md)
proved only local frontend settings rendering and a stubbed Python backend.
`python3 scripts/framey-test-decky-runtime.py --case 135 --output /tmp/framey-deck-shelves-native-20261029.json`
passed with the published Decky 3.3.0 x86_64 loader in a disposable container
with no external network, host devices, session bus or user home. Decky
discovered the official API v1 plugin. Its public `get_settings` returned
disabled defaults; `set_settings` saved one private test shelf; the JSON file
and a fresh `get_settings` agreed. The same shelf ID and title returned after
stopping and restarting the Decky loader. The runner removed its container
and image. No headset lock, real Frame service, Steam library or external
account was used.
This verifies only private backend settings persistence. The plugin's primary
Steam Home shelf injection has no verified target in Framey's isolated panel.
Real Frame ARM64 settings, wearer-visible VR and physical controller input
remain unverified. Deck Shelves stays **partial**; compatibility counts do not
change.
@@ -0,0 +1,27 @@
# Storage Cleaner 1.4.0 managed backend, 29 October 2026
The exact official store archive for ID 50 has SHA-256
`1afa1a6f129c5d419de0e0bf0f6db8d8e50385a5659179346e996031ea372591`.
Its `decky-storage-cleaner/main.py` has SHA-256
`6fd4d012568edae22ba421495455b3c02899155e4c7484f5aa5be6ce9b14dd5f`.
The original `delete_cache` awaits synchronous `shutil.rmtree`: a disposable
fixture was deleted, then the method raised `TypeError`. The managed installer
checks both digests before adapting its private copy. It calls `rmtree`
synchronously and accepts only `shadercache`, `compatdata`, or a numeric app-ID
child under either. It rejects traversal and symlink paths. The official ZIP
is unchanged; an already installed managed copy needs Update.
`python3 scripts/framey-test-decky-runtime.py --case 50 --output /tmp/framey-storage-cleaner-native-20261029.json`
passed on native x86_64 with the published Decky 3.3.0 loader. Its disposable
container had no external network, host devices, session bus, or user home.
Decky discovered the official plugin, returned the private fixture's size
and game-list row, deleted one selected shader-cache app directory, then
deleted the whole private compatdata fixture. Both RPCs completed without
the previous error. The 41 store transaction tests passed, including guard
checks for traversal and symlink paths, install receipt, and remove. The
container and image were removed. No real Frame lock or device was used.
The deletion fixture does not prove real Steam cache cleanup or recovery.
Real Frame ARM64 backend, frontend confirmation flow, worn VR and physical
controller behavior remain unverified. Storage Cleaner stays **partial**;
the compatibility counts do not change.
+2 -2
View File
@@ -72,7 +72,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 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 |
| 49: Decky Cloud Save 1.4.2 | legacy / legacy | broken | The exact official ZIP contains a 60,776,600-byte x86_64 rclone member, above the 30 MiB member limit and unusable on ARM64. Installation remains blocked. | static archive, local browser, fake-Frame |
| 50: Storage Cleaner 1.4.0 | 1 / 2 | partial | Empty-data panel rendered; cleanup was deliberately not run because it can remove local data. | static archive, local browser, fake-Frame |
| 50: Storage Cleaner 1.4.0 | 1 / 2 | partial | The empty-data panel renders locally. Exact managed 1.4.0 installs repair the backend delete-then-TypeError bug and restrict deletion to shadercache/compatdata or numeric app-ID children. Published Decky 3.3.0 x86_64 RPC passes private size/list and selected/whole fixture deletion in a network-isolated container. Real Steam cache deletion, Frame ARM64 backend, frontend confirmation flow and worn VR remain unverified; older copies need Update. | static archive, local browser, fake-Frame, isolated Decky 3.3.0 x86_64 backend |
| 52: Game Theme Music 1.7.1-1 | 1 / 2 | partial | An inert local component lets the exact frontend initialize and render its Settings view in opaque Chromium. Backend get_setting and plugin network were denied in the audit; Steam game-page theme injection has no verified target in Framey. Music search, playback, downloads, settings persistence and worn VR remain unverified. | static archive, local browser, fake-Frame |
| 53: Shotty 0.1.0 | legacy / legacy | partial | The frontend starts with the read-only Frame library snapshot. The exact-digest click.get_app_dir shim lets actual Decky 3.3.0 in isolated amd64 and ARM64 containers discover Shotty and complete empty screenshot RPCs; isolated Python also copied a synthetic screenshot into a temporary Pictures folder. The managed backend on the real Frame, live screenshots/rescue, in-VR panel and physical controller use remain unverified. | static archive, local browser, fake-Frame, real Frame SSH, isolated Python backend, isolated Decky 3.3.0 amd64/ARM64 empty backend RPCs |
| 56: MangoPeel 0.0.7 | 1 / 2 | partial | Initializer succeeds; gamescope/MangoHud actions and backend are unverified. | static archive, local browser, fake-Frame |
@@ -148,7 +148,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 132: SuggestMe 1.5.3 | 1 / 2 | partial | Initializer succeeds; recommendation backend and Steam library data unverified. | static archive, local browser, fake-Frame |
| 133: Non-Steam Badges 0.1.0 | 1 / 2 | partial | Initializer succeeds; Non-Steam game badges have no supported Steam page injection target. | static archive, local browser, fake-Frame |
| 134: Deck Progress Tracker 1.3.6 | 1 / 2 | partial | Initializer succeeds; Steam game/library statistics and page patches unverified. | static archive, local browser, fake-Frame |
| 135: Deck Shelves 2.0.0 | 1 / 1 | partial | The exact chunked frontend renders settings in opaque local Chromium. The official Python backend saved a shelf and returned it after reopen with a stub Decky module and temporary settings directory. Actual Decky RPC, real Frame settings, Steam Home shelf injection and worn VR remain unverified. | static archive, local browser, fake-Frame, isolated Python backend |
| 135: Deck Shelves 2.0.0 | 1 / 1 | partial | The exact chunked frontend renders settings in opaque local Chromium. The official Python backend now passes published Decky 3.3.0 x86_64 discovery, private shelf settings save/readback and persistence after a loader restart in a network-isolated container. Steam Home shelf injection has no verified target in the isolated panel; real Frame ARM64 settings, worn VR and physical controller use remain unverified. | static archive, local browser, fake-Frame, isolated Decky 3.3.0 x86_64 backend |
| 136: Decky-Clipper 1.0.2 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: Decky-Clipper/bin/gstreamer-1.0/libgstproxy.so. | static archive, local browser, fake-Frame |
| 137: Decky Proton Launch 0.9.0 | 1 / 2 | partial | Initializer succeeds; Proton wrapper installation and launch changes were not run. | static archive, local browser, fake-Frame |
| 138: Docky 1.4.13 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
+10
View File
@@ -19,6 +19,16 @@
"shim": "radiyo-verified-tls",
"probe": "radio-local"
},
{
"id": 135,
"shim": null,
"probe": "shelves-settings"
},
{
"id": 50,
"shim": "storage-cleaner-bounded-delete",
"probe": "storage-cleaner"
},
{
"id": 104,
"shim": null,
+46
View File
@@ -350,6 +350,52 @@ def main():
worker.join(timeout=3)
report['result'] = ('discovery, loopback HTTP fetch/redirect and self-signed HTTPS rejection passed; '
'external stations, trusted HTTPS, playback, real Frame and VR unverified')
elif case['probe'] == 'shelves-settings':
# Private configuration only. Never register a Steam Home
# patch, import a user's settings, or touch their library.
defaults = current('get_settings')
assert defaults['enabled'] is False and defaults['shelves'] == [], defaults
shelf = {'id': 'framey-private-shelf', 'title': 'Private Test',
'source': {'type': 'tab', 'tab': 'all'}, 'limit': 4}
assert current('set_settings', {'enabled': False, 'shelves': [shelf]}) is True
saved = current('get_settings')
assert len(saved['shelves']) == 1 and saved['shelves'][0]['title'] == 'Private Test', saved
state = base/'settings'/record['folder']/'settings.json'
assert json.loads(state.read_text())['state']['shelves'][0]['id'] == shelf['id']
os.killpg(backend.pid, signal.SIGTERM)
backend.wait(timeout=8)
backend = subprocess.Popen([str(loader)], env=env, stdout=log, stderr=log,
start_new_session=True)
processes.append(backend)
until_ready(lambda: rpc('loader/get_plugins'), 'restarted Decky discovery')
restored = until_ready(lambda: current('get_settings')['shelves'],
'private shelf after restart')
assert restored[0]['id'] == shelf['id'] and restored[0]['title'] == shelf['title'], restored
report['result'] = ('published Decky discovery, private shelf settings and restart persistence passed; '
'Steam Home injection, real Frame and VR unverified')
elif case['probe'] == 'storage-cleaner':
# The only deletion is an owned marker under this disposable
# container's private Steam fixture, never a real game cache.
apps = home/'.steam/steam/steamapps'
selected = apps/'shadercache/424242'
selected.mkdir(parents=True)
marker = b'Framey private cache fixture\n'
(selected/'marker').write_bytes(marker)
assert current('get_size', 'shadercache/424242') == len(marker)
rows = json.loads(current('list_games_with_temp_data', 'shadercache'))
assert rows == [{'appid': '424242', 'size': len(marker),
'size_readable': f'{len(marker)}.0 B'}], rows
assert current('delete_cache', 'shadercache/424242') is None
assert not selected.exists()
assert apps.joinpath('shadercache').is_dir()
whole = apps/'compatdata'
whole.mkdir()
(whole/'marker').write_bytes(marker)
assert current('get_size', 'compatdata') == len(marker)
assert current('delete_cache', 'compatdata') is None
assert not whole.exists()
report['result'] = ('published Decky discovery, private size/list and selected/whole fixture deletion passed; '
'real Steam caches, Frame and VR unverified')
elif case['probe'] == 'toggle-settings':
def call(method, *values):
result = rpc('loader/call_plugin_method', record['name'], method, *values)
+4
View File
@@ -68,6 +68,10 @@ and redirect, and rejects a self-signed loopback HTTPS endpoint from both
methods. The certificate and private key under `tests/decky-runtime/fixtures`
are public test fixtures; they grant no trusted access. External radio, trusted
HTTPS and audio playback remain unverified.
Storage Cleaner runs through the published Decky loader with only disposable
cache markers: size/list, selected-app delete and whole-directory delete pass.
The managed copy rejects traversal and symlink delete paths. No real game or
Steam cache is deleted.
Game Sync runs through actual published Decky 3.3.0 instance RPC in the same
isolation. Only default configuration and a private setting surviving restart
are checked. No rclone, cloud sync, process signal, Frame backend or VR action
+40
View File
@@ -212,6 +212,46 @@ class StoreTransaction(unittest.TestCase):
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse(folder.exists())
def test_exact_storage_cleaner_deletes_only_private_cache_fixture(self):
from framey_official import PLUGINS, archive as official_archive
plugin = next(p for p in PLUGINS if p['id'] == 50)
data = official_archive(plugin)
self.assertEqual(frame_decky_store.archive_info(data, plugin['sha256'], plugin['name']), plugin['folder'])
result = self.apply('install', data, plugin_id=50,
folder=plugin['folder'], version=plugin['version'])
self.assertEqual(result.returncode, 0, result.stderr)
folder = self.folder.parent/plugin['folder']
receipt = json.loads((folder/'.frame-control-store.json').read_text())
self.assertEqual(receipt['compatShims'], ['storage-cleaner-bounded-delete'])
source = (folder/'main.py').read_text(encoding='utf-8')
self.assertNotIn('await shutil.rmtree', source)
steamapps = Path(self.tmp.name)/'.steam/steam/steamapps'
selected = steamapps/'shadercache/424242'
selected.mkdir(parents=True)
(selected/'marker').write_text('private fixture')
outside = Path(self.tmp.name)/'outside'
outside.mkdir()
(outside/'marker').write_text('must survive')
namespace = {'__name__': 'storage_cleaner_fixture'}
fake_decky = types.SimpleNamespace(DECKY_USER_HOME=self.tmp.name,
DECKY_PLUGIN_DIR=str(folder))
with patch.dict(sys.modules, {'decky': fake_decky}):
exec(compile(source, str(folder/'main.py'), 'exec'), namespace)
plugin_class = namespace['Plugin']
for name in ('../outside', 'shadercache/../outside', 'compatdata/../../outside',
'shadercache/not-an-app', '/tmp/outside'):
with self.subTest(name=name), self.assertRaisesRegex(ValueError, 'Invalid cache path'):
asyncio.run(plugin_class.delete_cache(name))
(steamapps/'compatdata').symlink_to(outside, target_is_directory=True)
with self.assertRaisesRegex(ValueError, 'symbolic link'):
asyncio.run(plugin_class.delete_cache('compatdata'))
self.assertTrue((outside/'marker').is_file())
self.assertIsNone(asyncio.run(plugin_class.delete_cache('shadercache/424242')))
self.assertFalse(selected.exists())
self.assertTrue((outside/'marker').is_file())
result = self.apply('remove', plugin_id=50, folder=plugin['folder'])
self.assertEqual(result.returncode, 0, result.stderr)
def test_modified_fileserver_archive_does_not_inherit_exception(self):
from framey_official import PLUGINS, archive as official_archive
record = next(p for p in PLUGINS if p['id'] == 60)
+2 -1
View File
@@ -21,7 +21,8 @@ class RuntimeCoverage(unittest.TestCase):
self.assertIn(case['id'], official)
self.assertIn(case['probe'], ('mpris', 'screenshots', 'system-monitor', 'timer-lifecycle',
'toggle-settings', 'fileserver', 'localsend', 'network-sentinel',
'battery-tracker', 'free-loader', 'radio-local', 'game-sync-config'))
'battery-tracker', 'free-loader', 'radio-local', 'game-sync-config',
'shelves-settings', 'storage-cleaner'))
def test_new_shim_without_runtime_case_fails_coverage(self):
import frame_decky_store
+36
View File
@@ -43,6 +43,8 @@ BATTERY_TRACKER_DIGEST = '91c388d3c1b52079314045b79671ad55c767510658b6193f6936ed
BATTERY_TRACKER_MAIN = '2303ec70c262f1cbfe1c8c138770c6e7c3496827e748e679c94ca43ff9f28725'
RADIYO_DIGEST = '647111578aa7a5ecd16af8d7fe6c068a6d7ea8737c6dce5ac2d52e84a549ec43'
RADIYO_MAIN = '868332dbe3feb1e7a81e35a0c01fdc0a6b68421ec1f84e238564bfad4946e87e'
STORAGE_CLEANER_DIGEST = '1afa1a6f129c5d419de0e0bf0f6db8d8e50385a5659179346e996031ea372591'
STORAGE_CLEANER_MAIN = '6fd4d012568edae22ba421495455b3c02899155e4c7484f5aa5be6ce9b14dd5f'
VERSION = re.compile(r'[A-Za-z0-9][A-Za-z0-9._+\-]{0,39}\Z')
@@ -176,6 +178,10 @@ def archive_info(data, expected_hash, expected_name):
if (expected_hash == RADIYO_DIGEST and expected_name == 'RadiYo!' and folder == 'radiyo-steam-deck'
and hashlib.sha256(archive.read(folder+'/main.py')).hexdigest() != RADIYO_MAIN):
raise ValueError('RadiYo source differs from the pinned archive')
if (expected_hash == STORAGE_CLEANER_DIGEST and expected_name == 'Storage Cleaner'
and folder == 'decky-storage-cleaner'
and hashlib.sha256(archive.read(folder+'/main.py')).hexdigest() != STORAGE_CLEANER_MAIN):
raise ValueError('Storage Cleaner source differs from the pinned archive')
return folder
@@ -730,6 +736,36 @@ def split_by_app(data):
data=data.replace(unsafe,b'').replace(broken,b"'URL Error: ' + str(error.reason)")
main.write_bytes(data)
compat_shims.append('radiyo-verified-tls')
# The exact published backend awaits synchronous shutil.rmtree:
# it deletes the cache then reports TypeError. Restrict deletion
# to the two directories selected by its UI and their numeric app IDs.
if int(plugin_id)==50 and digest=='1afa1a6f129c5d419de0e0bf0f6db8d8e50385a5659179346e996031ea372591' and folder=='decky-storage-cleaner':
main=unpacked/'main.py'
data=main.read_bytes()
if hashlib.sha256(data).hexdigest()!='6fd4d012568edae22ba421495455b3c02899155e4c7484f5aa5be6ce9b14dd5f':
raise RuntimeError('Storage Cleaner source differs from pinned archive')
old=b" @classmethod\n async def delete_cache(cls, dirName):\n await shutil.rmtree(homeDir + '/.steam/steam/steamapps/' + dirName)"
new=b""" @classmethod
async def delete_cache(cls, dirName):
if not isinstance(dirName, str):
raise ValueError('Invalid cache path')
parts = dirName.split('/')
if (len(parts) not in (1, 2) or parts[0] not in ('shadercache', 'compatdata')
or (len(parts) == 2 and (not parts[1].isascii() or not parts[1].isdecimal()))):
raise ValueError('Invalid cache path')
root = os.path.realpath(os.path.join(homeDir, '.steam', 'steam', 'steamapps'))
target = os.path.join(root, *parts)
cursor = root
for part in parts:
cursor = os.path.join(cursor, part)
if os.path.islink(cursor):
raise ValueError('Cache path is a symbolic link')
if os.path.commonpath((root, os.path.realpath(target))) != root:
raise ValueError('Cache path escapes Steam apps')
shutil.rmtree(target)"""
if data.count(old)!=1: raise RuntimeError('Storage Cleaner delete source changed')
main.write_bytes(data.replace(old,new))
compat_shims.append('storage-cleaner-bounded-delete')
# Recheck under the transaction lock. Another store operation may
# have installed a colliding name while this archive downloaded.
proposed_name=json.loads((unpacked/'plugin.json').read_text()).get('name')
+5 -4
View File
@@ -3714,12 +3714,13 @@
"https://github.com/mcarlucci/decky-storage-cleaner.git"
],
"status": "partial",
"reason": "Empty-data panel rendered; cleanup was deliberately not run because it can remove local data.",
"reason": "The empty-data panel renders locally. Exact managed 1.4.0 installs repair the backend delete-then-TypeError bug and restrict deletion to shadercache/compatdata or numeric app-ID children. Published Decky 3.3.0 x86_64 RPC passes private size/list and selected/whole fixture deletion in a network-isolated container. Real Steam cache deletion, Frame ARM64 backend, frontend confirmation flow and worn VR remain unverified; older copies need Update.",
"blockInstall": false,
"verification": [
"static archive",
"local browser",
"fake-Frame"
"fake-Frame",
"isolated Decky 3.3.0 x86_64 backend"
],
"browser": {
"id": 50,
@@ -12196,13 +12197,13 @@
"https://github.com/sponsors/santojon"
],
"status": "partial",
"reason": "The exact chunked frontend renders settings in opaque local Chromium. The official Python backend saved a shelf and returned it after reopen with a stub Decky module and temporary settings directory. Actual Decky RPC, real Frame settings, Steam Home shelf injection and worn VR remain unverified.",
"reason": "The exact chunked frontend renders settings in opaque local Chromium. The official Python backend now passes published Decky 3.3.0 x86_64 discovery, private shelf settings save/readback and persistence after a loader restart in a network-isolated container. Steam Home shelf injection has no verified target in the isolated panel; real Frame ARM64 settings, worn VR and physical controller use remain unverified.",
"blockInstall": false,
"verification": [
"static archive",
"local browser",
"fake-Frame",
"isolated Python backend"
"isolated Decky 3.3.0 x86_64 backend"
],
"browser": {
"id": 135,