Compare commits

...
Author SHA1 Message Date
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
SAPHID f7ff94aae4 Record live release lookup for audit fix 2026-10-29 01:44:03 +00:00
SAPHID f4c8308f8a Merge remote-tracking branch 'origin/main' into fix/release-audit-full-pagination 2026-10-29 01:41:38 +00:00
Alex Southwell a2e578f6c3 Merge pull request #400 from saphid/fix/release-gate-smoke-proof-20261029
Reject unreadable Frame smoke sensor values
2026-10-29 01:41:02 +00:00
SAPHID e8fb154107 Audit every release-list page before choosing a draft 2026-10-29 01:33:33 +00:00
SAPHID 35b59e62da Document Frame smoke sensor gate evidence 2026-10-29 01:29:48 +00:00
SAPHID be07dbeeeb Reject unreadable Frame smoke sensor values 2026-10-29 01:25:26 +00:00
Alex Southwell b123f5ff34 Merge pull request #399 from saphid/test/framey-gamesync-native
Verify Game Sync configuration in native Decky
2026-10-29 01:07:59 +00:00
SAPHID 08c99c2d7d docs: scope Game Sync ARM64 limit to cloud sync 2026-10-29 01:00:28 +00:00
SAPHID b3e3e21f46 test: remove unverified rclone path assertion 2026-10-29 00:58:58 +00:00
SAPHID 84bb7fa0b1 test: verify Game Sync configuration in native Decky 2026-10-29 00:55:54 +00:00
Alex Southwell 01e5d8b2db Merge pull request #398 from saphid/test/framey-radiyo-native
Verify RadiYo backend through isolated Decky
2026-10-29 00:49:26 +00:00
SAPHID 130d82128b test: include RadiYo native probe in runtime inventory 2026-10-29 00:38:22 +00:00
SAPHID 65e629f8c3 Verify RadiYo backend through isolated native Decky 2026-10-29 00:32:26 +00:00
Alex Southwell e33ed00945 Merge pull request #397 from saphid/fix/release-all-tag-runs
Reject earlier failed builds for a release tag
2026-10-29 00:09:11 +00:00
SAPHID 419d69ef8a Reject prior failed tagged release runs before publishing 2026-10-28 23:58:30 +00:00
Alex Southwell 8cf2fb9da3 Merge pull request #395 from saphid/ci/frame-smoke-source-commit
Bind Frame smoke evidence to release commit and lock cleanup
2026-10-28 23:39:18 +00:00
31 changed files with 656 additions and 40 deletions

No files matched your search

@@ -0,0 +1,20 @@
# Frame smoke sensor validation, 29 October 2026
The locked real Frame smoke reads SteamVR activity, IIO proximity and battery
before installing a test title. The proximity reader converts sensor text to a
Python float. Python accepts `NaN` and infinity as floats, while range
comparisons with `NaN` do not reject it. Before this change, a proximity array
containing `NaN` could pass the standby preflight and the saved-result release
verifier.
Focused local tests supplied `NaN`, positive and negative infinity to both
the live preflight parser and the release-result verifier. They also supplied
boolean, nonfinite, below-threshold and above-100 battery values. Every invalid
sample was rejected. Valid standby, finite proximity and 99–100% integer
battery samples continued to pass. The 25 smoke/cleanup tests, six result-gate
tests and 16 publisher tests passed locally.
This evidence uses simulated sensor responses. It does not record a real Frame
smoke, a worn-headset check, or controller input. Release publication still
requires a complete locked smoke from the exact clean release commit, including
the x86 launch that was skipped in the held v0.4.54 candidate.
@@ -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,24 @@
# Game Sync 1.0.4 isolated backend check, 29 October 2026
The official Decky archive for Game Sync (ID 104) is 2,228,806 bytes with
SHA-256 `65e021ebb37d2ebfbfbe3dbc8af95aef9556cf484730362c6b26c4ecc3e62ccd`.
Its unmodified ZIP is pinned as `tests/fixtures/framey-official/104.zip`.
`python3 scripts/framey-test-decky-runtime.py --case 104 --output /tmp/framey-gamesync-native-104.json`
passed with the published Decky 3.3.0 x86_64 loader, SHA-256
`e80d03e641d1766a33a817cb27ab8d2e0a4633685e5d27e4de948a6687526181`.
The disposable container had no external network or host devices, dropped
capabilities and used a private home. The real loader discovered the exact
plugin. Its `get_config` returned defaults with automatic game sync and
capture upload disabled. A private `advanced_mode` setting changed from false
to true and remained true after the loader restarted. The runner removed its
container and image; a separate `docker ps` and image-list audit found no
owned resources.
The original opaque-iframe browser audit blocked `get_config` before panel
initialization completed, so it did not prove a usable frontend. This native
test confirms only a configuration backend path. No cloud authentication,
rclone download or sync, screenshot deletion, process signal, game event,
real Frame backend, VR visibility or physical controller was exercised.
The published `rclone_manager.py` names an amd64 rclone download, so this
result does not establish ARM64 sync support. Game Sync remains **partial**.
@@ -0,0 +1,46 @@
# RadiYo! 1.3 isolated backend check, 29 October 2026
The official Decky store archive for RadiYo! (ID 24) is 33,686 bytes with
SHA-256 `647111578aa7a5ecd16af8d7fe6c068a6d7ea8737c6dce5ac2d52e84a549ec43`.
Its unmodified ZIP is pinned as `tests/fixtures/framey-official/24.zip` and
passes Frame Control's archive validation and fake Frame panel packaging.
`python3 scripts/framey-test-decky-runtime.py --case 24 --output /tmp/framey-radiyo-decky-runtime-20261029.json`
ran the published Decky 3.3.0 x86_64 loader, digest
`e80d03e641d1766a33a817cb27ab8d2e0a4633685e5d27e4de948a6687526181`,
in a disposable container with `--network none`, no host devices, dropped
capabilities and a private home. The real loader discovered the exact plugin
and dispatched its legacy `fetch` and `getRedirectUrl` RPCs. A server bound to
container loopback returned a fixed station marker: regular fetch returned it,
custom-station fetch omitted the body as upstream intends, and a redirect
resolved to the same fixture. The runner removed its container and image.
The previously recorded opaque-iframe browser audit showed frontend
initialization, but denied backend and network calls. This native probe adds
backend transport evidence only. No external radio station, HTTPS endpoint,
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.
+4 -4
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; radio discovery and playback remain unverified. | static archive, local browser, fake-Frame |
| 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 |
@@ -118,7 +118,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 101: XIVOmega 1.0.2 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
| 102: DeckSP 1.1.3 | 1 / 2 | broken | Optional UI-audio lookup succeeds, but the published frontend raises "Cannot access PluginManager before initialization" when Steam MobX reaction lookup is unavailable. JamesDSP installation/audio routing and ARM64 remain unverified. | static archive, local browser, fake-Frame |
| 103: Decky Ludusavi 1.2.2 | 1 / 2 | partial | Initializer succeeds; Flatpak Ludusavi discovery and backup/restore unverified. | static archive, local browser, fake-Frame |
| 104: Game Sync 1.0.4 | 1 / 2 | partial | Backend get_config is intentionally rejected by the audit before initialization completes; actual backend compatibility is unknown. | static archive, local browser, fake-Frame |
| 104: Game Sync 1.0.4 | 1 / 2 | partial | Exact official backend discovers under Decky 3.3.0 amd64; default get_config and a private setting read/write survive a loader restart. The isolated browser baseline still stops at its denied get_config call. Cloud login, rclone download/sync (including ARM64), process signals, real Frame and VR are unverified. The published updater names an amd64 rclone download, so no ARM64 sync claim follows. | static archive, local browser, fake-Frame, native Decky amd64 configuration |
| 105: WiFi Locker 1.0.2 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
| 106: LetMeReShade 1.8 | 1 / 2 | partial | Initializer succeeds; Deck-model assumptions, shader downloads and game modifications unverified. | static archive, local browser, fake-Frame |
| 107: Decky Sunshine 2025.10.27-dddf365 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | 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 |
+3 -3
View File
@@ -74,9 +74,9 @@ count. So a build reaches people only when you publish it, after testing it.
The script checks that the tag is on GitHub, that exact-commit main CI
(including e2e), native amd64/ARM64 Decky runtime checks, and the tagged
release workflow are green with no failed,
cancelled, skipped or running jobs in any run attempt, and that all ten installers
are attached, each with the SHA-256 digest GitHub records. It also checks
release workflow are green with no failed, cancelled, skipped or running
jobs in any exact-tag push run or attempt, and that all ten installers are
attached, each with the SHA-256 digest GitHub records. It also checks
that the draft does not begin with a `WITHDRAWN` notice, so the preserved
v0.4.48 draft cannot accidentally be offered again. It checks
that the uploaded `SHA256SUMS` digest matches a fresh list of those ten
+8 -8
View File
@@ -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):
+9 -2
View File
@@ -70,10 +70,15 @@ def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--loader', type=Path, help='Optional cached, digest-verified native Decky 3.3.0 binary')
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--case', type=int, help='Run one official plugin case locally; CI runs all cases')
args = parser.parse_args()
if sys.platform != 'linux' or not shutil.which('docker'):
parser.error('Linux with Docker is required')
cases = check_coverage()
if args.case is not None:
cases = [case for case in cases if case['id'] == args.case]
if not cases:
parser.error('Unknown official plugin case')
data, digest = loader_bytes(args.loader)
tag = 'framey-decky-runtime:'+uuid.uuid4().hex[:12]
report = {'decky': RELEASE, 'architecture': platform.machine(), 'loaderSha256': digest, 'cases': []}
@@ -85,8 +90,10 @@ def main():
loader.write_bytes(data)
loader.chmod(0o644) # Private container user copies/chmods its own executable.
probes = [(case['id'], None) for case in cases] + [
(111, '--reproduce-bound-method-regression'), (96, '--reproduce-timer-regression'),
(60, '--reproduce-fileserver-empty-flag-regression')]
(plugin_id, negative) for plugin_id, negative in [
(111, '--reproduce-bound-method-regression'), (96, '--reproduce-timer-regression'),
(60, '--reproduce-fileserver-empty-flag-regression')]
if args.case is None or args.case == plugin_id]
for plugin_id, negative in probes:
name = 'framey-decky-'+uuid.uuid4().hex[:12]
command = ['docker', 'run', '--rm', '--name', name, '--network', 'none', '--read-only',
+1 -1
View File
@@ -33,7 +33,7 @@ gh api "repos/$repo/git/ref/tags/$tag" >/dev/null 2>&1 \
# A complete asset list is insufficient: a cancelled installer build can leave
# old uploaded bytes behind. Check the exact tagged commit's main/e2e jobs and
# the latest exact-tag release attempt before any release mutation.
# every exact-tag push run and attempt before any release mutation.
python3 "$(dirname "$0")/verify-release-gates.py" "$tag" "${FRAME_SMOKE_RESULT:-}"
# Every release, drafts included, one JSON object per line.
+3 -1
View File
@@ -2,6 +2,7 @@
"""Check a locked Frame smoke result against the exact release commit."""
import json
import math
import re
import sys
from pathlib import Path
@@ -40,7 +41,8 @@ def verify(result, commit):
state = result.get('preflight') or {}
if (not isinstance(state, dict) or state.get('activity') != 3 or
not isinstance(state.get('proximity'), list) or not state['proximity'] or
any(type(value) not in (int, float) or value < 0 or value > 10
any(type(value) not in (int, float) or not math.isfinite(value) or
value < 0 or value > 10
for value in state['proximity']) or
not isinstance(state.get('battery'), list) or not state['battery'] or
any(type(value) is not int or value < 20 or value > 100
+24 -4
View File
@@ -61,14 +61,29 @@ def tag_commit(tag):
raise GateError(f"{tag} has too many nested tag objects")
def exact_run(workflow, branch, sha, event):
def exact_runs(workflow, branch, sha, event):
query = f"actions/workflows/{workflow}/runs?branch={quote(branch, safe='')}&head_sha={sha}&per_page=100"
runs = api(query).get("workflow_runs") or []
runs = []
for page in range(1, 21):
data = api(f"{query}&page={page}")
batch = data.get("workflow_runs")
total = data.get("total_count")
if not isinstance(batch, list) or type(total) is not int or total < 0:
raise GateError(f"invalid {workflow} run list for {branch} at {sha}")
runs.extend(batch)
if len(runs) >= total or len(batch) < 100:
break
if len(runs) != total or len({run.get('id') for run in runs}) != len(runs):
raise GateError(f"incomplete or duplicate {workflow} run list for {branch} at {sha}")
matching = [run for run in runs if run.get("head_sha") == sha
and run.get("head_branch") == branch and run.get("event") == event]
if not matching:
raise GateError(f"no exact {workflow} {event} run for {branch} at {sha}")
run = max(matching, key=lambda item: item["id"])
return sorted(matching, key=lambda item: item["id"])
def exact_run(workflow, branch, sha, event):
run = exact_runs(workflow, branch, sha, event)[-1]
if run.get("status") != "completed" or run.get("conclusion") != "success":
raise GateError(f"{workflow} run {run['id']} is {run.get('status')}/{run.get('conclusion')}")
return run
@@ -107,7 +122,12 @@ def main(tag, smoke_result):
sha = tag_commit(tag)
successful_jobs(exact_run("checks.yml", "main", sha, "push"), MAIN_JOBS)
successful_jobs(exact_run("decky-runtime.yml", "main", sha, "push"), DECKY_JOBS)
successful_jobs(exact_run("release.yml", tag, sha, "push"), RELEASE_JOBS)
# A fresh run must not hide a failed, cancelled, or skipped installer in an
# earlier separate push run for the same tag and commit.
for run in exact_runs("release.yml", tag, sha, "push"):
if run.get("status") != "completed" or run.get("conclusion") != "success":
raise GateError(f"release.yml run {run['id']} is {run.get('status')}/{run.get('conclusion')}")
successful_jobs(run, RELEASE_JOBS)
command = [sys.executable, str(Path(__file__).with_name("verify-frame-smoke-result.py")),
smoke_result, sha]
checked = subprocess.run(command, text=True, capture_output=True, check=False)
+20
View File
@@ -14,6 +14,26 @@
"shim": null,
"probe": "toggle-settings"
},
{
"id": 24,
"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,
"probe": "game-sync-config"
},
{
"id": 63,
"shim": null,
+131
View File
@@ -2,6 +2,7 @@
import argparse
import asyncio
import hashlib
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import os
from pathlib import Path
@@ -11,6 +12,7 @@ import socket
import ssl
import subprocess
import sys
import threading
import time
import urllib.request
import zipfile
@@ -265,6 +267,135 @@ def main():
assert current('read_deals') == {}
report['result'] = ('discovery, default settings, empty local deals, '
'private setting write/read and restart persistence passed; feed, notifications and VR unverified')
elif case['probe'] == 'game-sync-config':
# Exercise only private settings. This plugin also exposes
# rclone cloud sync and arbitrary process signals; neither
# belongs in a passive compatibility probe.
settings = current('get_config')
assert settings['sync_on_game_start'] is False, settings
assert settings['sync_on_game_stop'] is False, settings
assert settings['capture_upload'] is False, settings
assert settings['advanced_mode'] is False, settings
assert current('set_config', 'advanced_mode', True) is None
assert current('get_config')['advanced_mode'] is True
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')
assert until_ready(lambda: current('get_config')['advanced_mode'],
'private Game Sync setting after restart') is True
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':
# 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):
if self.path == '/redirect':
self.send_response(302)
self.send_header('Location', '/station')
self.end_headers()
elif self.path == '/station':
data = marker.encode()
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
else:
self.send_error(404)
def log_message(self, *_):
pass
server = ThreadingHTTPServer(('127.0.0.1', 0), RadioHandler)
worker = threading.Thread(target=server.serve_forever, daemon=True)
worker.start()
try:
base_url = f'http://127.0.0.1:{server.server_port}'
station = legacy('fetch', url=base_url+'/station', customStation=False)
assert station['status'] == 200 and station['body'] == marker, station
assert station['headers']['Content-Type'] == 'text/plain', station
custom = legacy('fetch', url=base_url+'/station', customStation=True)
assert custom['status'] == 200 and custom['body'] == '', custom
redirect = legacy('getRedirectUrl', url=base_url+'/redirect')
assert redirect == {'response': base_url+'/station'}, redirect
finally:
server.shutdown()
server.server_close()
worker.join(timeout=3)
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-----
+14 -3
View File
@@ -96,15 +96,26 @@ elif method == "GET" and endpoint.startswith(repo + "actions/workflows/"):
"status": gate.get("decky_status" if run_id == 503 else ("main_status" if is_main else "release_status"), "completed"),
"conclusion": gate.get("decky_conclusion" if run_id == 503 else ("main_conclusion" if is_main else "release_conclusion"), "success"),
"html_url": "https://github.com/saphid/frame-control/actions/runs/%s" % run_id}
print(json.dumps({"workflow_runs": [run]}))
runs = [run]
if run_id == 502 and gate.get("prior_release_run_bad_job"):
runs.append(dict(run, id=500, run_attempt=1, status="completed",
conclusion=gate.get("prior_release_run_conclusion", "failure"),
html_url="https://github.com/saphid/frame-control/actions/runs/500"))
if run_id == 502 and gate.get("release_run_list_incomplete"):
print(json.dumps({"total_count": len(runs) + 1, "workflow_runs": runs}))
else:
print(json.dumps({"total_count": len(runs), "workflow_runs": runs}))
elif method == "GET" and endpoint.startswith(repo + "actions/runs/"):
parts = endpoint[len(repo + "actions/runs/"):].split("/")
run_id = parts[0]
attempt = int(parts[2])
names = {"501": main_jobs, "502": release_jobs, "503": decky_jobs}[run_id]
names = {"500": release_jobs, "501": main_jobs, "502": release_jobs,
"503": decky_jobs}[run_id]
names = [name for name in names if name != gate.get("omit_job")]
jobs = [{"name": name, "status": "completed",
"conclusion": (gate.get("prior_bad_job_conclusion", "failure")
"conclusion": (gate.get("prior_release_run_bad_conclusion", "cancelled")
if run_id == "500" and name == gate.get("prior_release_run_bad_job")
else gate.get("prior_bad_job_conclusion", "failure")
if run_id == "502" and attempt == 1 and name == gate.get("prior_bad_job")
else gate.get("bad_job_conclusion", "success") if name == gate.get("bad_job")
else "success")}
Binary file not shown.
Binary file not shown.
+17 -1
View File
@@ -3,7 +3,7 @@
These are unmodified ZIPs downloaded from the official Decky store CDN on
2026-10-25, except `96.zip` and `111.zip` downloaded on 2026-10-26 and `60.zip` on
2026-10-27; `13.zip` and `125.zip` were added from the digest-verified audit cache on 2026-10-27,
and `67.zip` and `114.zip` on 2026-10-28.
and `67.zip` and `114.zip` on 2026-10-28. `24.zip` and `104.zip` were added on 2026-10-29.
Each ZIP includes its upstream `LICENSE`. They are test data, not plugins shipped or installed by
Frame Control. The fake Frame tests serve them
from a local fixture so CI does not depend on the store being online.
@@ -11,6 +11,8 @@ from a local fixture so CI does not depend on the store being online.
| File | Official plugin | Version | Store SHA-256 |
| --- | --- | --- | --- |
| `13.zip` | Pause Games | 1.0.3 | `4aefdfb0ef25dea9c6d9fd037731c8b99ee8a1be966c2f7d5b0602112ef29f62` |
| `24.zip` | RadiYo! | 1.3 | `647111578aa7a5ecd16af8d7fe6c068a6d7ea8737c6dce5ac2d52e84a549ec43` |
| `104.zip` | Game Sync | 1.0.4 | `65e021ebb37d2ebfbfbe3dbc8af95aef9556cf484730362c6b26c4ecc3e62ccd` |
| `60.zip` | DeckyFileServer | 1.2.0 | `7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0` |
| `125.zip` | Decky Localsend | 0.4.5 | `e271ccb55eb23f918b99a69f79aec905df4c9b2ef7b23bbf4ec1c2d1a6f0c6b7` |
| `114.zip` | Network Sentinel | 1.0.4 | `d3e07f40a0c800e72d15594e03c8c61e9bf3092524a578a0260520e735b971a5` |
@@ -60,6 +62,20 @@ Pause Games runs through actual published Decky 3.3.0 discovery and instance RPC
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. 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
is tested.
Network Sentinel runs through the same isolated native Decky gate. Its exact
archive gets plugin-local psutil and a user-service manifest; the probe checks
discovery, private settings and one monitoring-loop sample without external
+4
View File
@@ -10,6 +10,10 @@ PLUGINS = (
'folder': 'network-sentinel', 'sha256': 'd3e07f40a0c800e72d15594e03c8c61e9bf3092524a578a0260520e735b971a5'},
{'id': 13, 'name': 'Pause Games', 'version': '1.0.3', 'api_version': 1,
'folder': 'SDH-PauseGames', 'sha256': '4aefdfb0ef25dea9c6d9fd037731c8b99ee8a1be966c2f7d5b0602112ef29f62'},
{'id': 24, 'name': 'RadiYo!', 'version': '1.3', 'api_version': 'legacy',
'folder': 'radiyo-steam-deck', 'sha256': '647111578aa7a5ecd16af8d7fe6c068a6d7ea8737c6dce5ac2d52e84a549ec43'},
{'id': 104, 'name': 'Game Sync', 'version': '1.0.4', 'api_version': 1,
'folder': 'SDH-GameSync', 'sha256': '65e021ebb37d2ebfbfbe3dbc8af95aef9556cf484730362c6b26c4ecc3e62ccd'},
{'id': 60, 'name': 'DeckyFileServer', 'version': '1.2.0', 'api_version': 1,
'folder': 'DeckyFileServer', 'sha256': '7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0'},
{'id': 125, 'name': 'Decky Localsend', 'version': '0.4.5', 'api_version': 1,
+6 -2
View File
@@ -33,6 +33,7 @@ headset isn't reachable. A skipped launch is not a release smoke pass.
"""
import argparse
import json
import math
import os
import platform
import re
@@ -126,10 +127,13 @@ def preflight():
proximity = state.get('proximity')
if (not isinstance(proximity, list) or not proximity or
any(not isinstance(value, (int, float)) or isinstance(value, bool) or
value < 0 or value > 10 for value in proximity)):
not math.isfinite(value) or value < 0 or value > 10
for value in proximity)):
raise RuntimeError('Frame proximity is active or unknown; active session untouched')
battery = state.get('battery')
if not battery or min(battery) < 20:
if (not isinstance(battery, list) or not battery or
any(type(value) is not int or value < 20 or value > 100
for value in battery)):
raise RuntimeError('Frame battery is unavailable or below the smoke threshold')
return state
+32
View File
@@ -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}
+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'))
'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
+24
View File
@@ -199,6 +199,30 @@ class PublishRelease(unittest.TestCase):
self.assertIsNone(upload)
self.assertFalse(any("-X" in call for call in log))
def test_refuses_bad_installer_in_separate_prior_tag_run(self):
for run_conclusion, job_conclusion, expected in (
("failure", "cancelled", "failure"),
("success", "skipped", "skipped")):
with self.subTest(run_conclusion=run_conclusion, job_conclusion=job_conclusion):
gates = {"prior_release_run_bad_job":
"build (macos-latest, dist, app/dist/*.dmg app/dist/*.zip)",
"prior_release_run_conclusion": run_conclusion,
"prior_release_run_bad_conclusion": job_conclusion}
p, log, upload = self.run_script([draft()], "v9.8.7", gates=gates)
self.assertNotEqual(p.returncode, 0)
self.assertIn("run 500", p.stderr)
self.assertIn(expected, p.stderr)
self.assertIsNone(upload)
self.assertFalse(any("-X" in call for call in log))
def test_refuses_incomplete_tag_run_list(self):
p, log, upload = self.run_script([draft()], "v9.8.7",
gates={"release_run_list_incomplete": True})
self.assertNotEqual(p.returncode, 0)
self.assertIn("incomplete", p.stderr)
self.assertIsNone(upload)
self.assertFalse(any("-X" in call for call in log))
def test_refuses_missing_or_nonexact_gate_results(self):
for gates in ({"omit_job": "e2e"}, {"main_sha": "c" * 40},
{"decky_conclusion": "failure"}, {"release_conclusion": "failure"},
+6 -2
View File
@@ -108,7 +108,8 @@ class Safety(unittest.TestCase):
self.assertEqual(frame_smoke.source_revision(), {'commit': None, 'clean': False})
def test_locked_preflight_rejects_active_or_unknown_proximity_before_titles(self):
for proximity in ([], [88.0625], [4.6875, 88.0625], [None]):
for proximity in ([], [88.0625], [4.6875, 88.0625], [None],
[float('nan')], [float('inf')], [float('-inf')]):
with self.subTest(proximity=proximity), mock.patch.object(
frame_smoke, 'remote', return_value=mock.Mock(
returncode=0, stdout=json.dumps({
@@ -261,7 +262,10 @@ class SharedSmokeLock(unittest.TestCase):
for state in ({'activity':1,'proximity':[1],'battery':[100]},
{'activity':None,'proximity':[],'battery':[100]},
{'activity':3,'proximity':[],'battery':[19]},
{'activity':3,'proximity':[],'battery':[]}):
{'activity':3,'proximity':[],'battery':[]},
{'activity':3,'proximity':[0],'battery':[True]},
{'activity':3,'proximity':[0],'battery':[float('nan')]},
{'activity':3,'proximity':[0],'battery':[101]}):
with self.subTest(state=state), mock.patch.object(frame_smoke, 'remote',
return_value=mock.Mock(returncode=0, stdout=json.dumps(state))):
with self.assertRaises(RuntimeError):
+15
View File
@@ -1,6 +1,7 @@
"""The release smoke gate requires a complete result from its exact checkout."""
import importlib.util
import math
import unittest
from pathlib import Path
@@ -51,6 +52,20 @@ class VerifyFrameSmokeResult(unittest.TestCase):
with self.assertRaisesRegex(ValueError, 'preflight'):
gate.verify(data, SHA)
def test_rejects_nonfinite_proximity_and_invalid_battery(self):
for proximity in (math.nan, math.inf, -math.inf):
with self.subTest(proximity=proximity):
data = result()
data['preflight']['proximity'] = [proximity]
with self.assertRaisesRegex(ValueError, 'preflight'):
gate.verify(data, SHA)
for battery in ([True], [math.nan], [101], [19], ['99']):
with self.subTest(battery=battery):
data = result()
data['preflight']['battery'] = battery
with self.assertRaisesRegex(ValueError, 'preflight'):
gate.verify(data, SHA)
def test_rejects_missing_lock_release_record(self):
data = result()
data['lock_released'] = False
+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')
+11 -8
View File
@@ -1479,12 +1479,13 @@
"https://github.com/isiah-lloyd/radiyo-steam-deck"
],
"status": "partial",
"reason": "Initializer succeeds; radio discovery and playback remain unverified.",
"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",
"local browser",
"fake-Frame"
"fake-Frame",
"isolated Decky 3.3.0 x86_64 backend"
],
"browser": {
"id": 24,
@@ -3713,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,
@@ -8879,12 +8881,13 @@
"https://github.com/GedasFX/decky-cloud-save"
],
"status": "partial",
"reason": "Backend get_config is intentionally rejected by the audit before initialization completes; actual backend compatibility is unknown.",
"reason": "Exact official backend discovers under Decky 3.3.0 amd64; default get_config and a private setting read/write survive a loader restart. The isolated browser baseline still stops at its denied get_config call. Cloud login, rclone download/sync (including ARM64), process signals, real Frame and VR are unverified. The published updater names an amd64 rclone download, so no ARM64 sync claim follows.",
"blockInstall": false,
"verification": [
"static archive",
"local browser",
"fake-Frame"
"fake-Frame",
"native Decky amd64 configuration"
],
"browser": {
"id": 104,
@@ -12194,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,