Compare commits

...
17 changed files with 173 additions and 18 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,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,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.
+1 -1
View File
@@ -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 |
+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):
+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
+5
View File
@@ -19,6 +19,11 @@
"shim": null,
"probe": "radio-local"
},
{
"id": 104,
"shim": null,
"probe": "game-sync-config"
},
{
"id": 63,
"shim": null,
+21
View File
@@ -267,6 +267,27 @@ 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':
# The published backend accepts arbitrary URLs and disables
# HTTPS verification. Exercise only a loopback fixture inside
Binary file not shown.
+6 -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. `24.zip` was added on 2026-10-29.
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.
@@ -12,6 +12,7 @@ from a local fixture so CI does not depend on the store being online.
| --- | --- | --- | --- |
| `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` |
@@ -64,6 +65,10 @@ 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.
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
+2
View File
@@ -12,6 +12,8 @@ PLUGINS = (
'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}
+1 -1
View File
@@ -21,7 +21,7 @@ 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'))
'battery-tracker', 'free-loader', 'radio-local', 'game-sync-config'))
def test_new_shim_without_runtime_case_fails_coverage(self):
import frame_decky_store
+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
+3 -2
View File
@@ -8880,12 +8880,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,