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
SAPHID 3495a017d1 fix(framey): verify RadiYo HTTPS certificates 2026-10-29 02:16:19 +00:00
Alex Southwell 2a18f2bda7 Merge pull request #401 from saphid/fix/release-audit-full-pagination
Audit every release-list page before choosing a draft
2026-10-29 01:54:14 +00:00
13 changed files with 335 additions and 17 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.
@@ -22,3 +22,25 @@ audio playback, real Frame service, VR visibility or controller was tested.
The published backend sets `check_hostname=False` and `CERT_NONE` for its
outbound HTTPS requests; the loopback HTTP test did not exercise that path.
RadiYo! remains **partial**. The compatibility count does not change.
## Managed TLS repair
The exact official archive's `radiyo-steam-deck/main.py` has SHA-256
`868332dbe3feb1e7a81e35a0c01fdc0a6b68421ec1f84e238564bfad4946e87e`.
The managed installer checks that hash, removes the two certificate and
hostname bypasses from its private copy, and formats certificate errors as
strings for both `fetch` and `getRedirectUrl`. The official ZIP is unchanged;
an already installed managed copy needs Update to receive this repair.
`python3 scripts/framey-test-decky-runtime.py --case 24 --output /tmp/framey-radiyo-tls-runtime-20261029.json`
passed on native x86_64 with the published Decky 3.3.0 loader in a disposable,
network-isolated container. HTTP loopback fetch, custom fetch and redirect
still passed. Both HTTPS methods returned `CERTIFICATE_VERIFY_FAILED` for the
loopback self-signed test certificate. The disposable container and image were
removed. The 40 store transaction tests passed, including managed RadiYo
install, receipt check and removal. No headset lock was taken, and no real
Frame or external station was contacted.
This proves rejection of an untrusted certificate in the isolated backend.
Trusted HTTPS stations, audio playback, live frontend controls, real Frame and
worn VR behavior remain unverified. RadiYo! stays **partial**.
@@ -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.
+3 -3
View File
@@ -56,7 +56,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 21: PowerTools 2.0.3 | legacy / legacy | N/A | Not supported on Frame: Targets AMD Ryzen power controls; Frame uses an ARM64 SoC. | static archive, local browser, fake-Frame |
| 22: Memory Deck 0.1.8 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: memory-deck/bin/libscanmem.so.1.0.0. | static archive, local browser, fake-Frame |
| 23: Animation Changer 1.3.2 | legacy / legacy | partial | Initializer succeeds; forced backend failure prevents loading animations. Boot-video changes untested. | static archive, local browser, fake-Frame |
| 24: RadiYo! 1.3 | legacy / legacy | partial | Initializer succeeds. Exact official legacy backend passed published Decky 3.3.0 x86_64 discovery and loopback-only station fetch, custom-station fetch and redirect in a network-isolated container. Real radio services, HTTPS behavior, audio playback, real Frame backend and worn VR remain unverified. The published backend disables HTTPS certificate verification; no external request was made. | static archive, local browser, fake-Frame, isolated Decky 3.3.0 x86_64 backend |
| 24: RadiYo! 1.3 | legacy / legacy | partial | Initializer succeeds. The exact official legacy backend passes isolated Decky 3.3.0 discovery and loopback HTTP station fetch, custom fetch and redirect. A digest-scoped managed install restores TLS certificate and hostname checks; its fetch and redirect methods reject a loopback self-signed HTTPS endpoint. External radio services, trusted HTTPS, audio playback, real Frame backend and worn VR remain unverified. Older installed copies need an update for the TLS fix. | static archive, local browser, fake-Frame, isolated Decky 3.3.0 x86_64 backend |
| 26: HLTB for Deck 2.0.12 | 1 / 2 | partial | Settings render without the prior IndexedDB SecurityError; Steam library badges have no patch target, and live lookup is unverified. | static archive, local browser, fake-Frame |
| 27: AutoSuspend 2.2.0 | legacy / legacy | broken | Frontend audit failed: Cannot read properties of null (reading '6'). The exact frontend also contains SteamOS suspend and shutdown paths; threshold behavior and safe Frame operation are unverified. | static archive, local browser, fake-Frame |
| 28: Emuchievements 2.0.5 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: Emuchievements/bin/hash. | static archive, local browser, fake-Frame |
@@ -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 |
+11 -1
View File
@@ -16,9 +16,19 @@
},
{
"id": 24,
"shim": null,
"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,
+70 -5
View File
@@ -289,10 +289,10 @@ def main():
report['result'] = ('discovery, default config and private setting restart persistence passed; '
'rclone, cloud authentication, sync, process signals, real Frame and VR unverified')
elif case['probe'] == 'radio-local':
# The published backend accepts arbitrary URLs and disables
# HTTPS verification. Exercise only a loopback fixture inside
# this network-isolated container; no station or audio opens.
# Exercise HTTP behavior and prove managed RadiYo rejects a
# self-signed HTTPS endpoint. No station or audio opens.
marker = 'Framey private radio station\n'
assert 'ctx.verify_mode = ssl.CERT_NONE' not in (plugin/'main.py').read_text()
class RadioHandler(BaseHTTPRequestHandler):
def do_GET(self):
@@ -329,8 +329,73 @@ def main():
server.shutdown()
server.server_close()
worker.join(timeout=3)
report['result'] = ('actual Decky discovery and local station fetch/redirect passed; '
'external radio, HTTPS, audio playback, Frame and VR unverified')
secure = ThreadingHTTPServer(('127.0.0.1', 0), RadioHandler)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
fixtures = ROOT/'tests/decky-runtime/fixtures'
context.load_cert_chain(fixtures/'localhost-selfsigned.crt',
fixtures/'localhost-selfsigned.key')
secure.socket = context.wrap_socket(secure.socket, server_side=True)
worker = threading.Thread(target=secure.serve_forever, daemon=True)
worker.start()
try:
base_url = f'https://localhost:{secure.server_port}'
for method, kwargs in (
('fetch', {'url': base_url+'/station', 'customStation': False}),
('getRedirectUrl', {'url': base_url+'/redirect'})):
result = legacy(method, **kwargs)
assert 'error' in result and 'CERTIFICATE_VERIFY_FAILED' in result['error'], result
finally:
secure.shutdown()
secure.server_close()
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)
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgIUei7D5XAp4cxefO1O9TUr1wm52PEwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI2MTAyOTAyMDUzOVoYDzIxMjYx
MDA1MDIwNTM5WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDaSOVSxyLrzuOleRA/6RJL+3zXhZPdCjQGC6O2vx7B
EU+8hmvWMpJrUBjE8DncduB9gu8j03vtxX/ZC2lx6GQOFJGPsFEbYwB5InZaCl8E
hV42aYrhukxSJYETy+XrzEa6WT3CBZG8X4vHD+31tO5vBIeUGsWp5Mv2tQY+LAIm
4vgkBfy6dvADyttM7SjN0n/hUVRH+2cO9QiA5GWxmQOinZMmk8er9LpVkjTEdKAx
KxsQP/qZLrY2LOUM0qXpRGaG/1do+wmaid8B1tKrHm5AIMaitDznVSTf/ckcYDPW
BjYVrWdqcwt4v/lrkPEJRQKDJQeLDvQrQD6mGK7PKDsHAgMBAAGjUzBRMB0GA1Ud
DgQWBBSSw7MvTccz05JmuXR7lwnWlrpPejAfBgNVHSMEGDAWgBSSw7MvTccz05Jm
uXR7lwnWlrpPejAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBt
1xwvIf3PU3GWogGHyHxBtnfT1vMSoJ9SknrMD6pjtRfErXwB/ys+G6NRq5PQVXhM
GfpIrDeYFZ6TuWSqPkmNjVc88+rffg34kEuB0h3PPgmGxkvkoar6X72cCJ5DrAC0
P+ufIHLVepoz80/RED9Tf7Xa1YAG8uoVmZXaKBneTfRvRzhtbvksDlOMAOiQfxtq
l8qVGoYctfNl6ka/5AWq7KFvcFaXSSnvsLSWrwYZ+9B21ClxdIGEhYzbKJiPSc00
PbNL5be/QaLpptM/8QSFGjgzwZH7GoZe53XuSgCygvhAgVxDsMyi5qP+6JDGiPci
d4Jd914sn4ONMRq99lZd
-----END CERTIFICATE-----
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDaSOVSxyLrzuOl
eRA/6RJL+3zXhZPdCjQGC6O2vx7BEU+8hmvWMpJrUBjE8DncduB9gu8j03vtxX/Z
C2lx6GQOFJGPsFEbYwB5InZaCl8EhV42aYrhukxSJYETy+XrzEa6WT3CBZG8X4vH
D+31tO5vBIeUGsWp5Mv2tQY+LAIm4vgkBfy6dvADyttM7SjN0n/hUVRH+2cO9QiA
5GWxmQOinZMmk8er9LpVkjTEdKAxKxsQP/qZLrY2LOUM0qXpRGaG/1do+wmaid8B
1tKrHm5AIMaitDznVSTf/ckcYDPWBjYVrWdqcwt4v/lrkPEJRQKDJQeLDvQrQD6m
GK7PKDsHAgMBAAECggEAU3PeFLUkYTHiL4+mykBRqfH2azFa8/1kIVGO4B2WZOq4
6NMdZDFGcDeZmKVdAbADKIkH41MFxMQmmjS2NgHKSuXLNruKbqV9d1JZHBht9j0E
5VmZaq4+YoOLIyXne6xH+BOJPGXDr1h6CHtLFw56v7/kE5ferRLXdbUEs31Av1t+
1Vwj5Ne7QvNTEUTyBFn7fuXmeoUThM8ZdkW5b2fzQ8UZY/UjcOzo2jdg24MYwmBz
Jc1Ysdy6Qiz5SWl3SWTkpdMO6kaHG2cWXOECHtYEXj7HAIiZQJAjsZUHR5rsGrVh
3ecZHA5PIrrUojI2uls1PtFgrAnDN7ZYVJ3Ab2KFkQKBgQD4tl5p3tW1PRAUauWl
e4Brhn3yc9BvIH5aknKX0EBxp9CK1pcRb4c5RvGqmHENG90jeX95vRfCr6okKicX
wpIVqzp9aznA7uzJfAZ2SGsD+rnNfAHCr2lxjhhVCs/WS6Ck5a6i6DolXutunhD3
uad42YGNwYOV7cnZlyauf7GW1wKBgQDgrkjUu/hcn9jCaXHGd3xyelow0S8PZEIB
ogw6BN4eecxnCBExibkpGE+OeCyf/Os2HxObHwd51UNyhQ/FEhdrv4vu+rNb3pIL
8b7wu3zopC01BX62rDMy2abKwD9RyhOJYOflYZ6gJxpWjRTOIcQNBsSmxmXslTk2
iMKskBFnUQKBgHFeJ4EYqs83K0ozQ8R/nBTnNCe89MiCxjWgxY0/xvBHrz5p/Ghd
LwVDukqpu/Zeuhk+e4XRVLBQhDyB3lntPcuUxk9+/c9c2SAqYYOvFOC2e9UXbnIA
pQ62146EJCKpTGt9qnHlawO31ZcRg+h3fsgOVQh5qfJAdp6xUFeAK0H3AoGAeXff
6hsBZ9oHTvvAxgJ9JkrdjbzZy2Fxif54y7kr+J/ykNU8rmJ91yjMggKYZZpP83+T
ku6QlR5mC3jCkNF6FgeFeJrS8tz+ajS0/OLv4bo4soyY99tbBqfC06Rh1y7/purc
FBfVsVHSnG5DjDmXC0/Yay2hlukJ+GCxlqeAsTECgYAXgHIRET8nBkDwF7mCZnm9
IjTVYF/p4OzY9kKHRPUWjdaxEzoZfKWEDohz1x+2ruilzVYEU4b3ISlYzjIqOjaM
w4Kmwd97UlZnOe+13R64951xlN+6MxVUQ0J1Aty1e5QT8FNUD4c2277xvFvDOoep
RmBmgFaX/lBlVfiMlW/Bpg==
-----END PRIVATE KEY-----
+9 -2
View File
@@ -63,8 +63,15 @@ for private settings and a synthetic exclusion list. No process-control method
is called. The official frontend's standalone Toggle component separately
checks saved true/false state and matching settings callbacks; effects are inert.
RadiYo! runs through actual published Decky 3.3.0 legacy RPC in a network-isolated
container. Only loopback station fetch, custom-station fetch and redirect are
tested. External radio, HTTPS and audio playback remain unverified.
container. The managed copy passes loopback station fetch, custom-station fetch
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
+60
View File
@@ -192,6 +192,66 @@ class StoreTransaction(unittest.TestCase):
self.assertNotEqual(result.returncode, 0)
self.assertTrue(external.exists())
def test_exact_radiyo_managed_copy_restores_tls_verification(self):
from framey_official import PLUGINS, archive as official_archive
plugin = next(p for p in PLUGINS if p['id'] == 24)
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=24,
folder=plugin['folder'], version=plugin['version'])
self.assertEqual(result.returncode, 0, result.stderr)
folder = self.folder.parent/plugin['folder']
source = (folder/'main.py').read_text(encoding='utf-8')
receipt = json.loads((folder/'.frame-control-store.json').read_text())
self.assertEqual(receipt['compatShims'], ['radiyo-verified-tls'])
self.assertEqual(source.count('ctx = ssl.create_default_context()'), 2)
self.assertNotIn('ssl.CERT_NONE', source)
self.assertNotIn('ctx.check_hostname = False', source)
self.assertEqual(source.count("'URL Error: ' + str(error.reason)"), 2)
result = self.apply('remove', plugin_id=24, folder=plugin['folder'])
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
+56
View File
@@ -41,6 +41,10 @@ NETWORK_SENTINEL_DIGEST = 'd3e07f40a0c800e72d15594e03c8c61e9bf3092524a578a026052
NETWORK_SENTINEL_MAIN = '2baa390cb66dc267e8f04e61ba1edc43af75115ad5e453d308f6bf26ed70b6b9'
BATTERY_TRACKER_DIGEST = '91c388d3c1b52079314045b79671ad55c767510658b6193f6936ed6cad383b94'
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')
@@ -171,6 +175,13 @@ def archive_info(data, expected_hash, expected_name):
if battery_tracker and (flags != ['_root'] or 'api_version' in manifest
or hashlib.sha256(archive.read(folder+'/main.py')).hexdigest() != BATTERY_TRACKER_MAIN):
raise ValueError('Battery Tracker manifest or source differs from the pinned archive')
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
@@ -710,6 +721,51 @@ def split_by_app(data):
source=source.replace(old,old+' env["LD_LIBRARY_PATH"] = env.get("LD_LIBRARY_PATH_ORIG", "")\n')
main.write_text(source,encoding='utf-8')
compat_shims.append('music-control-system-dbus-env')
# RadiYo's exact 1.3 backend turns off certificate and hostname
# checks for both outbound requests. Restore Python's default TLS
# policy only for the pinned official source in this managed copy.
if int(plugin_id)==24 and digest=='647111578aa7a5ecd16af8d7fe6c068a6d7ea8737c6dce5ac2d52e84a549ec43' and folder=='radiyo-steam-deck':
main=unpacked/'main.py'
data=main.read_bytes()
if hashlib.sha256(data).hexdigest()!='868332dbe3feb1e7a81e35a0c01fdc0a6b68421ec1f84e238564bfad4946e87e':
raise RuntimeError('RadiYo source differs from pinned archive')
unsafe=b' ctx.check_hostname = False\n ctx.verify_mode = ssl.CERT_NONE\n'
broken=b"'URL Error: ' + error.reason"
if data.count(unsafe)!=2 or data.count(broken)!=2:
raise RuntimeError('RadiYo TLS source changed')
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')
+6 -5
View File
@@ -1479,7 +1479,7 @@
"https://github.com/isiah-lloyd/radiyo-steam-deck"
],
"status": "partial",
"reason": "Initializer succeeds. Exact official legacy backend passed published Decky 3.3.0 x86_64 discovery and loopback-only station fetch, custom-station fetch and redirect in a network-isolated container. Real radio services, HTTPS behavior, audio playback, real Frame backend and worn VR remain unverified. The published backend disables HTTPS certificate verification; no external request was made.",
"reason": "Initializer succeeds. The exact official legacy backend passes isolated Decky 3.3.0 discovery and loopback HTTP station fetch, custom fetch and redirect. A digest-scoped managed install restores TLS certificate and hostname checks; its fetch and redirect methods reject a loopback self-signed HTTPS endpoint. External radio services, trusted HTTPS, audio playback, real Frame backend and worn VR remain unverified. Older installed copies need an update for the TLS fix.",
"blockInstall": false,
"verification": [
"static archive",
@@ -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,