mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-29 04:02:36 +01:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af8c0a533f | ||
|
|
081ce61adb | ||
|
|
7c30f3311b | ||
|
|
81c2e86f20 | ||
|
|
ddbaff8f6a | ||
|
|
250f2b9674 | ||
|
|
3495a017d1 | ||
|
|
2a18f2bda7 | ||
|
|
f7ff94aae4 | ||
|
|
f4c8308f8a | ||
|
|
a2e578f6c3 | ||
|
|
e8fb154107 |
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.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Release audit pagination check, 29 October 2026
|
||||
|
||||
The independent installer audit previously returned on the first GitHub
|
||||
release-list page containing a matching tag or draft title. A second matching
|
||||
draft on a later page was never inspected. The audit could therefore report
|
||||
one release while the complete list was ambiguous.
|
||||
|
||||
Local tests mocked two GitHub pages with the same version draft. The old code
|
||||
accepted the first draft; the corrected lookup rejects the duplicate after
|
||||
reading both pages. A second test finds a release on page two, and a full
|
||||
20-page response fails closed because uniqueness cannot be established. All
|
||||
13 focused `test_audit_release_assets.py` tests passed locally.
|
||||
|
||||
The corrected lookup also read GitHub's live release list without downloading
|
||||
assets or changing a release. It found the published `v0.4.53` release as ID
|
||||
`427006429`.
|
||||
|
||||
The duplicate and pagination cases use mocked API responses. No release asset
|
||||
was downloaded, no release was changed, and these results are not a full release
|
||||
verification or real Frame smoke.
|
||||
@@ -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 |
|
||||
|
||||
@@ -51,16 +51,16 @@ def find_release(tag):
|
||||
if not isinstance(batch, list):
|
||||
raise AuditError("GitHub release list is not an array")
|
||||
releases.extend(batch)
|
||||
try:
|
||||
return CHECKSUMS["find_release"](
|
||||
(json.dumps(item) for item in releases), tag
|
||||
)
|
||||
except ValueError as error:
|
||||
if "found 0" not in str(error):
|
||||
raise AuditError(str(error)) from error
|
||||
if len(batch) < 100:
|
||||
break
|
||||
raise AuditError(f"no unique release found for {tag}")
|
||||
else:
|
||||
raise AuditError("release list exceeded 20 pages; cannot verify uniqueness")
|
||||
try:
|
||||
return CHECKSUMS["find_release"](
|
||||
(json.dumps(item) for item in releases), tag
|
||||
)
|
||||
except ValueError as error:
|
||||
raise AuditError(str(error)) from error
|
||||
|
||||
|
||||
def validate_assets(release):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -34,6 +34,38 @@ def release_assets():
|
||||
|
||||
|
||||
class AssetAudit(unittest.TestCase):
|
||||
def test_release_lookup_rejects_matching_draft_on_later_page(self):
|
||||
first = {"id": 42, "tag_name": "untagged-first", "draft": True,
|
||||
"name": "Frame Control 9.8.7"}
|
||||
second = {"id": 43, "tag_name": "untagged-second", "draft": True,
|
||||
"name": "Frame Control 9.8.7"}
|
||||
first_page = [first] + [
|
||||
{"id": number, "tag_name": f"v0.0.{number}", "draft": False}
|
||||
for number in range(1, 100)
|
||||
]
|
||||
with mock.patch.object(audit, "github_json", side_effect=[first_page, [second]]) as github:
|
||||
with self.assertRaisesRegex(audit.AuditError, "expected one release"):
|
||||
audit.find_release("v9.8.7")
|
||||
self.assertEqual(github.call_count, 2)
|
||||
|
||||
def test_release_lookup_finds_match_after_full_first_page(self):
|
||||
first_page = [
|
||||
{"id": number, "tag_name": f"v0.0.{number}", "draft": False}
|
||||
for number in range(1, 101)
|
||||
]
|
||||
matching = {"id": 42, "tag_name": "v9.8.7", "draft": True}
|
||||
with mock.patch.object(audit, "github_json", side_effect=[first_page, [matching]]) as github:
|
||||
self.assertEqual(audit.find_release("v9.8.7"), matching)
|
||||
self.assertEqual(github.call_count, 2)
|
||||
|
||||
def test_release_lookup_refuses_unfinished_pagination(self):
|
||||
page = [{"id": number, "tag_name": "v9.8.7" if number == 1 else f"v0.0.{number}"}
|
||||
for number in range(1, 101)]
|
||||
with mock.patch.object(audit, "github_json", return_value=page) as github:
|
||||
with self.assertRaisesRegex(audit.AuditError, "cannot verify uniqueness"):
|
||||
audit.find_release("v9.8.7")
|
||||
self.assertEqual(github.call_count, 20)
|
||||
|
||||
def test_published_manifest_matches_release_bytes(self):
|
||||
assets, _ = release_assets()
|
||||
release = {"tag_name": "v9.8.7", "body": "Release notes", "assets": assets}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user