mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-26 03:01:59 +01:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c4581f122 | ||
|
|
4372c913a6 | ||
|
|
5239dae1a8 | ||
|
|
9bc5294f36 | ||
|
|
7ed08c5bbd | ||
|
|
449146bbdb | ||
|
|
e115006ac9 | ||
|
|
30fbadbb23 | ||
|
|
cae1270796 | ||
|
|
dd62de4f6d | ||
|
|
5a4848cab9 | ||
|
|
480c6dcc36 | ||
|
|
30b20cbdd6 |
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"version": "0.4.36",
|
||||
"version": "0.4.37",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frame-control",
|
||||
"version": "0.4.36",
|
||||
"version": "0.4.37",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^44.4.5",
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"productName": "Frame Control",
|
||||
"version": "0.4.36",
|
||||
"version": "0.4.37",
|
||||
"description": "Desktop app for managing a Valve Steam Frame over SSH",
|
||||
"private": true,
|
||||
"main": "main.js",
|
||||
@@ -55,7 +55,8 @@
|
||||
"*.html",
|
||||
"*.js",
|
||||
"*.css",
|
||||
"telemetry.json"
|
||||
"telemetry.json",
|
||||
"framey-compatibility.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# What we have tested
|
||||
|
||||
Current for Frame Control **v0.4.36**, 25 October 2026. “Real Frame + physical PC” means an actual headset and a non-virtual computer. “VM only” means a Windows virtual machine, sometimes connected to the real Frame. A CI test or a simulated device is **not** a physical hardware test. The date/version column is the last relevant check, not a promise that every new release was retested on that platform. See each link for the exact procedure and limits.
|
||||
Current for Frame Control **v0.4.37**, 26 October 2026. “Real Frame + physical PC” means an actual headset and a non-virtual computer. “VM only” means a Windows virtual machine, sometimes connected to the real Frame. A CI test or a simulated device is **not** a physical hardware test. The date/version column is the last relevant check, not a promise that every new release was retested on that platform. See each link for the exact procedure and limits.
|
||||
|
||||
| Feature | Real Frame + physical PC tested | VM only | Untested or limited | Last check and proof |
|
||||
|---|---|---|---|---|
|
||||
| Core title smoke test | Linux lxso2 installed, launched and removed reserved native/Proton titles on the real Frame; all 13 steps passed | No | Unworn headset; visible rendering and controller input not checked. Other feature paths retain their separate limits below | 25 Oct, v0.4.36 tag `1701e62`, Frame OS 0.5.6 build `20261022.6153909`: [release smoke evidence](evidence/v0436-release-smoke-2026-10-25.md); [earlier detailed results](evidence/round2-smoke-2026-10-25.json) |
|
||||
| Core title smoke test | Linux lxso2 installed and removed reserved native/Proton titles on the real Frame; all 13 steps passed. ARM64 and Windows titles launched | No | The x86_64 title could not launch because Steam Linux Runtime 4.0 was not installed. The headset was unworn; visible rendering and controller input were not checked. Other feature paths retain their separate limits below | 26 Oct, v0.4.37 tag `5239dae`, Frame OS 0.5.6 build `20261022.6153909`: [release smoke evidence](evidence/v0437-release-smoke-2026-10-26.md); [earlier detailed results](evidence/round2-smoke-2026-10-25.json) |
|
||||
| Windows desktop in the headset | No physical Windows PC test | Windows 11 VM sent a whole desktop to a real Frame; this used an intermediate fix, not the final native DLL | Physical GPU, capture and controller input; individual windows unavailable in bundled DXGI build | 24 Oct, v0.4.10 installer + candidate fix: [VM log](evidence/windows-vm-2026-10-24.md), [PC guide](pc-in-headset.md) |
|
||||
| Linux desktop in the headset | No | No | Native Linux PC capture is disabled; lxso2 lacked a desktop portal and PipeWire session. The separate Steam `-pipewire` helper is not Frame Control's PC capture | 24 Oct, v0.4.17: [host audit](evidence/linux-pc-host-lxso2-2026-10-24.md), [Steam helper PR #163](https://github.com/saphid/frame-control/pull/163) |
|
||||
| Mac desktop in the headset | A physical Mac sent its screen/window to a real Frame | No | Current v0.4.26 Mac installer and worn-headset controller input have not been repeated | 28 Sep, pre-v0.4 / Frame OS 0.4.1: [Mac guide and measurement](streaming.md#b-show-the-macs-desktop-inside-the-frame) |
|
||||
@@ -22,8 +22,8 @@ Current for Frame Control **v0.4.36**, 25 October 2026. “Real Frame + physical
|
||||
| Passthrough in recordings | Real room pixels in OBS and a production MP4 after safe SteamVR wake | No | **Yes** for the checked 0.5.6 mirror. Raw cameras are not opened. Other games/modes and Steam clips may differ | 25 Oct: [validation](evidence/obs-validation-2026-10-25.md), [recording guide](recording.md#does-record-include-passthrough) |
|
||||
| Open at login | No Frame needed; Linux autostart file toggled on a physical Linux host with an isolated config directory | No | A full login session and physical Windows/macOS host login have not been checked; OS API calls were unit tested | 25 Oct, v0.4.26: [PR #189](https://github.com/saphid/frame-control/pull/189) |
|
||||
| Local Frame Control extensions | Example panel displayed live Frame status in a browser preview; the real Frame served a read-only example in the experimental headset panel | No | No wearer-visible extension view; third-party extensions and physical Windows/macOS extension folders untested | 25 Oct, v0.4.29: [PR #194](https://github.com/saphid/frame-control/pull/194), [PR #201](https://github.com/saphid/frame-control/pull/201), [extension guide](extensions.md) |
|
||||
| Experimental Frame Control SteamVR panel | Real Frame OS 0.5.6 installed its user service, registered an overlay, served authenticated live status and a read-only extension, saved a screenshot and short MP4, and removed owned resources | No | Headset was unworn in standby: overlay reported `not_visible`, stereo capture was dark; wearer-visible pixels and controller input are unverified. The new large read-only status screen has browser layout and packaging checks only | 25 Oct, v0.4.29 device check and v0.4.31 status screen: [PR #198](https://github.com/saphid/frame-control/pull/198), [PR #201](https://github.com/saphid/frame-control/pull/201), [PR #220](https://github.com/saphid/frame-control/pull/220), [panel guide](headset-app.md) |
|
||||
| Experimental Decky Loader manager | Real Frame OS 0.5.6 ARM64 user-service install, status, update and remove completed; seven official-store plugin backends loaded, and ProtonDB Badges completed managed store install/update/remove | No | v0.4.33 shared transaction lock and stopped-loader plugin removal have Linux and CI tests, but no live recovery test on the Frame. v0.4.35 treats `_root` store tags as root-required and hides duplicate IDs; this has test coverage, not a live store check. v0.4.36 stops a store install or update when the catalog hash changed after review; this was tested with a fake Frame, not with a live store change on the headset. Plugin pages, settings, routes, toasts and controller input in VR remain unverified; some plugins need root, x86_64 binaries or Deck-specific hardware | 25 Oct, v0.4.31 hardware and v0.4.36 tests: [PR #208](https://github.com/saphid/frame-control/pull/208), [PR #230](https://github.com/saphid/frame-control/pull/230), [PR #234](https://github.com/saphid/frame-control/pull/234), [PR #246](https://github.com/saphid/frame-control/pull/246), [PR #253](https://github.com/saphid/frame-control/pull/253), [compatibility table](decky-frame.md) |
|
||||
| Experimental Framey VR plugin panels and store | No live real-Frame panel test | No | ProtonDB Badges settings rendered and sent a setting call in local Chromium. Official Steam Status rendered in local Chromium; a session-only cache avoided its earlier storage errors. A later local browser check restored a bounded per-plugin cache after panel reopen; live Decky and offline behavior were not tested. Fake Frame tests cover setting read/write through update and reinstall and install/remove two pinned official API v2 archives plus the modern API v1 Storage Cleaner archive. Page tests cover the official JSX runtime, disabled controls, text fields, sliders, grouped dropdowns and an author sample bundle with a settings tab and read-only backend quick action. The sample is not published in the store. The 11-archive manifest audit found only ProtonDB Badges API v2 in that set. Storage Cleaner 1.4.0 showed an empty-data panel and four disabled cleanup buttons in local Chromium with a stub backend; no cleanup action was tested. HLTB for Deck 2.0.12 built a shallow settings view and Bluetooth 3.1.1 built its initial panel in local frontend tests without effects or backend calls. Their badges, search, pairing and Bluetooth changes remain unverified. A hash match, local panel and fake Frame install do not prove VR use. Wearer-visible pixels, controller input and live settings persistence remain unverified | 25 Oct, v0.4.32–0.4.36: [#223](https://github.com/saphid/frame-control/pull/223), [#224](https://github.com/saphid/frame-control/pull/224), [#225](https://github.com/saphid/frame-control/pull/225), [#226](https://github.com/saphid/frame-control/pull/226), [#227](https://github.com/saphid/frame-control/pull/227), [#229](https://github.com/saphid/frame-control/pull/229), [#231](https://github.com/saphid/frame-control/pull/231), [#232](https://github.com/saphid/frame-control/pull/232), [#233](https://github.com/saphid/frame-control/pull/233), [#236](https://github.com/saphid/frame-control/pull/236), [#237](https://github.com/saphid/frame-control/pull/237), [#238](https://github.com/saphid/frame-control/pull/238), [#239](https://github.com/saphid/frame-control/pull/239), [#240](https://github.com/saphid/frame-control/pull/240), [#241](https://github.com/saphid/frame-control/pull/241), [#243](https://github.com/saphid/frame-control/pull/243), [#244](https://github.com/saphid/frame-control/pull/244), [#245](https://github.com/saphid/frame-control/pull/245), [#247](https://github.com/saphid/frame-control/pull/247), [#248](https://github.com/saphid/frame-control/pull/248), [#251](https://github.com/saphid/frame-control/pull/251), [#252](https://github.com/saphid/frame-control/pull/252), [#253](https://github.com/saphid/frame-control/pull/253), [browser/fake Frame evidence](evidence/framey-browser-2026-10-25.md), [guide](framey-loader.md) |
|
||||
| Experimental Frame Control SteamVR panel | Frame OS 0.5.6 registered and served the opt-in overlay. A later unattended check safely woke SteamVR and found the panel in stereo compositor and mirror captures; it removed its temporary service and restored power settings | No | No person wore the headset for that check. Physical controller input, scrolling, VR keyboard, comfort and legibility remain unverified. The large status view has browser and packaging checks, not a wearer review | 25 Oct, v0.4.29–0.4.37: [panel PR #201](https://github.com/saphid/frame-control/pull/201), [unattended evidence](evidence/framey-vr-2026-10-25/README.md), [worn checklist](framey-vr-check.md) |
|
||||
| Experimental Decky Loader manager | Frame OS 0.5.6 ARM64 user-service install/status/update/remove and managed ProtonDB install/update/remove passed earlier. A private Decky 3.3.0 service later returned read-only ProtonDB and Steam Status setting RPCs through Steam CEF; it was removed under the shared lock | No | The loader remains opt-in. The exact-version official catalog covers 116 plugins: 0 works, 43 partial, 62 broken and 11 not applicable. Fake Frame transport and local Chromium do not prove official plugin backends or VR controls. Physical controller use and wearer-visible official plugin panels remain unverified | 25–26 Oct, v0.4.31–0.4.37: [managed store PR #208](https://github.com/saphid/frame-control/pull/208), [catalog PR #266](https://github.com/saphid/frame-control/pull/266), [backend evidence](evidence/framey-official-backend-2026-10-25.json), [compatibility table](framey-compatibility.md) |
|
||||
| Experimental Framey VR plugin panels and store | An unattended Frame OS 0.5.6 run showed the Framey sample panel in stereo compositor and mirror captures. Synthetic OpenVR clicks changed a live sample setting, read its Decky backend file and confirmed it after a fresh iframe reopen | No | This was the repository sample, not an official store plugin. Physical controllers, native scrolling, VR keyboard, wearer comfort and legibility are unverified. CSS Loader and SteamGridDB exact archives passed fake Frame install/remove and panel packing; this does not establish theme or artwork controls in VR. React toast content is tested in the host and fake Frame only; it is a panel notice, not a Steam system notification. The 116-plugin catalog had 60 fake installs/removals and 56 expected blocks; 96 local Chromium initializers were ready and 20 failed. No plugin is marked fully working | 25–26 Oct, v0.4.37: [unattended evidence and cleanup](evidence/framey-vr-2026-10-25/README.md), [CSS/SteamGridDB PR #257](https://github.com/saphid/frame-control/pull/257), [legacy bridge PR #261](https://github.com/saphid/frame-control/pull/261), [UI shims PR #263](https://github.com/saphid/frame-control/pull/263), [catalog PR #266](https://github.com/saphid/frame-control/pull/266), [toast PR #267](https://github.com/saphid/frame-control/pull/267), [worn checklist](framey-vr-check.md) |
|
||||
|
||||
Release notes must preserve these limits until a newer recorded check changes them. When a feature is retested, add its date, exact app/Frame version, host type and proof here. The older [v0.3.1 matrix](support-matrix.html) is a historical snapshot and does not describe current coverage.
|
||||
+5
-2
@@ -88,8 +88,11 @@ entries. Install, update, and remove need an explicit confirmation. They only
|
||||
change Frame Control owned plugin folders under
|
||||
`~/.local/share/frame-control/decky/plugins`. The installer checks the store
|
||||
hash and archive paths, blocks root plugins and non-ARM64 binaries, and rolls
|
||||
back an update if the service restart fails. The current VR shell does not
|
||||
provide Decky's Steam Deck Quick Access Menu. Frame Control's headset panel
|
||||
back an update if the service restart fails. An update is also refused if its
|
||||
new plugin name is already used by another installed plugin, because Decky
|
||||
routes backend calls by name. This collision check has local tests only.
|
||||
The current VR shell does not provide Decky's Steam Deck Quick Access Menu.
|
||||
Frame Control's headset panel
|
||||
shows installed plugins and store actions. Its separate Framey iframe hosts
|
||||
API v2 pages and API v1 bundles that use Decky's modern frontend initializer,
|
||||
with settings, local routes, and panel toasts where supported.
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"timeUtc": "2026-10-25T23:35:22.022586+00:00",
|
||||
"method": "Real Frame ARM64 Decky 3.3.0, isolated removable user service; explicit read-only plugin methods only",
|
||||
"plugins": [
|
||||
{
|
||||
"id": 14,
|
||||
"version": "1.3.4",
|
||||
"sha256": "90a3453ac02709cad64096e7539fd8141fa56f99ae3441fc24a4990dc9684358",
|
||||
"method": "get_setting",
|
||||
"result": {
|
||||
"result": "default"
|
||||
},
|
||||
"errors": [
|
||||
"Transient CEF reconnect during Decky frontend injection; succeeded on retry."
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 123,
|
||||
"version": "1.2.3",
|
||||
"sha256": "9053308e04a74f44745b45b9f4f50af3673348d367e7b14bacc5677de707ac4e",
|
||||
"method": "get_settings",
|
||||
"result": {
|
||||
"keys": [
|
||||
"enable_notification_antiflood",
|
||||
"enable_notifications",
|
||||
"gateway_api_key",
|
||||
"gateway_url",
|
||||
"refresh_interval_seconds",
|
||||
"show_history",
|
||||
"show_regions",
|
||||
"show_trending_games"
|
||||
],
|
||||
"defaultRefreshInterval": 120
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
],
|
||||
"before": {
|
||||
"battery": [
|
||||
100
|
||||
],
|
||||
"deckyService": "inactive",
|
||||
"panelService": "inactive"
|
||||
},
|
||||
"service": {
|
||||
"created": true
|
||||
},
|
||||
"decky": {
|
||||
"version": "v3.3.0",
|
||||
"sha256": "8ef5dcf36cbbbb6e97201a6baed5d79c4cc44ec27789fc9af1c5b6ae8f0b25b6"
|
||||
},
|
||||
"passed": true,
|
||||
"cleanup": {
|
||||
"baseAbsent": true,
|
||||
"unitAbsent": true
|
||||
},
|
||||
"after": {
|
||||
"battery": [
|
||||
99
|
||||
],
|
||||
"deckyService": "inactive",
|
||||
"panelService": "inactive"
|
||||
},
|
||||
"lockReleased": true,
|
||||
"cleanupErrors": []
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# v0.4.37 release smoke on a real Frame
|
||||
|
||||
On 26 October 2026 at 00:27 UTC, a physical Linux host ran the release
|
||||
`scripts/frame-smoke.sh` against a real Steam Frame. The smoke scripts were
|
||||
byte-identical to tag `v0.4.37` (`5239dae1a82c35635ef89f59eac9721c5014e2ed`).
|
||||
The headset reported Frame OS 0.5.6, build `20261022.6153909`.
|
||||
|
||||
The test acquired `/tmp/frame-test.lock` with a unique owner marker before
|
||||
reading the headset. SteamVR activity stayed at 3 (standby); initial proximity
|
||||
raw was 6.3125 and battery was 99%. The test released only its own lock after
|
||||
cleanup. Battery read 100% afterward.
|
||||
|
||||
All **13 of 13** smoke steps passed. ARM64 and Windows test titles installed,
|
||||
launched, and were removed. The x86_64 title installed and was removed; its
|
||||
launch reported that Steam Linux Runtime 4.0 was found but not installed, so
|
||||
this result does **not** establish x86_64 game execution. Devkit properties and
|
||||
status reads passed. No key was paired. The final cleanup removed the reserved
|
||||
test titles and processes. The private result remains under
|
||||
`/tmp/frame-v0437-smoke-results/`; this summary omits headset identifiers and
|
||||
device status details.
|
||||
|
||||
Because the headset was in standby, this test did not verify wearer-visible
|
||||
pixels, physical controller input, a Framey panel, or official Decky plugin
|
||||
settings in VR. Those checks remain on the [worn checklist](../framey-vr-check.md).
|
||||
@@ -0,0 +1,311 @@
|
||||
# Official Decky store compatibility with Framey
|
||||
|
||||
Snapshot: 2026-10-25T23:19:48.854504+00:00; 116 current visible official plugins from https://plugins.deckbrew.xyz/plugins.
|
||||
|
||||
This table and both store views use [`ui/framey-compatibility.json`](../ui/framey-compatibility.json). Results apply only to the exact store ID, version and SHA-256. A changed digest/version is **unverified**. Regenerate this document with `python3 scripts/framey-compatibility-doc.py`; use `--check` in review.
|
||||
|
||||
**Works: 0 · Partial: 43 · Broken: 62 · N/A: 11.**
|
||||
|
||||
- **Works** requires verified useful plugin behavior with its real backend. None qualifies from this audit.
|
||||
- **Partial** means a limited frontend/settings surface initialized or rendered. Backend-dependent functionality remains unverified; it is not an end-to-end success claim.
|
||||
- **Broken** means a concrete archive, frontend or Steam dependency blocks the audited version. An artificial backend/network denial alone is not evidence that the real backend is broken.
|
||||
- **N/A** means the plugin targets hardware or a display/UI surface that Framey does not provide. The store disables installation for these versions.
|
||||
|
||||
## Verification limits
|
||||
|
||||
All exact archives were hash-checked and statically inspected. The local Chromium host used Framey commit `779f9c5`, React 18.3.1 and an opaque `allow-scripts` iframe. Each frontend was attempted for 1.2 seconds, including React effects. All plugin backend calls, fetches and websockets were denied. Some plugins attempt mutating actions on mount; none reached a backend. Initialization (`ready`) does not guarantee React rendered, and delayed errors/actions may occur after the observation window. The JSON preserves initializer errors, capped console observations and attempted method names.
|
||||
|
||||
The optional full-catalog fake-Frame audit exercises the real HTTP/SSH installer, receipt, frontend bytes and removal, or expected rejection. It uses a private disposable Compose project with all plugin backend execution stubbed. It checks archive transport and safeguards, not Python/native execution. Run it with:
|
||||
|
||||
```sh
|
||||
python3 scripts/framey-audit-store.py --cache /tmp/framey-archives --output /tmp/framey-observations.json
|
||||
python3 scripts/framey-audit-fake.py --cache /tmp/framey-archives --output /tmp/framey-fake/results.json
|
||||
python3 scripts/framey-audit-browser.py --cache /tmp/framey-archives
|
||||
```
|
||||
|
||||
Open the browser audit URL in local Chromium, run `runAudit()` and inspect/download `results`. All backend/network actions remain denied.
|
||||
|
||||
The current published digests are pinned in the JSON; if the store changed, retain/download those exact archives for reproduction. Audit downloads permit larger ZIPs for inspection; production installer limits remain 50 MiB ZIP / 150 MiB unpacked / 30 MiB per member.
|
||||
|
||||
After the VR job released the shared lock, this audit acquired it and used a private temporary Decky 3.3.0 ARM64 user service. Read-only ProtonDB `get_setting` and Steam Status `get_settings` calls succeeded through the live Steam CEF websocket. The service and private base were removed and the lock released; normal managed services stayed inactive, battery was 100 → 99, and no power setting was changed. [Redacted results](evidence/framey-official-backend-2026-10-25.json) record exactly what ran. All other official backends remain unexecuted. The separate VR job proves Frame Control/sample compositor rendering, not any official plugin. No worn-VR official-plugin result is claimed. Earlier focused checks cover [ProtonDB](evidence/framey-browser-2026-10-25.md), [Steam Status/cache](evidence/framey-cache-browser-2026-10-25.md), [Storage Cleaner](evidence/framey-api-v1-browser-2026-10-25.md), and [legacy MusicControl](evidence/framey-legacy-2026-10-25.md).
|
||||
|
||||
## Every current plugin
|
||||
|
||||
The API column shows manifest / actual bundled frontend API. Many manifests say v1 while their bundle connects API2. Missing manifest API is labelled legacy; an unrecognized bundle is unknown. Globals and hardware are static signals, not exhaustive dependencies.
|
||||
|
||||
| Plugin / version | Manifest / bundle API | Status | Reason | Verified by |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1: XR Gaming 1.6.2 | 1 / 2 | N/A | Not supported on Frame: Requires supported external XR glasses and the Breezy/XRLinuxDriver stack; it does not target Steam Frame displays. | static archive, local browser, fake-Frame |
|
||||
| 7: CSS Loader 2.1.3 | legacy / legacy | partial | Panel initialization succeeds; CSS themes patch Steam UI and cannot theme the isolated VR shell. | static archive, local browser, fake-Frame |
|
||||
| 10: vibrantDeck 2.0.1 | legacy / legacy | N/A | Not supported on Frame: Changes X11 gamescope color properties, not the Frame SteamVR compositor. | static archive, local browser, fake-Frame |
|
||||
| 11: Fantastic 0.5.1 | legacy / legacy | N/A | Not supported on Frame: Targets Steam Deck fan hardware; no Frame fan-control mapping is provided. | static archive, local browser, fake-Frame |
|
||||
| 12: MusicControl 1.1.6 | legacy / legacy | partial | Legacy media controls initialize; player discovery and playback are unverified. | static archive, local browser, fake-Frame |
|
||||
| 13: Pause Games 1.0.3 | 1 / 2 | broken | Frontend audit failed: SteamClient.System.UI.RegisterForSystemKeyEvents is not a function. | static archive, local browser, fake-Frame |
|
||||
| 14: ProtonDB Badges 1.3.4 | 2 / 2 | partial | Settings render locally and read-only get_setting RPC returned the default through real Decky 3.3.0 on ARM64 Frame. Steam game-page badges have no patch target; live writes and VR controls unverified. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 16: Audio Loader 1.6.1 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. Frontend audit failed: Cannot read properties of null (reading 'm_GamepadUIAudioStore'). | static archive, local browser, fake-Frame |
|
||||
| 18: Bluetooth 3.1.1 | 1 / 2 | partial | Controls render with fixture replies; actual Bluetooth discovery, labels and connections remain unverified. | static archive, local browser, fake-Frame |
|
||||
| 19: Notebook 0.1.5 | legacy / legacy | partial | Notebook initializes; real note load/save remains unverified. | static archive, local browser, fake-Frame |
|
||||
| 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 |
|
||||
| 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'). | 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 |
|
||||
| 30: TunnelDeck 1.0.4 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 32: AutoFlatpaks 1.6.8 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 35: MoonDeck 1.13.0 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: moondeck/python/externals/yarl/_quoting_c.cpython-313-x86_64-linux-gnu.so. Frontend initializes; native CPython wheels and executables in the published archive are x86_64. | static archive, local browser, fake-Frame |
|
||||
| 36: SteamGridDB 1.8.1 | 1 / 2 | broken | Frontend audit failed: DFL.createModuleMapping is not a function. | static archive, local browser, fake-Frame |
|
||||
| 37: Discord Status 1.5.0 | 1 / 2 | broken | RunningApps and appStore are absent; Discord activity integration fails after initialization. | static archive, local browser, fake-Frame |
|
||||
| 38: Volume Mixer 1.2.1-1 | legacy / legacy | partial | Initializer succeeds; user PulseAudio/PipeWire mixer backend and volume changes unverified. | static archive, local browser, fake-Frame |
|
||||
| 40: Controller Tools 2.0.2 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: ControllerTools/bin/backend. Frontend audit failed: Audit blocks WebSocket. | static archive, local browser, fake-Frame |
|
||||
| 43: Tailscale Control 0.1.3 | legacy / legacy | broken | Initializer succeeds but React rejects a missing component (#130); no usable control claim. | static archive, local browser, fake-Frame |
|
||||
| 44: DeckMTP 1.0.5 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: DeckMTP/bin/umtprd. | static archive, local browser, fake-Frame |
|
||||
| 46: Steamback 1.0.0-1 | legacy / legacy | broken | Unsafe plugin archive member. Frontend audit failed: App is not defined. | static archive, local browser, fake-Frame |
|
||||
| 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 | Unsafe plugin archive member. | 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 |
|
||||
| 52: Game Theme Music 1.7.1-1 | 1 / 2 | broken | Frontend audit failed: Cannot read properties of null (reading 'type'). | static archive, local browser, fake-Frame |
|
||||
| 53: Shotty 0.1.0 | legacy / legacy | broken | Frontend audit failed: Cannot read properties of undefined (reading 'allApps'). | static archive, local browser, fake-Frame |
|
||||
| 56: MangoPeel 0.0.7 | 1 / 2 | partial | Initializer succeeds; gamescope/MangoHud actions and backend are unverified. | static archive, local browser, fake-Frame |
|
||||
| 58: Volume Boost 0.0.7 | legacy / legacy | partial | Initializer succeeds; PulseAudio/PipeWire volume backend is unverified. | static archive, local browser, fake-Frame |
|
||||
| 59: Web Browser 1.4.3 | legacy / legacy | broken | Frontend audit failed: deckyFrontendLib.getReactRoot is not a function. | static archive, local browser, fake-Frame |
|
||||
| 60: DeckyFileServer 1.2.0 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: DeckyFileServer/bin/backend. | static archive, local browser, fake-Frame |
|
||||
| 61: TabMaster 2.15.1 | 1 / 2 | broken | Frontend audit failed: Cannot read properties of null (reading 'GamepadUIAudio'). | static archive, local browser, fake-Frame |
|
||||
| 62: TS3 QuickAccess 1.2.0 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: ts3-qs4sd/bin/ts3-qs4sd.so. | static archive, local browser, fake-Frame |
|
||||
| 63: Free Loader 1.6.0 | 1 / 2 | broken | Unsafe plugin archive member. | static archive, local browser, fake-Frame |
|
||||
| 64: Syncthing 0.2.7 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: decky-syncthing/bin/decky-syncthing-watchdog. | static archive, local browser, fake-Frame |
|
||||
| 65: Decky Terminal 0.5.3 | 1 / 2 | partial | Local routes initialize after parameter-route support; no shell commands were run. | static archive, local browser, fake-Frame |
|
||||
| 66: PlayTime 3.2.5 | 1 / 2 | broken | Frontend audit failed: Cannot read properties of undefined (reading 'GetJSON'). | static archive, local browser, fake-Frame |
|
||||
| 67: Battery Tracker 0.2.0 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 68: Wine Cellar 0.2.1 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-wine-cellar/bin/backend. Frontend audit failed: Audit blocks WebSocket. | static archive, local browser, fake-Frame |
|
||||
| 70: MicroSDeck 0.11.2 | legacy / 2 | broken | Plugin contains a non-ARM64 Linux binary: MicroSDeck/bin/backend. | static archive, local browser, fake-Frame |
|
||||
| 72: ScreenshotUploader 0.0.3 | legacy / legacy | partial | Initializer succeeds; screenshot upload requires external credentials/network and was not run. | static archive, local browser, fake-Frame |
|
||||
| 73: EmuDecky 1.0.2-1 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: EmuDecky/bin/hello. | static archive, local browser, fake-Frame |
|
||||
| 74: KDE Connect 0.1.0-1 | legacy / legacy | partial | Initializer succeeds; KDE Connect installation/pairing/file transfers unverified. | static archive, local browser, fake-Frame |
|
||||
| 75: Reshadeck 0.2.0 | legacy / legacy | partial | Initializer succeeds; ReShade deployment into Proton games is unverified. | static archive, local browser, fake-Frame |
|
||||
| 76: CheatDeck 1.1.6 | 1 / 2 | broken | Frontend audit failed: Cannot convert undefined or null to object. | static archive, local browser, fake-Frame |
|
||||
| 77: BT Wake Control 1.2.0 | 1 / 2 | partial | Initializer succeeds; backend uses DMI board paths and Bluetooth wake permissions, unverified on ARM64 Frame. | static archive, local browser, fake-Frame |
|
||||
| 78: Brightness Bar 1.0.4 | legacy / legacy | N/A | Not supported on Frame: Targets Steam/QAM brightness shortcuts and a global Steam overlay; neither is attached to the Framey panel. | static archive, local browser, fake-Frame |
|
||||
| 79: Decky Notifications 1.0.1 | legacy / legacy | partial | Initializer succeeds; device/account setup and notification delivery unverified. | static archive, local browser, fake-Frame |
|
||||
| 80: MagicBlack 2.0.2 | 1 / 2 | N/A | Not supported on Frame: Targets a global Steam Deck screen-black overlay; the isolated panel cannot black out the Frame compositor. | static archive, local browser, fake-Frame |
|
||||
| 81: IsThereAnyDeal for Deck 1.1.1 | legacy / legacy | broken | Country lookup calls missing SteamClient.User.GetIPCountry; deal loading fails after initialization. | static archive, local browser, fake-Frame |
|
||||
| 82: MagicPods 2.0.21 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: MagicPodsDecky/bin/magicpodscore. Frontend audit failed: Audit blocks WebSocket. | static archive, local browser, fake-Frame |
|
||||
| 84: Junk-Store 1.1.13 | legacy / legacy | broken | Route registration succeeds; rendering fails on a missing Steam ContainerBackground component. | static archive, local browser, fake-Frame |
|
||||
| 85: Decky-Undervolt 1.0.8 | 1 / 2 | N/A | Not supported on Frame: RyzenAdj Curve Optimizer requires an AMD CPU; Frame is ARM64. | static archive, local browser, fake-Frame |
|
||||
| 87: ScreenSaver 1.1.2 | legacy / legacy | partial | Initializer succeeds; global screen overlay and idle/suspend integration have no verified VR target. | static archive, local browser, fake-Frame |
|
||||
| 88: steamdeck-input-disabler 1.0.2 | legacy / legacy | N/A | Not supported on Frame: Targets the built-in Steam Deck USB gamepad; Frame has different controllers. | static archive, local browser, fake-Frame |
|
||||
| 89: DeckySpy 0.6.7 | legacy / legacy | partial | Initializer succeeds; account tokens and external service integration were not exercised. | static archive, local browser, fake-Frame |
|
||||
| 90: Picture in Picture 1.0.0 | 1 / 2 | partial | Initializer succeeds; global picture-in-picture injection cannot escape the isolated panel. | static archive, local browser, fake-Frame |
|
||||
| 91: PlayCount 1.7 | 1 / 2 | partial | Initializer succeeds; Steam game-page play-count patch has no supported VR target. | static archive, local browser, fake-Frame |
|
||||
| 92: Crosshair 1.1.0 | 1 / 2 | partial | Initializer succeeds; global crosshair injection has no supported compositor target. | static archive, local browser, fake-Frame |
|
||||
| 93: Deck Settings 1.2.6 | 1 / 2 | broken | Initializer succeeds; game selection fails because SteamClient.InstallFolder.GetInstallFolders is unavailable. | static archive, local browser, fake-Frame |
|
||||
| 94: Decky DoT DNS 0.0.2 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 95: Decky-SpoofDPI 0.12.0 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-spoof-dpi/bin/spoofdpi. | static archive, local browser, fake-Frame |
|
||||
| 96: Simple Timer 1.0.0 | 1 / 2 | partial | Timer panel initializes; live timer persistence, notifications and audio are unverified. | static archive, local browser, fake-Frame |
|
||||
| 97: Decky-Framegen 0.11.15 | 1 / 2 | broken | Plugin archive size or SHA-256 did not match the store. | static archive, local browser, fake-Frame |
|
||||
| 98: Decky-Lookup 0.1.0 | 1 / 2 | partial | Initializer succeeds; game-page context actions and live lookups remain unverified. | static archive, local browser, fake-Frame |
|
||||
| 99: Decky Zerotier 0.3.1 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 100: Virtual Surround Sound 0.0.5 | 1 / 2 | partial | Initializer succeeds; avatar proxy is unavailable and live audio-routing backend unverified. | static archive, local browser, fake-Frame |
|
||||
| 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 | Frontend audit failed: Cannot read properties of null (reading 'GamepadUIAudio'). | 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 |
|
||||
| 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 |
|
||||
| 108: HueSync 3.8.0 | 1 / 2 | N/A | Not supported on Frame: Targets handheld RGB/LED controllers (AYANEO/Ally/MSI), not Frame hardware. | static archive, local browser, fake-Frame |
|
||||
| 109: MuraDeck 2.1.4 | 1 / 2 | N/A | Not supported on Frame: Calibration is specifically for defective Samsung Steam Deck OLED panels, not Frame displays. | static archive, local browser, fake-Frame |
|
||||
| 110: Dimmer Deck 0.0.4 | 1 / 2 | N/A | Not supported on Frame: Changes gamescope/X11 dimming properties; no mapping to Frame VR display brightness exists. | static archive, local browser, fake-Frame |
|
||||
| 111: Music Control 1.1.7 | legacy / legacy | partial | Panel initializer succeeds with backend/network blocked; actual controls and backend remain unverified. | static archive, local browser, fake-Frame |
|
||||
| 112: Better Keyboard 0.3.4 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. Frontend audit failed: window.SteamClient.UI.GetUIMode is not a function. | static archive, local browser, fake-Frame |
|
||||
| 113: Decky LSFG-VK 0.12.2 | 1 / 2 | partial | Initializer succeeds; runtime LSFG/Windows DLL downloads and Vulkan integration are not ARM64-validated. | static archive, local browser, fake-Frame |
|
||||
| 114: Network Sentinel 1.0.4 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 115: Decky Clash 1.3.0 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 116: Muon 0.2.1 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 117: DeckyParty File Manager 1.0.0 | legacy / legacy | partial | Initializer succeeds; privileged file operations and backend are unverified. No file changes were run. | static archive, local browser, fake-Frame |
|
||||
| 118: Speed Test 1.0.2 | 1 / 2 | partial | Initializer succeeds; automatic bandwidth tests were blocked by the audit network policy. | static archive, local browser, fake-Frame |
|
||||
| 119: Decky Clipboard 1.0.1 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-clipboard/bin/xclip. | static archive, local browser, fake-Frame |
|
||||
| 120: Reroll 0.2.1 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: Reroll/bin/memscan.so. Frontend audit failed: window.SteamClient.Apps.RegisterForGameActionUserRequest is not a function. | static archive, local browser, fake-Frame |
|
||||
| 121: DeckPass 1.1.1 | 1 / 2 | partial | Initializer succeeds; KeePass Flatpak discovery and secrets integration were not exercised. | static archive, local browser, fake-Frame |
|
||||
| 123: Steam Status 1.2.3 | 2 / 2 | partial | Settings/public status rendered locally; cache errors fixed. Read-only get_settings RPC returned actual Decky 3.3.0 defaults on ARM64 Frame. Persistence, live backend diagnostics and VR controls unverified. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 124: AlarMe 1.6.3 | 1 / 2 | partial | Initializer succeeds; timers, reminders, suspend inhibition and audio are unverified. | static archive, local browser, fake-Frame |
|
||||
| 125: Decky Localsend 0.4.5 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-localsend/bin/localsend-core. | static archive, local browser, fake-Frame |
|
||||
| 126: Launch Options 1.9.2 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-launch-options/bin/hello. Frontend audit failed: Cannot convert undefined or null to object. | static archive, local browser, fake-Frame |
|
||||
| 127: Decky Translator 0.9.0 | 1 / 2 | partial | Initializer succeeds; x86_64 download and hidraw/controller assumptions need an ARM64 audit. OCR/translation not exercised. | static archive, local browser, fake-Frame |
|
||||
| 128: Quick User Switcher 1.0.0 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 129: ShadowDeck 1.1.11 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 130: Games Prefix Manager 1.0.1 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 131: Download All 1.1.0 | 1 / 2 | broken | Initializer succeeds; Steam download registration is absent, so the primary action is unavailable. | static archive, local browser, fake-Frame |
|
||||
| 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 / unknown | broken | Frontend audit failed: Failed to resolve module specifier "./chunks/pluginApi-B8sV7970.js". Invalid relative url or base scheme isn't hierarchical.. | static archive, local browser, fake-Frame |
|
||||
| 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 |
|
||||
| 139: Output Manager 1.4.0 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 140: IPv6 Enabler Plugin 0.0.1 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 141: DeckyClocker 1.0.0 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 142: MuteOnSleep 1.0.1 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 143: Desktop Apps 0.1.0 | 1 / 2 | partial | Initializer succeeds; Flatpak app discovery and Steam shortcut changes unverified. | static archive, local browser, fake-Frame |
|
||||
| 144: Mixer 0.1.0 | 1 / 2 | partial | Initializer succeeds; actual audio mixer backend and volume changes unverified. | static archive, local browser, fake-Frame |
|
||||
| 145: VARTA 1.0.21 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
|
||||
## API, UI and backend inventory
|
||||
|
||||
DFL includes exports imported through decky-frontend-lib / @decky/ui. Steam stores and router/toaster/QAM signals are listed separately. No signal means the static scan did not detect one, not that no dependency exists. Native binaries are classified by ELF machine or Windows MZ header; Python files and source links are preserved in the JSON.
|
||||
|
||||
| Plugin | SteamClient methods | DFL / @decky/ui exports | Other Steam globals / UI surfaces | Backend | Hardware / OS signals |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| XR Gaming | none detected | ButtonItem, ConfirmModal, DialogButton, DialogButtonPrimary, DialogButtonSecondary, DropdownItem, Field, Focusable, ModalRoot, Navigation, PanelSection, PanelSectionRow, SliderField, Spinner, TextField, ToggleField, definePlugin, gamepadDialogClasses, showModal, staticClasses | window.SP_REACT | Python | Requires supported external XR glasses and the Breezy/XRLinuxDriver stack; it does not target Steam Frame displays. |
|
||||
| CSS Loader | none detected | ButtonItem, ColorPickerModal, ConfirmModal, DialogButton, DialogCheckbox, Dropdown, DropdownItem, Focusable, ModalRoot, Navigation, PanelSection, PanelSectionRow, ScrollPanelGroup, SidebarNavigation, SliderField, Tabs, TextField, Toggle, ToggleField, classMap, definePlugin, findModuleChild, gamepadDialogClasses, gamepadSliderClasses, getGamepadNavigationTrees, replacePatch, showModal, staticClasses | routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, QAM/global tabs, Steam page patches/module lookup | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| vibrantDeck | none detected | PanelSection, PanelSectionRow, Router, SliderField, ToggleField, definePlugin, staticClasses | Router, window.SP_REACT | Python | Changes X11 gamescope color properties, not the Frame SteamVR compositor. |
|
||||
| Fantastic | none detected | Field, Navigation, PanelSection, PanelSectionRow, ToggleField, definePlugin, staticClasses | window.SP_REACT, QAM/global tabs | Python + native payload; x86_64 | Targets Steam Deck fan hardware; no Frame fan-control mapping is provided. |
|
||||
| MusicControl | none detected | ButtonItem, DialogButton, Focusable, Menu, MenuItem, PanelSection, PanelSectionRow, Router, SidebarNavigation, SliderField, SteamSpinner, definePlugin, showContextMenu, staticClasses | Router, routerHook, window.SP_REACT, local/Steam routes | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Pause Games | Apps, Apps.RegisterForGameActionTaskChange, GameSessions.RegisterForAppLifetimeNotifications, System.RegisterForOnResumeFromSuspend, System.RegisterForOnSuspendRequest, System.UI.RegisterForFocusChangeEvents, System.UI.RegisterForSystemKeyEvents | Button, Field, Focusable, Marquee, PanelSection, PanelSectionRow, Router, Toggle, ToggleField, beforePatch, sleep | Router, window.SP_REACT, Steam page patches/module lookup, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| ProtonDB Badges | none detected | ButtonItem, ConfirmModal, DialogButton, DropdownItem, Field, Focusable, Navigation, PanelSection, PanelSectionRow, ScrollPanel, ScrollPanelGroup, ToggleField, afterPatch, appDetailsClasses, appDetailsHeaderClasses, createReactTreePatcher, definePlugin, findInReactTree, findModuleExport, showModal, staticClasses, useParams | SteamUIStore, appStore, routerHook, toaster, local/Steam routes, toasts, QAM/global tabs, Steam page patches/module lookup, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Audio Loader | GameSessions.RegisterForAppLifetimeNotifications, d.ts | ButtonItem, DialogButton, Dropdown, DropdownItem, Focusable, Navigation, PanelSection, PanelSectionRow, Router, SliderField, Tabs, TextField, ToggleField, afterPatch, beforePatch, definePlugin, findModuleChild, gamepadDialogClasses, staticClasses | Router, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, QAM/global tabs, Steam page patches/module lookup, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Bluetooth | none detected | DialogButton, Field, Focusable, PanelSection, PanelSectionRow, ToggleField, definePlugin, staticClasses | none detected | Python | /sys/class/power_supply |
|
||||
| Notebook | System.UI.RegisterForFocusChangeEvents | DialogButton, Dropdown, Marquee, PanelSection, PanelSectionRow, Router, definePlugin, staticClasses | Router, toaster, window.SP_REACT, toasts | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| PowerTools | Apps.RegisterForGameActionEnd, Apps.RegisterForGameActionStart, GameSessions.RegisterForAppLifetimeNotifications, User.GetLoginUsers, User.RegisterForCurrentUserChanges | ButtonItem, DialogButton, Dropdown, Field, Focusable, ModalRoot, Navigation, PanelSection, PanelSectionRow, QuickAccessTab, Router, SliderField, Spinner, TextField, ToggleField, definePlugin, showModal, staticClasses | Router, appStore, routerHook, window.SP_REACT, local/Steam routes, QAM/global tabs, Steam stores | Python + native payload; x86_64 | Targets AMD Ryzen power controls; Frame uses an ARM64 SoC. |
|
||||
| Memory Deck | none detected | ButtonItem, DialogButton, DropdownItem, Focusable, PanelSection, PanelSectionRow, Router, SteamSpinner, definePlugin, gamepadDialogClasses, joinClassNames, staticClasses | Router, routerHook, window.SP_REACT, local/Steam routes, Steam stores | Python + native payload; x86_64 | gamescope |
|
||||
| Animation Changer | none detected | ButtonItem, DialogButton, Dropdown, DropdownItem, Focusable, ModalRoot, Navigation, PanelSection, PanelSectionRow, Router, Spinner, Tabs, TextField, ToggleField, definePlugin, findModule, findSP, showModal, staticClasses | Router, routerHook, window.SP_REACT, local/Steam routes, QAM/global tabs, Steam page patches/module lookup | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| RadiYo! | none detected | ConfirmModal, DialogButton, Dropdown, Focusable, ModalRoot, Navigation, PanelSection, QuickAccessTab, Router, SliderField, Spinner, TextField, definePlugin, showModal, staticClasses | Router, window.SP_REACT | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| HLTB for Deck | none detected | ButtonItem, DialogButtonPrimary, DropdownItem, MenuItem, Navigation, PanelSection, PanelSectionRow, SliderField, SteamSpinner, ToggleField, afterPatch, appDetailsClasses, appDetailsHeaderClasses, createReactTreePatcher, definePlugin, fakeRenderComponent, findInReactTree, findInTree, findModuleChild, removeHookStubs, staticClasses | routerHook, local/Steam routes, Steam page patches/module lookup, Steam stores | no Python/native backend found | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| AutoSuspend | Downloads.RegisterForDownloadItems, Input.RegisterForControllerInputMessages, Settings.RegisterForSettingsChanges, System, System.RegisterForOnResumeFromSuspend, System.RegisterForOnSuspendRequest, System.ShutdownPC, URL.ExecuteSteamURL, User.GetLoginUsers, User.RegisterForShutdownDone | ConfirmModal, DialogBody, DialogButton, DialogControlsSection, DropdownItem, Field, Focusable, Navigation, SidebarNavigation, SliderField, TextField, ToggleField, definePlugin, findModuleChild, showModal, sleep, staticClasses | routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, QAM/global tabs, Steam page patches/module lookup, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Emuchievements | Apps.RegisterForAppDetails, GameSessions.RegisterForAppLifetimeNotifications, User.RegisterForLoginStateChange | ButtonItem, Dropdown, Field, Focusable, Navigation, PanelSection, PanelSectionRow, ProgressBarItem, ProgressBarWithInfo, Router, SidebarNavigation, TextField, ToggleField, afterPatch, beforePatch, callOriginal, definePlugin, findInTree, findModuleChild, replacePatch, sleep, staticClasses | Router, appDetailsStore, appStore, collectionStore, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, Steam page patches/module lookup, Steam stores | Python + native payload; x86_64 | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| TunnelDeck | none detected | ButtonItem, Field, Navigation, PanelSection, PanelSectionRow, ToggleField, definePlugin, staticClasses | window.SP_REACT | Python | systemctl |
|
||||
| AutoFlatpaks | InstallFolder.GetInstallFolders | ConfirmModal, DialogButton, Dropdown, Focusable, ModalRoot, PanelSection, PanelSectionRow, Router, ScrollPanel, SteamSpinner, Tabs, TextField, Toggle, ToggleField, definePlugin, findModuleChild, showModal, staticClasses | Router, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, QAM/global tabs, Steam page patches/module lookup, Steam stores | Python | flatpak; x86_64 |
|
||||
| MoonDeck | Apps.AddShortcut, Apps.CancelGameAction, Apps.GetResolutionOverrideForApp, Apps.RegisterForAppDetails, Apps.RegisterForGameActionEnd, Apps.RegisterForGameActionStart, Apps.RegisterForGameActionTaskChange, Apps.RemoveShortcut, Apps.RunGame, Apps.SetAppLaunchOptions, Apps.SetAppResolutionOverride, Apps.SetShortcutName, Apps.SetThirdPartyControllerConfiguration, Apps.TerminateApp, Apps.ToggleOverrideResolutionForInternalDisplay, GameSessions.RegisterForAppLifetimeNotifications, System.Audio.GetDevices, System.DisplayManager.RegisterForStateChanges, User.RegisterForLoginStateChange, User.StartRestart | Button, ButtonItem, ConfirmModal, DialogBody, DialogButton, DialogButtonPrimary, DialogButtonSecondary, DialogControlsSection, DialogControlsSectionHeader, DialogHeader, Dropdown, Field, Focusable, Menu, MenuItem, ModalRoot, NavEntryPositionPreferences, Navigation, PanelSection, PanelSectionRow, SidebarNavigation, Spinner, TextField, ToggleField, afterPatch, appDetailsClasses, appDetailsHeaderClasses, basicAppDetailsSectionStylerClasses, createReactTreePatcher, findInReactTree, findModuleExport, findSP, joinClassNames, playSectionClasses, showContextMenu, showModal, sleep, staticClasses | appStore, collectionStore, routerHook, toaster, local/Steam routes, toasts, QAM/global tabs, Steam page patches/module lookup, Steam stores | Python + native payload; x86_64 | /dev/input; /sys/socket.h; amd64; flatpak; x86_64 |
|
||||
| SteamGridDB | Apps.ClearCustomArtworkForApp, Apps.RegisterForAppDetails, Apps.SetCustomArtworkForApp, Apps.SetShortcutName, User.StartRestart | ConfirmModal, ControlsList, DialogBody, DialogBodyText, DialogButton, DialogButtonPrimary, DialogCheckbox, DialogControlsSection, DialogControlsSectionHeader, DialogFooter, DialogHeader, Dropdown, Field, Focusable, IconsModule, Marquee, Menu, MenuItem, ModalRoot, Navigation, PanelSection, PanelSectionRow, ProgressBar, ReorderableList, SliderField, Spinner, Tabs, TextField, ToggleField, afterPatch, appDetailsHeaderClasses, callOriginal, createModuleMapping, definePlugin, fakeRenderComponent, findClassModule, findInReactTree, findInTree, findModule, findModuleByExport, findModuleExport, findSP, joinClassNames, quickAccessMenuClasses, replacePatch, showContextMenu, showModal, useParams, wrapReactType | appDetailsStore, appStore, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, QAM/global tabs, Steam page patches/module lookup, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Discord Status | Apps.RegisterForAppDetails, Apps.RunGame, GameSessions.RegisterForAppLifetimeNotifications, User.RegisterForPrepareForSystemSuspendProgress, User.RegisterForResumeSuspendedGamesProgress | ButtonItem, DropdownItem, Field, PanelSection, PanelSectionRow, Router, Spinner, sleep, staticClasses | Router, appStore, toaster, window.SP_REACT, toasts, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Volume Mixer | none detected | DialogButton, Focusable, PanelSection, PanelSectionRow, SliderField, definePlugin, staticClasses | window.SP_REACT | Python | sudo |
|
||||
| Controller Tools | none detected | ButtonItem, PanelSection, PanelSectionRow, ToggleField, definePlugin, gamepadDialogClasses, joinClassNames, staticClasses | toaster, window.SP_REACT, toasts | Python + native payload; x86_64 | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Tailscale Control | none detected | ButtonItem, ConfirmModal, DropdownItem, Field, Focusable, PanelSection, PanelSectionRow, SuspensefulImage, TextField, ToggleField, definePlugin, showModal, staticClasses | window.SP_REACT | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| DeckMTP | none detected | PanelSection, PanelSectionRow, ToggleField, definePlugin, staticClasses | window.SP_REACT | Python + native payload; x86_64 | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Steamback | GameSessions.RegisterForAppLifetimeNotifications, InstallFolder.GetInstallFolders | ButtonItem, ConfirmModal, Navigation, PanelSection, PanelSectionRow, Router, definePlugin, showModal, staticClasses | Router, appStore, toaster, window.SP_REACT, toasts, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Quick Launch | Apps.AddShortcut, Apps.RunGame, Apps.SetCustomArtworkForApp, Apps.SetShortcutExe, Apps.SetShortcutLaunchOptions, Apps.SetShortcutName, Apps.SpecifyCompatTool | ButtonItem, ConfirmModal, DialogButton, Dropdown, Focusable, ModalRoot, PanelSection, PanelSectionRow, TextField, ToggleField, definePlugin, showModal, staticClasses | appStore, toaster, window.SP_REACT, toasts, Steam stores | Python | flatpak |
|
||||
| Decky Cloud Save | GameSessions.RegisterForAppLifetimeNotifications, Settings, Settings.GetCurrentLanguage | ButtonItem, ConfirmModal, Menu, MenuItem, Navigation, PanelSection, PanelSectionRow, TextField, ToggleField, definePlugin, showContextMenu, showModal, sleep, staticClasses | appStore, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, Steam stores | Python + native payload; x86_64 | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Storage Cleaner | Apps.RegisterForAppDetails | ButtonItem, ConfirmModal, DialogCheckbox, PanelSection, PanelSectionRow, definePlugin, showModal, staticClasses | window.SP_REACT | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Game Theme Music | GameSessions.RegisterForAppLifetimeNotifications | ButtonItem, ConfirmModal, DialogButton, DropdownItem, Field, Focusable, Menu, MenuItem, ModalRoot, Navigation, PanelSection, PanelSectionRow, ProgressBarWithInfo, SliderField, SteamSpinner, Tabs, TextField, ToggleField, afterPatch, appDetailsClasses, createReactTreePatcher, definePlugin, fakeRenderComponent, findInReactTree, findInTree, findModuleChild, showContextMenu, showModal, staticClasses, useParams | appStore, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, Steam page patches/module lookup, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Shotty | GameSessions.RegisterForScreenshotNotification | ButtonItem, PanelSection, PanelSectionRow, Router, definePlugin, staticClasses | Router, appStore, toaster, window.SP_REACT, toasts, Steam stores | Python | gamescope |
|
||||
| MangoPeel | none detected | ButtonItem, ConfirmModal, DropdownItem, Field, Focusable, Marquee, PanelSection, PanelSectionRow, Router, SliderField, Tabs, TextField, ToggleField, definePlugin, findModuleChild, showModal, sleep, staticClasses | Router, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, Steam page patches/module lookup, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Volume Boost | none detected | PanelSection, PanelSectionRow, SliderField, ToggleField, definePlugin, staticClasses | window.SP_REACT | Python | pactl |
|
||||
| Web Browser | GameSessions.RegisterForAppLifetimeNotifications, Input.ControllerKeyboardSetKeyState, Overlay.RegisterForOverlayActivated, System, System.RegisterForOnResumeFromSuspend | ButtonItem, ConfirmModal, DialogBodyText, DialogButton, DialogButtonSecondary, DialogSubHeader, DropdownItem, Field, Focusable, GamepadButton, Marquee, Menu, MenuGroup, MenuItem, ModalPosition, ModalRoot, Navigation, PanelSection, PanelSectionRow, Router, SimpleModal, SteamSpinner, Tabs, TextField, ToggleField, afterPatch, definePlugin, findClassModule, findInReactTree, findModuleChild, gamepadContextMenuClasses, gamepadDialogClasses, gamepadTabbedPageClasses, getFocusNavController, getReactRoot, quickAccessControlsClasses, replacePatch, showContextMenu, showModal, sleep, staticClasses | Router, routerHook, window.SP_REACT, local/Steam routes, QAM/global tabs, Steam page patches/module lookup, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| DeckyFileServer | none detected | ButtonItem, DialogBody, DialogButton, DialogFooter, DialogHeader, DialogSubHeader, DropdownItem, Field, Focusable, ModalRoot, PanelSection, PanelSectionRow, TextField, Toggle, ToggleField, definePlugin, showModal, staticClasses | window.SP_REACT | Python + native payload; x86_64 | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| TabMaster | Apps.GetStoreTagLocalization, GameSessions.RegisterForAppLifetimeNotifications, InstallFolder.RegisterForInstallFolderChanges, System, System.RegisterForOnResumeFromSuspend, System.RestartPC, User.RegisterForLoginStateChange | Button, ButtonItem, ConfirmModal, DialogBody, DialogButton, DialogCheckbox, DialogControlsSection, DialogHeader, Dropdown, DropdownItem, Field, Focusable, GamepadButton, Marquee, Menu, MenuGroup, MenuItem, ModalPosition, ModalRoot, Navigation, PanelSection, PanelSectionRow, ReorderableList, ScrollPanelGroup, SidebarNavigation, SliderField, TextField, ToggleField, achievementClasses, afterPatch, findInReactTree, findInTree, findModuleExport, gamepadContextMenuClasses, gamepadDialogClasses, gamepadSliderClasses, gamepadTabbedPageClasses, joinClassNames, mainMenuAppRunningClasses, quickAccessControlsClasses, quickAccessMenuClasses, replacePatch, scrollPanelClasses, showContextMenu, showModal, sleep, wrapReactType | DeckyPluginLoader, appStore, collectionStore, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, QAM/global tabs, Steam page patches/module lookup, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| TS3 QuickAccess | none detected | DialogBody, DialogButton, DialogFooter, DialogHeader, Dropdown, Field, Focusable, ModalRoot, PanelSection, PanelSectionRow, SliderField, TextField, definePlugin, showModal | none detected | Python + native payload; x86_64 | flatpak |
|
||||
| Free Loader | none detected | ButtonItem, ConfirmModal, DialogButton, Focusable, Navigation, PanelSection, PanelSectionRow, QuickAccessTab, SliderField, SteamSpinner, ToggleField, showModal | DeckyPluginLoader, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Syncthing | none detected | DialogBody, DialogButton, DialogControlsSection, DialogControlsSectionHeader, DialogHeader, Dropdown, Field, Focusable, ModalRoot, Navigation, PanelSection, PanelSectionRow, ProgressBarItem, QuickAccessTab, Router, SidebarNavigation, Spinner, SteamSpinner, TextField, Toggle, definePlugin, showModal, sleep, staticClasses | Router, routerHook, window.SP_REACT, local/Steam routes | Python + native payload; x86_64 | flatpak; gamescope |
|
||||
| Decky Terminal | none detected | DialogButton, Dropdown, Field, Focusable, GamepadButton, PanelSection, Router, SidebarNavigation, SteamSpinner, TextField, ToggleField, definePlugin, staticClasses, useParams | Router, routerHook, window.SP_REACT, local/Steam routes, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| PlayTime | Apps.RegisterForAppDetails, Input.RegisterForControllerInputMessages, Storage.GetJSON, Storage.SetObject, System, System.RegisterForOnResumeFromSuspend, System.RegisterForOnSuspendRequest, User, User.RegisterForLoginStateChange, User.RegisterForPrepareForSystemSuspendProgress, User.RegisterForResumeSuspendedGamesProgress | ButtonItem, ConfirmModal, DialogButton, Dropdown, Field, Focusable, Menu, MenuGroup, MenuItem, ModalRoot, Navigation, PanelSection, PanelSectionRow, ScrollPanelGroup, SidebarNavigation, Tabs, TextField, afterPatch, definePlugin, findClass, findSP, showContextMenu, showModal, staticClasses, useParams | SteamUIStore, appStore, collectionStore, routerHook, toaster, local/Steam routes, toasts, QAM/global tabs, Steam page patches/module lookup, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Battery Tracker | none detected | PanelSection, PanelSectionRow, Router, SliderField, definePlugin, staticClasses | Router, routerHook, window.SP_REACT, local/Steam routes | Python | /sys/class/power_supply/BAT1/capacity; /sys/class/power_supply/BAT1/current_now; /sys/class/power_supply/BAT1/status; /sys/class/power_supply/BAT1/voltage_now |
|
||||
| Wine Cellar | Apps.GetAvailableCompatTools, Apps.SpecifyCompatTool, InstallFolder.GetInstallFolders, Settings.GetGlobalCompatTools, User.StartRestart | ButtonItem, ConfirmModal, DialogBody, DialogButton, DialogControlsSection, DialogControlsSectionHeader, Dropdown, Field, Focusable, GamepadButton, Menu, MenuItem, ModalRoot, Navigation, PanelSection, PanelSectionRow, ProgressBar, Router, ScrollPanelGroup, SidebarNavigation, TextField, definePlugin, showContextMenu, showModal, staticClasses | Router, appStore, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, Steam stores | Python + native payload; x86_64 | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| MicroSDeck | none detected | ConfirmModal, DialogButton, DialogCheckbox, Field, Focusable, Menu, MenuItem, ModalPosition, Navigation, PanelSection, ReorderableList, ScrollPanelGroup, SideMenu, SidebarNavigation, SimpleModal, TextField, ToggleField, afterPatch, appDetailsClasses, createReactTreePatcher, definePlugin, findInReactTree, findModule, findModuleChild, gamepadDialogClasses, quickAccessControlsClasses, showContextMenu, showModal, staticClasses | collectionStore, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, Steam page patches/module lookup, Steam stores | Python + native payload; x86_64 | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| ScreenshotUploader | GameSessions.RegisterForScreenshotNotification, Screenshots.UploadLocalScreenshot | DropdownItem, Field, PanelSection, PanelSectionRow, ToggleField, definePlugin, findModuleChild, staticClasses | toaster, window.SP_REACT, toasts, Steam page patches/module lookup | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| EmuDecky | none detected | ButtonItem, DialogButton, Dropdown, Menu, MenuItem, PanelSection, PanelSectionRow, Router, SteamSpinner, ToggleField, definePlugin, showContextMenu, staticClasses | Router, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts | Python + native payload; x86_64 | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| KDE Connect | none detected | PanelSection, PanelSectionRow, ToggleField, definePlugin, staticClasses | window.SP_REACT | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Reshadeck | Input.RegisterForControllerStateChanges, System.RegisterForOnResumeFromSuspend, System.RegisterForOnSuspendRequest | ButtonItem, Dropdown, PanelSection, PanelSectionRow, definePlugin, staticClasses | toaster, window.SP_REACT, toasts | Python | gamescope |
|
||||
| CheatDeck | Apps.RegisterForAppDetails, Apps.SetAppLaunchOptions | DialogBodyText, DialogButton, DialogHeader, Dropdown, Field, Focusable, MenuItem, ModalRoot, Navigation, PanelSection, PanelSectionRow, SidebarNavigation, TextField, ToggleField, afterPatch, definePlugin, fakeRenderComponent, findInReactTree, findInTree, findModuleByExport, showModal, staticClasses, useParams | routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, Steam page patches/module lookup | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| BT Wake Control | none detected | ButtonItem, IconsModule, PanelSection, PanelSectionRow, ToggleField, definePlugin, staticClasses | toaster, window.SP_REACT, toasts | Python | /sys/devices/virtual/dmi/id/board_name; /sys/devices/virtual/dmi/id/board_vendor; bluetoothctl |
|
||||
| Brightness Bar | System.Display.RegisterForBrightnessChanges | ButtonItem, ColorPickerModal, PanelSection, PanelSectionRow, SliderField, ToggleField, definePlugin, findModuleChild, showModal, staticClasses | routerHook, window.SP_REACT, local/Steam routes, QAM/global tabs, Steam page patches/module lookup | Python | Targets Steam/QAM brightness shortcuts and a global Steam overlay; neither is attached to the Framey panel. |
|
||||
| Decky Notifications | none detected | ButtonItem, Menu, MenuItem, PanelSection, PanelSectionRow, definePlugin, showContextMenu, staticClasses | DeckyPluginLoader, toaster, window.SP_REACT, toasts | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| MagicBlack | System.Audio.GetDevices, System.Audio.SetDeviceVolume | DialogButton, Field, Focusable, ModalRoot, Navigation, PanelSection, PanelSectionRow, SliderField, ToggleField, definePlugin, findModuleChild, findModuleExport, gamepadDialogClasses, joinClassNames, quickAccessMenuClasses, showModal, staticClasses | SteamUIStore, routerHook, window.SP_REACT, local/Steam routes, QAM/global tabs, Steam page patches/module lookup, Steam stores | Python | Targets a global Steam Deck screen-black overlay; the isolated panel cannot black out the Frame compositor. |
|
||||
| IsThereAnyDeal for Deck | User.GetIPCountry | DialogButton, DropdownItem, Field, Focusable, ModalRoot, Navigation, PanelSection, PanelSectionRow, ToggleField, definePlugin, findModuleChild, showModal, staticClasses | routerHook, window.SP_REACT, local/Steam routes, QAM/global tabs, Steam page patches/module lookup, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| MagicPods | Input.RegisterForControllerStateChanges, System.Audio.GetDevices, System.Audio.RegisterForDeviceVolumeChanged, System.Audio.SetDeviceVolume, System.Bluetooth, System.Bluetooth.RegisterForStateChanges, System.Bluetooth.SetEnabled | ButtonItem, DialogButton, Field, Focusable, Menu, MenuItem, ModalRoot, Navigation, PanelSection, PanelSectionRow, Router, SliderField, Tabs, ToggleField, afterPatch, definePlugin, findClassModule, findModuleChild, findModuleDetailsByExport, findSP, gamepadDialogClasses, gamepadSliderClasses, getGamepadNavigationTrees, joinClassNames, quickAccessMenuClasses, showContextMenu, showModal, staticClasses | Router, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, QAM/global tabs, Steam page patches/module lookup, Steam stores | Python + native payload; x86_64 | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Junk-Store | Apps.AddShortcut, Apps.GetAvailableCompatTools, Apps.RemoveShortcut, Apps.RunGame, Apps.SetAppLaunchOptions, Apps.SetCustomArtworkForApp, Apps.SetShortcutExe, Apps.SetShortcutLaunchOptions, Apps.SetShortcutName, Apps.SetShortcutStartDir, Apps.SpecifyCompatTool, GameSessions.RegisterForAppLifetimeNotifications, Input.RegisterForControllerInputMessages, Installs.OpenInstallWizard | Button, ButtonItem, ConfirmModal, DialogBody, DialogButton, DialogControlsSection, DialogLabel, Dropdown, Field, Focusable, GamepadButton, Marquee, Menu, MenuItem, ModalPosition, ModalRoot, Navigation, PanelSection, PanelSectionRow, ProgressBar, ScrollPanelGroup, SidebarNavigation, SliderField, Spinner, SteamSpinner, Tabs, TextField, ToggleField, afterPatch, definePlugin, findClassModule, gamepadDialogClasses, gamepadTabbedPageClasses, joinClassNames, quickAccessControlsClasses, quickAccessMenuClasses, scrollPanelClasses, showContextMenu, showModal, sleep, staticClasses, useParams | Router, SteamUIStore, appDetailsStore, appStore, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, QAM/global tabs, Steam page patches/module lookup, Steam stores | Python | sudo |
|
||||
| Decky-Undervolt | GameSessions.RegisterForAppLifetimeNotifications, System.RegisterForOnResumeFromSuspend | ButtonItem, DialogButton, DropdownItem, Navigation, PanelSection, PanelSectionRow, Router, SidebarNavigation, SliderField, TextField, ToggleField, definePlugin | Router, appStore, routerHook, window.SP_REACT, local/Steam routes, Steam stores | Python + native payload; x86_64 | RyzenAdj Curve Optimizer requires an AMD CPU; Frame is ARM64. |
|
||||
| ScreenSaver | Input.RegisterForControllerInputMessages, Input.RegisterForControllerStateChanges, Settings.SetSetting, System.RegisterForOnSuspendRequest, System.UpdateSettings, User.RegisterForPrepareForSystemSuspendProgress | PanelSection, PanelSectionRow, ToggleField, definePlugin, findModuleChild, staticClasses | toaster, window.SP_REACT, toasts, Steam page patches/module lookup | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| steamdeck-input-disabler | none detected | PanelSection, PanelSectionRow, ToggleField, definePlugin, staticClasses | window.SP_REACT | Python | Targets the built-in Steam Deck USB gamepad; Frame has different controllers. |
|
||||
| DeckySpy | System.RegisterForOnResumeFromSuspend | ButtonItem, Field, PanelSection, PanelSectionRow, Router, SliderField, ToggleField, definePlugin, staticClasses | Router, toaster, window.SP_REACT, toasts | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Picture in Picture | none detected | ButtonItem, ConfirmModal, DropdownItem, PanelSection, PanelSectionRow, Router, SliderField, TextField, ToggleField, findModuleChild, getGamepadNavigationTrees, quickAccessMenuClasses, showModal | Router, routerHook, window.SP_REACT, local/Steam routes, QAM/global tabs, Steam page patches/module lookup, Steam stores | no Python/native backend found | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| PlayCount | none detected | Button, ButtonItem, DialogBody, DialogButton, DialogControlsSection, DialogHeader, DropdownItem, ModalRoot, Navigation, PanelSection, PanelSectionRow, SliderField, Tabs, ToggleField, afterPatch, appDetailsClasses, createReactTreePatcher, definePlugin, findInReactTree, findModuleChild, showModal, staticClasses | routerHook, window.SP_REACT, local/Steam routes, QAM/global tabs, Steam page patches/module lookup, Steam stores | no Python/native backend found | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Crosshair | none detected | ButtonItem, PanelSection, PanelSectionRow, staticClasses | window.SP_REACT, QAM/global tabs | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Deck Settings | InstallFolder.GetInstallFolders, System.GetSystemInfo | ButtonItem, ConfirmModal, DialogBody, DialogButton, Dropdown, Field, Focusable, GamepadButton, ModalRoot, Navigation, PanelSection, PanelSectionRow, Router, ScrollPanelGroup, SliderField, Spinner, TextField, ToggleField, gamepadDialogClasses, scrollPanelClasses, showModal, staticClasses | Router, collectionStore, toaster, window.SP_REACT, toasts, QAM/global tabs, Steam stores | Python | /sys/class/dmi/id/sys_vendor; /sys/class/dmi/id/sys_vendor, |
|
||||
| Decky DoT DNS | none detected | ButtonItem, Dropdown, PanelSection, PanelSectionRow, TextField, ToggleField, staticClasses | window.SP_REACT | Python | systemctl |
|
||||
| Decky-SpoofDPI | User.StartRestart | ButtonItem, PanelSection, PanelSectionRow, TextField, ToggleField, staticClasses | toaster, window.SP_REACT, toasts | Python + native payload; x86_64 | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Simple Timer | none detected | Button, ButtonItem, ConfirmModal, Focusable, PanelSection, PanelSectionRow, ToggleField, findModuleChild, showModal, staticClasses | toaster, window.SP_REACT, toasts, Steam page patches/module lookup | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Decky-Framegen | none detected | ButtonItem, ConfirmModal, PanelSection, PanelSectionRow, showModal | toaster, window.SP_REACT, toasts | Python + native payload; Windows | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Decky-Lookup | none detected | ButtonItem, PanelSection, PanelSectionRow, Router, staticClasses | Router, window.SP_REACT | no Python/native backend found | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Decky Zerotier | none detected | ConfirmModal, DialogBody, DialogButton, DialogSubHeader, Field, Focusable, PanelSection, PanelSectionRow, TextField, ToggleField, showModal | Router, toaster, window.SP_REACT, toasts | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Virtual Surround Sound | none detected | ConfirmModal, DialogButton, Dropdown, Field, Focusable, ModalRoot, Navigation, PanelSection, PanelSectionRow, Router, SliderField, ToggleField, showModal, staticClasses | Router, window.SP_REACT, QAM/global tabs | Python | pactl |
|
||||
| XIVOmega | none detected | ButtonItem, Menu, MenuItem, Navigation, PanelSection, PanelSectionRow, Router, SidebarNavigation, Spinner, TextField, ToggleField, showContextMenu, staticClasses | Router, routerHook, window.SP_REACT, local/Steam routes | Python | /sys/net/ethertypes.h; /sys/net/if_pflog.h; /sys/net/pfvar.h; /sys/socket.h.auto.html; /sys/sys/socket.h; sudo; x86_64 |
|
||||
| DeckSP | Input.ControllerKeyboardSetKeyState, System, System.Audio.GetDevices, System.Audio.RegisterForDeviceAdded, UI.RegisterForUIModeChanged, User.RegisterForShutdownStart | ButtonItem, ConfirmModal, DialogButton, Dropdown, DropdownItem, EUIMode, Field, Focusable, GamepadButton, Marquee, Menu, MenuGroup, MenuItem, ModalPosition, ModalRoot, Navigation, PanelSection, PanelSectionRow, ScrollPanelGroup, SimpleModal, SliderField, TextField, ToggleField, findModuleExport, gamepadContextMenuClasses, gamepadDialogClasses, quickAccessControlsClasses, quickAccessMenuClasses, scrollPanelClasses, showContextMenu, showModal, sleep | SteamUIStore, appStore, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, QAM/global tabs, Steam page patches/module lookup, Steam stores | Python | flatpak; systemctl |
|
||||
| Decky Ludusavi | Apps.GetLaunchOptionsForApp, GameSessions.RegisterForAppLifetimeNotifications | ButtonItem, ConfirmModal, DialogButton, Dropdown, Focusable, Navigation, PanelSection, PanelSectionRow, TextField, ToggleField, showModal | appStore, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, Steam stores | Python | flatpak |
|
||||
| Game Sync | GameSessions.RegisterForAppLifetimeNotifications, GameSessions.RegisterForScreenshotNotification, Screenshots.DeleteLocalScreenshot | ButtonItem, ConfirmModal, DialogButton, Field, Focusable, Menu, MenuItem, Navigation, PanelSection, PanelSectionRow, ReorderableList, SidebarNavigation, TextField, ToggleField, afterPatch, fakeRenderComponent, findInReactTree, findInTree, findModuleByExport, showContextMenu, showModal, sleep, useParams | appStore, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, QAM/global tabs, Steam page patches/module lookup, Steam stores | Python | amd64 |
|
||||
| WiFi Locker | none detected | ButtonItem, Field, PanelSection, PanelSectionRow, staticClasses | toaster, window.SP_REACT, toasts | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| LetMeReShade | Apps.SetAppLaunchOptions | ButtonItem, ConfirmModal, DropdownItem, PanelSection, PanelSectionRow, ToggleField, showModal | window.SP_REACT | Python | /sys/devices/virtual/dmi/id/board_name; /sys/devices/virtual/dmi/id/product_name; /sys/devices/virtual/dmi/id/sys_vendor |
|
||||
| Decky Sunshine | none detected | ButtonItem, DialogButton, DialogButtonPrimary, Field, ModalRoot, PanelSection, PanelSectionRow, Spinner, TextField, definePlugin, showModal | window.SP_REACT | Python | flatpak |
|
||||
| HueSync | Settings.GetCurrentLanguage, System.GetSystemInfo, System.RegisterForBatteryStateChanges, System.RegisterForOnResumeFromSuspend, System.RegisterForOnSuspendRequest, User.RegisterForPrepareForSystemSuspendProgress, User.RegisterForResumeSuspendedGamesProgress | DialogButton, DropdownItem, Field, Focusable, Marquee, ModalRoot, PanelSection, PanelSectionRow, Router, SliderField, TextField, ToggleField, definePlugin, findModuleChild, gamepadSliderClasses, showModal, staticClasses | Router, toaster, window.SP_REACT, toasts, Steam page patches/module lookup, Steam stores | Python + native payload; x86_64 | Targets handheld RGB/LED controllers (AYANEO/Ally/MSI), not Frame hardware. |
|
||||
| MuraDeck | System.Display.RegisterForBrightnessChanges | ButtonItem, ConfirmModal, Field, Focusable, Navigation, PanelSection, PanelSectionRow, SideMenu, SidebarNavigation, SliderField, ToggleField, quickAccessControlsClasses, showModal | SteamUIStore, routerHook, window.SP_REACT, local/Steam routes, Steam stores | Python | Calibration is specifically for defective Samsung Steam Deck OLED panels, not Frame displays. |
|
||||
| Dimmer Deck | Settings.GetCurrentLanguage, System.Display.RegisterForBrightnessChanges, System.Display.SetBrightness | PanelSection, PanelSectionRow, SliderField, ToggleField, staticClasses | toaster, window.SP_REACT, toasts, Steam stores | Python | Changes gamescope/X11 dimming properties; no mapping to Frame VR display brightness exists. |
|
||||
| Music Control | none detected | ButtonItem, DialogButton, Focusable, PanelSection, PanelSectionRow, Router, SidebarNavigation, SliderField, SteamSpinner, definePlugin, staticClasses | Router, routerHook, window.SP_REACT, local/Steam routes | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Better Keyboard | Settings.GetCurrentLanguage, UI.GetUIMode, UI.RegisterForUIModeChanged | ButtonItem, EUIMode, PanelSection, PanelSectionRow, ToggleField, staticClasses | SteamUIStore, window.SP_REACT, Steam stores | Python | /dev/input |
|
||||
| Decky LSFG-VK | none detected | ButtonItem, ConfirmModal, DialogBody, DialogButton, DialogControlsSection, DialogControlsSectionHeader, DialogHeader, Dropdown, Field, Focusable, ModalRoot, PanelSection, PanelSectionRow, Router, SliderField, Spinner, TextField, Toggle, ToggleField, showModal, staticClasses | Router, toaster, window.SP_REACT, toasts | Python | flatpak; x86_64 |
|
||||
| Network Sentinel | none detected | ButtonItem, Navigation, PanelSection, PanelSectionRow, SliderField, ToggleField, staticClasses | toaster, window.SP_REACT, toasts | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Decky Clash | Settings.GetCurrentLanguage | ButtonItem, ConfirmModal, DialogBody, DialogButton, DialogControlsSection, DialogControlsSectionHeader, Dropdown, DropdownItem, Field, Focusable, GamepadButton, Menu, MenuItem, Navigation, PanelSection, PanelSectionRow, Router, SidebarNavigation, SliderField, Spinner, TextField, ToggleField, showContextMenu, showModal, staticClasses | Router, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Muon | System, System.RegisterForOnResumeFromSuspend, System.RegisterForOnSuspendRequest | ButtonItem, DialogButton, Field, Focusable, ModalRoot, PanelSection, PanelSectionRow, TextField, Toggle, findModuleExport, showModal, staticClasses | toaster, window.SP_REACT, toasts, Steam page patches/module lookup | Python | sudo; systemctl |
|
||||
| DeckyParty File Manager | none detected | PanelSection, PanelSectionRow, ToggleField, definePlugin, staticClasses | window.SP_REACT | Python | sudo |
|
||||
| Speed Test | none detected | ButtonItem, PanelSection, PanelSectionRow, staticClasses | none detected | no Python/native backend found | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Decky Clipboard | none detected | ButtonItem, DialogBody, DialogButton, DialogControlsSection, DialogControlsSectionHeader, Field, Navigation, PanelSection, PanelSectionRow, Router, SidebarNavigation, SliderField, TextField, ToggleField, staticClasses | Router, routerHook, toaster, local/Steam routes, toasts | Python + native payload; x86_64 | gamescope; sudo |
|
||||
| Reroll | Apps.RegisterForGameActionUserRequest, GameSessions.RegisterForAppLifetimeNotifications, Settings.GetCurrentLanguage | ButtonItem, DialogButton, Field, Focusable, Marquee, ModalRoot, PanelSection, PanelSectionRow, SliderField, libraryAssetImageClasses, mainMenuAppRunningClasses, quickAccessMenuClasses, showModal | SteamUIStore, QAM/global tabs, Steam stores | Python + native payload; x86_64 | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| DeckPass | Apps.AddShortcut, Apps.RunGame, Apps.SetCustomArtworkForApp, Apps.SetShortcutExe, Apps.SetShortcutLaunchOptions, Apps.SetShortcutName, Input.ControllerKeyboardSendText | Button, ButtonItem, DialogButton, Field, Focusable, ModalRoot, PanelSection, PanelSectionRow, Router, TextField, findSP, showModal, sleep, staticClasses | Router, appDetailsStore, appStore, toaster, window.SP_REACT, toasts, Steam stores | Python | flatpak |
|
||||
| Steam Status | none detected | ButtonItem, ConfirmModal, DialogButton, Field, Focusable, Navigation, PanelSection, PanelSectionRow, SliderField, ToggleField, definePlugin, showModal, staticClasses | toaster, toasts | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| AlarMe | GameSessions.RegisterForAppLifetimeNotifications | ButtonItem, ConfirmModal, Dropdown, Focusable, Navigation, PanelSection, PanelSectionRow, ProgressBarWithInfo, SidebarNavigation, SliderField, TextField, ToggleField, findModuleChild, showModal, staticClasses | routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, QAM/global tabs, Steam page patches/module lookup | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Decky Localsend | none detected | ButtonItem, DialogBody, DialogButton, DialogButtonPrimary, DialogFooter, DialogHeader, Field, Focusable, ModalRoot, Navigation, PanelSection, PanelSectionRow, Router, SidebarNavigation, TextField, ToggleField, showModal, staticClasses | Router, routerHook, toaster, local/Steam routes, toasts, Steam stores | Python + native payload; x86_64 | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Launch Options | Apps.RegisterForAppDetails, Apps.SetAppLaunchOptions | ButtonItem, ConfirmModal, DialogBody, DialogButton, DialogButtonPrimary, DialogControlsSection, DialogControlsSectionHeader, DialogFooter, DialogHeader, Dropdown, Field, Focusable, MenuItem, ModalRoot, Navigation, PanelSection, PanelSectionRow, ScrollPanel, SidebarNavigation, SteamSpinner, Tabs, TextField, Toggle, ToggleField, afterPatch, createReactTreePatcher, fakeRenderComponent, findInReactTree, findInTree, findModule, findModuleByExport, showModal, staticClasses, useParams | SteamUIStore, routerHook, local/Steam routes, Steam page patches/module lookup, Steam stores | Python + native payload; x86_64 | flatpak; gamescope |
|
||||
| Decky Translator | User.RegisterForPrepareForSystemSuspendProgress | ButtonItem, DialogButton, Dropdown, DropdownItem, Field, Focusable, ModalRoot, Navigation, PanelSection, PanelSectionRow, Router, SliderField, Tabs, TextField, ToggleField, findModuleChild, showModal, staticClasses | Router, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, QAM/global tabs, Steam page patches/module lookup | Python | /dev/hidraw; /sys/class/hidraw/hidraw{i}; /sys/class/hidraw/hidraw{i}/device/uevent; x86_64 |
|
||||
| Quick User Switcher | none detected | ButtonItem, ConfirmModal, Focusable, PanelSection, PanelSectionRow, ToggleField, afterPatch, showModal, staticClasses, wrapReactType | Router, routerHook, local/Steam routes, Steam page patches/module lookup | Python | sudo |
|
||||
| ShadowDeck | none detected | ButtonItem, DialogButton, DropdownItem, Focusable, PanelSection, PanelSectionRow, TextField, Toggle, showModal, staticClasses | toaster, toasts | Python | amd64 |
|
||||
| Games Prefix Manager | none detected | ButtonItem, DialogButton, Focusable, ModalRoot, PanelSection, PanelSectionRow, TextField, definePlugin, showModal, staticClasses | none detected | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Download All | Downloads, Downloads.RegisterForDownloadItems | ButtonItem, PanelSection, PanelSectionRow, SliderField | appStore, toaster, toasts, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| SuggestMe | GameSessions.RegisterForAppLifetimeNotifications | ButtonItem, ConfirmModal, DropdownItem, Focusable, Navigation, PanelSection, PanelSectionRow, ProgressBar, SidebarNavigation, SliderField, Spinner, TextField, ToggleField, showModal, staticClasses | appStore, collectionStore, routerHook, toaster, window.SP_REACT, local/Steam routes, toasts, QAM/global tabs, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Non-Steam Badges | none detected | DialogButton, DropdownItem, Navigation, PanelSection, PanelSectionRow, ToggleField, afterPatch, appDetailsClasses, createReactTreePatcher, findInReactTree, getGamepadNavigationTrees | collectionStore, routerHook, local/Steam routes, Steam page patches/module lookup, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Deck Progress Tracker | Apps.GetAllApps, Apps.GetMyAchievementsForApp | ButtonItem, DialogButton, Focusable, Navigation, PanelSection, PanelSectionRow, afterPatch, appDetailsClasses, appDetailsHeaderClasses, createReactTreePatcher, findInReactTree, staticClasses | appStore, collectionStore, routerHook, toaster, local/Steam routes, toasts, Steam page patches/module lookup, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Deck Shelves | Apps.RegisterForAppDetails, System.GetOSVersion, System.GetSystemInfo | showContextMenu | SteamUIStore, appDetailsStore, appStore, collectionStore, routerHook, toaster, local/Steam routes, toasts, QAM/global tabs, Steam page patches/module lookup, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Decky-Clipper | none detected | ButtonItem, ModalRoot, Navigation, PanelSection, Router, ToggleField, showModal, staticClasses | Router | Python + native payload; x86_64 | gamescope |
|
||||
| Decky Proton Launch | Apps.RegisterForAppDetails, Apps.SetAppLaunchOptions, User.StartRestart | ConfirmModal, DialogButton, DropdownItem, Focusable, GamepadButton, ModalRoot, PanelSection, PanelSectionRow, TextField, ToggleField, showModal, staticClasses | toaster, toasts, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| Docky | none detected | ButtonItem, ConfirmModal, DialogButton, DropdownItem, Field, Focusable, ModalRoot, PanelSection, PanelSectionRow, TextField, ToggleField, definePlugin, showModal | toaster, window.SP_REACT, toasts | Python | /sys/bus/usb/devices/; /sys/bus/usb/devices/*; /sys/bus/usb/drivers/usb/; /sys/bus/usb/drivers/usb/%s; /sys/class/drm/*/status; /sys/class/drm/card*-*/; /sys/class/hwmon/hwmon*; /sys/class/hwmon/hwmonN; /sys/class/power_supply/*/online; /sys/class/thermal; /sys/class/thermal/thermal_zone*/temp; amdgpu; flatpak; gamescope; hwmon; pactl; sudo; systemctl |
|
||||
| Output Manager | none detected | ButtonItem, Navigation, PanelSection, PanelSectionRow, TextField, ToggleField, staticClasses | none detected | Python | /sys/class/drm; /sys/class/drm/card{card_number}/device; /sys/kernel/debug/dri; pactl |
|
||||
| IPv6 Enabler Plugin | none detected | DropdownItem, Field, PanelSection, PanelSectionRow, staticClasses | toaster, toasts | Python | /sys/net/ipv6/conf |
|
||||
| DeckyClocker | none detected | ButtonItem, Field, PanelSection, PanelSectionRow, SliderField | toaster, toasts | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
| MuteOnSleep | none detected | PanelSection, PanelSectionRow, ToggleField | Steam stores | Python | pactl; sudo |
|
||||
| Desktop Apps | Apps.AddShortcut, Apps.RemoveShortcut, Apps.RunGame, Apps.SetShortcutExe, Apps.SetShortcutLaunchOptions, Apps.SetShortcutName, Apps.SetShortcutStartDir, GameSessions.RegisterForAppLifetimeNotifications | DialogButton, Focusable, Menu, MenuItem, PanelSection, PanelSectionRow, showContextMenu, staticClasses | appStore, toaster, toasts, Steam stores | Python | flatpak |
|
||||
| Mixer | none detected | ButtonItem, ConfirmModal, DialogButton, Focusable, PanelSection, PanelSectionRow, SliderField, TextField, showModal, staticClasses | toaster, toasts | Python | flatpak |
|
||||
| VARTA | Store.SetWishlist, StoreItems.SetWishlist, User, User.GetSteamID | ButtonItem, DropdownItem, Field, PanelSection, PanelSectionRow, Spinner, ToggleField, afterPatch, appDetailsClasses, beforePatch, createReactTreePatcher, findInReactTree, findModule, findModuleByExport, findModuleExport, staticClasses, useParams | routerHook, toaster, local/Steam routes, toasts, Steam page patches/module lookup, Steam stores | Python | No specific hardware requirement detected statically; runtime requirements unverified. |
|
||||
|
||||
## Native payload portability
|
||||
|
||||
All published Linux native payloads in this snapshot are x86_64; no published aarch64 payload was found. The three Windows DLLs belong to Decky-Framegen. The installer still rejects these archives. Apart from the explicitly described LocalSend experiment, ARM64 builds were not attempted: the constraints below remain unresolved.
|
||||
|
||||
| Plugin | Native payloads | ARM64 result / remaining blocker |
|
||||
| --- | --- | --- |
|
||||
| Fantastic | bin/backend | Fan backend/driver is Deck-specific; an ARM64 rebuild would not provide compatible Frame hardware. |
|
||||
| PowerTools | bin/libryzenadj.so, bin/ryzenadj, bin/backend | RyzenAdj and AMD power backend cannot control an ARM64 Frame SoC. |
|
||||
| Memory Deck | bin/libscanmem.so.1.0.0 | libscanmem must be rebuilt for aarch64 and validated against target process architecture; memory writes were not tested. |
|
||||
| Emuchievements | bin/hash | The bin/hash helper needs an ARM64 build and cross-platform hashing tests; no ARM64 artifact is published. |
|
||||
| MoonDeck | python/externals/yarl/_quoting_c.cpython-313-x86_64-linux-gnu.so, python/externals/aiohttp/_http_parser.cpython-313-x86_64-linux-gnu.so, python/externals/aiohttp/_websocket/reader_c.cpython-313-x86_64-linux-gnu.so, python/externals/aiohttp/_websocket/mask.cpython-313-x86_64-linux-gnu.so, python/externals/aiohttp/_http_writer.cpython-313-x86_64-linux-gnu.so, … 26 total; full list in JSON | Rebuild vendored CPython 3.13 extension wheels for the headset Python ABI and ARM64; buddy/helper dependencies also need ports. |
|
||||
| Controller Tools | bin/backend | Native backend needs an ARM64 build and controller/HID dependencies; its direct websocket also bypasses the Framey bridge. |
|
||||
| DeckMTP | bin/umtprd | umtprd needs ARM64 libusb/MTP dependencies and USB gadget permissions; root user-service blocker remains. |
|
||||
| Decky Cloud Save | bin/rclone | rclone needs an ARM64 artifact; this ZIP also fails safe member validation. Cloud writes were not exercised. |
|
||||
| DeckyFileServer | bin/backend | Native file-server backend needs ARM64 dependencies; root requirement also blocks the user service. |
|
||||
| TS3 QuickAccess | bin/ts3-qs4sd.so | TeamSpeak client plugin must match an ARM64 TeamSpeak client and SDK ABI, not merely rebuild the .so. |
|
||||
| Syncthing | bin/decky-syncthing-watchdog | The watchdog needs an ARM64 build, plus a supported Syncthing runtime/package and safe path validation. |
|
||||
| Wine Cellar | bin/backend | Native backend needs an ARM64 build; direct websocket and Wine runtime assumptions also need integration. |
|
||||
| MicroSDeck | bin/backend | Native backend needs an ARM64 build and block-device/mount permission audit; no storage actions were run. |
|
||||
| EmuDecky | bin/hello | Published bin/hello is x86_64 even if unused; upstream must remove it or publish an ARM64 archive, plus remove root dependency. |
|
||||
| MagicPods | bin/magicpodscore | magicpodscore requires an upstream ARM64 artifact/source and protocol tests; frontend uses a direct websocket. |
|
||||
| Decky-Undervolt | bin/libryzenadj.so, bin/ryzenadj | RyzenAdj is AMD-specific and cannot be ported to control the Frame SoC. |
|
||||
| Decky-SpoofDPI | bin/spoofdpi | SpoofDPI is a separate upstream helper; needs an ARM64 artifact/protocol and network-permission validation. |
|
||||
| Decky-Framegen | bin/nvngx.dll, bin/dlssg_to_fsr3_amd_is_better.dll, bin/nvapi64.dll | Windows DLL payload targets x86 game translation rather than a native ARM backend; archive is 75,140,408 bytes, beyond the 50 MiB installer limit. |
|
||||
| HueSync | bin/hello, py_modules/portio.so | portio and handheld LED backends depend on x86 I/O/unsupported hardware; ARM64 compilation alone is insufficient. |
|
||||
| Decky Clipboard | bin/xclip | xclip needs ARM64 X11 libraries/server; root and gamescope assumptions also block this archive. |
|
||||
| Reroll | bin/memscan.so | memscan.so needs ARM64 process-memory support and ABI testing; root requirement and missing Steam hook remain. |
|
||||
| Decky Localsend | bin/localsend-core | LocalSend-Go commit 66edc509a28b9f877e7374504892dcbc653a4da4 cross-compiled successfully with CGO_ENABLED=0 GOOS=linux GOARCH=arm64. Static aarch64 ELF SHA-256 e79238f6051ce34f172f765ae918d3d902b819e27377989a55e5b1eba10e46f9. Not executed; source tip is not proven to match the shipped helper/protocol. Official ZIP is still x86_64. |
|
||||
| Launch Options | bin/hello | Remove the template x86 bin/hello or publish an ARM64 rebuild; missing Steam module mapping also prevents frontend initialization. |
|
||||
| Decky-Clipper | bin/gstreamer-1.0/libgstproxy.so, bin/gstreamer-1.0/libgstmidi.so, bin/gstreamer-1.0/libgstmpegtsmux.so, bin/gstreamer-1.0/gst-plugin-scanner, bin/gstreamer-1.0/libgstivfparse.so, … 115 total; full list in JSON | Rebuild all bundled GStreamer libraries/plugins for ARM64 and verify capture/codec ABI against Frame SteamVR; gamescope capture differs. |
|
||||
|
||||
## Evidence and maintenance
|
||||
|
||||
The canonical JSON contains full archive hashes, native paths, source URLs, frontend observations and fake-Frame outcomes. Store compatibility is advisory except known installation/hardware blockers; removal always remains available. A panel retains its compatibility reason after the initializer reports ready. Upstream fixes require a new digest-scoped audit. Do not mark a plugin works based on archive installation, fake backend replies or initializer completion.
|
||||
@@ -6,6 +6,8 @@ user services under `~/.local/share/frame-control`; neither needs root or a
|
||||
read-write SteamOS root filesystem. Decky's standard Quick Access menu still
|
||||
does not appear in the separate SteamVR CEF target.
|
||||
|
||||
See the [complete official-store compatibility table](framey-compatibility.md) for version-specific statuses, blockers and verification limits. Both stores and installed panels show the same digest-scoped results; a new published archive starts unverified.
|
||||
|
||||
## Use in the headset
|
||||
|
||||
Install **Frame Control in VR** and **Decky Loader on Frame** from Frame Control
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Serve exact official frontends to a local Chromium audit, with all actions denied.
|
||||
|
||||
Open the printed loopback URL with T3's collaborative browser. Run `runAudit()`
|
||||
in the page, then inspect `results` or download its JSON. This is initialization
|
||||
evidence only. No official Python/native backend is imported or executed.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import io
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT/'ui'))
|
||||
import framey_panel
|
||||
|
||||
PRELUDE = '''<script>
|
||||
const report=(kind,message)=>parent.postMessage({type:'audit:log',kind,message:String(message)},'*');
|
||||
addEventListener('error',event=>report('error',event.message));
|
||||
addEventListener('unhandledrejection',event=>report('rejection',event.reason?.stack||event.reason));
|
||||
for(const key of ['error','warn']){const original=console[key];console[key]=(...args)=>{
|
||||
report(key,args.map(String).join(' '));original(...args);};}
|
||||
window.fetch=()=>Promise.reject(Error('Audit blocks plugin network'));
|
||||
window.WebSocket=class{constructor(){throw Error('Audit blocks WebSocket');}};
|
||||
</script>'''
|
||||
|
||||
PAGE = '''<!doctype html><meta charset=utf-8><title>Framey catalog frontend audit</title>
|
||||
<h1>Framey catalog frontend audit</h1>
|
||||
<p>Exact archived frontend code runs in an opaque iframe. All backend calls and network requests are denied.
|
||||
Ready means initializer completion, not useful plugin behavior. No plugin backend executes.</p>
|
||||
<button id=run>Run catalog</button> <button id=download>Download observations</button><p id=progress></p>
|
||||
<iframe id=panel sandbox="allow-scripts" style="width:1100px;height:740px"></iframe>
|
||||
<script>
|
||||
const plugins=CATALOG_DATA;
|
||||
window.results=[];let active=null,running=false;
|
||||
addEventListener('message',event=>{
|
||||
if(event.source!==panel.contentWindow||!active)return;
|
||||
const data=event.data;
|
||||
if(data.type==='audit:log'&&active.logs.length<8)active.logs.push({kind:data.kind,message:data.message.split('\\n')[0].slice(0,400)});
|
||||
if(data.type==='framey:error')active.error=String(data.message).slice(0,500);
|
||||
if(data.type==='framey:ready')active.ready=true;
|
||||
if(data.type==='framey:call'){
|
||||
if(!active.methods.includes(data.method))active.methods.push(data.method);
|
||||
event.source.postMessage({type:'framey:reply',id:data.id,error:'Read-only compatibility audit: backend method blocked'},'*');}
|
||||
if(data.type==='framey:cache:init')event.source.postMessage({type:'framey:cache:seed',session:data.session,entries:{}},'*');
|
||||
});
|
||||
window.runAudit=async(ids)=>{
|
||||
if(running)throw Error('Audit already running');running=true;run.disabled=true;
|
||||
try{for(const plugin of plugins.filter(plugin=>!ids||ids.includes(plugin.id))){
|
||||
active={id:plugin.id,name:plugin.name,ready:false,error:null,logs:[],methods:[]};
|
||||
progress.textContent=plugin.id+' '+plugin.name;panel.src='/panel/'+plugin.id;
|
||||
await new Promise(resolve=>setTimeout(resolve,1200));results.push(active);active=null;
|
||||
panel.src='about:blank';await new Promise(resolve=>setTimeout(resolve,30));
|
||||
}}finally{running=false;run.disabled=false;progress.textContent='Done: '+results.length;}
|
||||
return results;
|
||||
};
|
||||
run.onclick=()=>runAudit();
|
||||
download.onclick=()=>{const url=URL.createObjectURL(new Blob([JSON.stringify(results,null,2)],{type:'application/json'}));
|
||||
const anchor=document.createElement('a');anchor.href=url;anchor.download='framey-browser-audit.json';anchor.click();setTimeout(()=>URL.revokeObjectURL(url),1000);};
|
||||
</script>'''
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--cache', type=Path, required=True)
|
||||
parser.add_argument('--catalog', type=Path, default=ROOT/'ui/framey-compatibility.json')
|
||||
parser.add_argument('--port', type=int, default=18765)
|
||||
args = parser.parse_args()
|
||||
plugins = json.loads(args.catalog.read_text())['plugins']
|
||||
records = {p['id']: p for p in plugins}
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *_):
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
path = self.path.split('?')[0]
|
||||
content_type = 'text/html; charset=utf-8'
|
||||
if path == '/':
|
||||
# Only names/IDs are needed by the UI; never embed raw executable source.
|
||||
catalog = json.dumps([{'id': p['id'], 'name': p['name']} for p in plugins]).replace('<', '\\u003c')
|
||||
body = PAGE.replace('CATALOG_DATA', catalog).encode()
|
||||
elif path.startswith('/panel/'):
|
||||
try:
|
||||
record = records[int(path.split('/')[-1])]
|
||||
data = (args.cache/(record['sha256']+'.zip')).read_bytes()
|
||||
if hashlib.sha256(data).hexdigest() != record['sha256']:
|
||||
raise ValueError('Archive hash mismatch')
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
||||
bundle = archive.read(record['folder']+'/dist/index.js')
|
||||
with patch.object(framey_panel, 'local_bundle', return_value=bundle):
|
||||
body = framey_panel.panel(record['id']).replace(
|
||||
b'<script id="framey-bundle"', PRELUDE.encode()+b'<script id="framey-bundle"')
|
||||
except (KeyError, ValueError, OSError, zipfile.BadZipFile):
|
||||
self.send_error(404, 'Verified frontend archive unavailable')
|
||||
return
|
||||
else:
|
||||
files = {'/framey-runtime/react.js': 'ui/vendor/react-18.3.1.min.js',
|
||||
'/framey-runtime/react-dom.js': 'ui/vendor/react-dom-18.3.1.min.js',
|
||||
'/framey-runtime/host.js': 'ui/framey-host.js'}
|
||||
if path not in files:
|
||||
self.send_error(404)
|
||||
return
|
||||
body = (ROOT/files[path]).read_bytes()
|
||||
content_type = 'text/javascript'
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', content_type)
|
||||
self.send_header('Content-Security-Policy', "default-src 'none'; script-src 'self' 'unsafe-inline' blob:; style-src 'unsafe-inline'; img-src data:; frame-src 'self'; connect-src 'none'")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
print(f'Frontend audit: http://127.0.0.1:{args.port}', flush=True)
|
||||
ThreadingHTTPServer(('127.0.0.1', args.port), Handler).serve_forever()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit a verified catalog cache in a private disposable fake-Frame project."""
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--cache', type=Path, required=True)
|
||||
parser.add_argument('--output', type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
cache = args.cache.resolve()
|
||||
output = args.output.resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
records = json.loads((root/'ui/framey-compatibility.json').read_text())['plugins']
|
||||
if not all((cache/(p['sha256']+'.zip')).is_file() for p in records):
|
||||
parser.error('cache must contain all exact catalog archives; run framey-audit-store.py first')
|
||||
# Unique project avoids tearing down another job's fake Frame or its volumes.
|
||||
project = 'framey-catalog-'+uuid.uuid4().hex[:10]
|
||||
subprocess.run(['docker', 'build', '-q', '-t', 'fakeframe-frame', '-f',
|
||||
str(root/'tests/fakeframe/Containerfile'), str(root/'tests/fakeframe')], check=True)
|
||||
subprocess.run(['docker', 'build', '-q', '-t', 'fakeframe-host', '-f',
|
||||
str(root/'tests/fakeframe/host.Containerfile'), str(root/'tests/fakeframe')], check=True)
|
||||
with tempfile.TemporaryDirectory(prefix='framey-catalog-') as temp:
|
||||
override = Path(temp)/'compose.json'
|
||||
override.write_text(json.dumps({'services': {'host': {
|
||||
'volumes': [f'{cache}:/audit-cache:ro', f'{output.parent}:/audit-output'],
|
||||
'environment': {'FRAMEY_AUDIT_CACHE': '/audit-cache',
|
||||
'FRAMEY_AUDIT_OUTPUT': '/audit-output/'+output.name}}}}))
|
||||
compose = ['docker', 'compose', '-p', project, '-f', str(root/'tests/fakeframe/compose.yaml'), '-f', str(override)]
|
||||
try:
|
||||
subprocess.run(compose+['up', '-d', '--wait'], check=True)
|
||||
subprocess.run(compose+['exec', '-T', '-w', '/repo/tests/e2e', 'host', 'python3', '-m',
|
||||
'unittest', '-v', 'test_framey_catalog.CatalogAudit'], check=True)
|
||||
finally:
|
||||
subprocess.run(compose+['down', '-v', '--remove-orphans'], check=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Inspect official Decky archives without importing or executing plugin code.
|
||||
|
||||
Archives stay in a caller-selected cache. SHA-256 is checked before inspection.
|
||||
The resulting observations are static evidence, not compatibility guarantees.
|
||||
"""
|
||||
import argparse
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
STORE = 'https://plugins.deckbrew.xyz/plugins'
|
||||
CDN = 'https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/'
|
||||
INIT = '__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit'
|
||||
|
||||
|
||||
def fetch(url, limit):
|
||||
request = urllib.request.Request(url, headers={'User-Agent': 'Frame-Control-compatibility-audit'})
|
||||
with urllib.request.urlopen(request, timeout=60) as response:
|
||||
if int(response.headers.get('Content-Length', '0')) > limit:
|
||||
raise ValueError('download exceeds audit size limit')
|
||||
data = response.read(limit + 1)
|
||||
if len(data) > limit:
|
||||
raise ValueError('download exceeds audit size limit')
|
||||
return data
|
||||
|
||||
|
||||
def inspect(item, cache):
|
||||
version = item['versions'][0]
|
||||
digest = version['hash']
|
||||
result = {'id': item['id'], 'name': item['name'], 'version': version['name'],
|
||||
'sha256': digest, 'tags': item.get('tags', [])}
|
||||
try:
|
||||
path = cache / (digest + '.zip')
|
||||
data = path.read_bytes() if path.exists() else fetch(CDN + digest + '.zip', 200 * 1024 * 1024)
|
||||
if hashlib.sha256(data).hexdigest() != digest:
|
||||
raise ValueError('official archive SHA-256 mismatch')
|
||||
path.write_bytes(data)
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
||||
entries = archive.infolist()
|
||||
if len(entries) > 6000 or sum(e.file_size for e in entries) > 500 * 1024 * 1024:
|
||||
raise ValueError('archive exceeds audit size limit')
|
||||
manifests = [e.filename for e in entries if e.filename.count('/') == 1 and e.filename.endswith('/plugin.json')]
|
||||
if len(manifests) != 1:
|
||||
raise ValueError('expected one top-level plugin.json')
|
||||
manifest = json.loads(archive.read(manifests[0]))
|
||||
folder = manifests[0].split('/')[0]
|
||||
frontend_path = folder + '/dist/index.js'
|
||||
frontend = archive.read(frontend_path).decode('utf-8', errors='replace') if frontend_path in archive.namelist() else ''
|
||||
result.update(folder=folder, archiveBytes=len(data), api=manifest.get('api_version', 'legacy'), flags=manifest.get('flags', []),
|
||||
modernBridge=INIT in frontend, frontend=bool(frontend),
|
||||
frontendApi=('legacy' if re.match(r'\s*\(function\s*\(', frontend) else
|
||||
next(iter(re.findall(r'API_VERSION\s*=\s*(\d+)', frontend)), 'unknown')))
|
||||
python_files, native, sources = [], [], []
|
||||
for entry in entries:
|
||||
if entry.is_dir():
|
||||
continue
|
||||
with archive.open(entry) as stream:
|
||||
header = stream.read(20)
|
||||
if header[:4] == b'\x7fELF':
|
||||
machine = int.from_bytes(header[18:20], 'little' if header[5] == 1 else 'big')
|
||||
native.append({'file': entry.filename, 'elfMachine': machine,
|
||||
'arch': {62: 'x86_64', 183: 'aarch64', 3: 'x86', 40: 'arm'}.get(machine, str(machine))})
|
||||
elif header[:2] == b'MZ':
|
||||
native.append({'file': entry.filename, 'arch': 'Windows'})
|
||||
if entry.filename.endswith('.py') and entry.file_size < 2 * 1024 * 1024:
|
||||
python_files.append(entry.filename)
|
||||
sources.append(archive.read(entry).decode('utf-8', errors='replace'))
|
||||
backend = '\n'.join(sources)
|
||||
metadata = json.dumps(manifest)
|
||||
for suffix in ('/package.json', '/README.md'):
|
||||
name = folder + suffix
|
||||
if name in archive.namelist() and archive.getinfo(name).file_size < 1024 * 1024:
|
||||
metadata += '\n' + archive.read(name).decode('utf-8', errors='replace')
|
||||
repositories = set(re.findall(r'https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+', metadata + '\n' + backend))
|
||||
repositories.update('https://github.com/' + match for match in re.findall(
|
||||
r'https://raw\.githubusercontent\.com/([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)', metadata))
|
||||
|
||||
result.update(pythonFiles=python_files, native=native,
|
||||
steamClient=sorted(set(re.findall(r'SteamClient\.([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)', frontend))),
|
||||
dfl=sorted(set(re.findall(r'(?:DFL|__DFL__|deckyFrontendLib)\.([A-Za-z_$][\w$]*)', frontend))),
|
||||
globals=sorted(set(re.findall(r'\b(?:Router|routerHook|toaster|SteamUIStore|appStore|appDetailsStore|collectionStore|achievementStore|window\.SP_[A-Z_]+|DeckyPluginLoader|webpackChunksteamui)\b', frontend))),
|
||||
uiSurfaces=[label for label, pattern in (
|
||||
('local/Steam routes', r'routerHook|addRoute|removeRoute'),
|
||||
('toasts', r'\btoaster\b|\.toast\('),
|
||||
('QAM/global tabs', r'quickAccessMenu|addGlobalComponent|addTab|QAM'),
|
||||
('Steam page patches/module lookup', r'afterPatch|beforePatch|findModule|createModuleMapping'),
|
||||
('Steam stores', r'\b\w+Store\b'),
|
||||
) if re.search(pattern, frontend)],
|
||||
hardwareSignals=sorted(set(re.findall(r'(?:/sys/[^\s\x22\x27)]+|/dev/(?:hidraw|input|i2c|drm)[\w/*-]*|\b(?:ryzenadj|intel_pstate|amdgpu|hwmon|gamescope|pactl|bluetoothctl|flatpak|systemctl|sudo|x86_64|amd64)\b)', backend))),
|
||||
sourceURLs=sorted(repositories)[:20])
|
||||
except Exception as failure:
|
||||
result['auditError'] = str(failure)
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--catalog', type=Path, help='saved official catalog (otherwise fetch current)')
|
||||
parser.add_argument('--cache', type=Path, required=True)
|
||||
parser.add_argument('--output', type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
records = json.loads(args.catalog.read_bytes() if args.catalog else fetch(STORE, 4 * 1024 * 1024))
|
||||
records = [r for r in records if r.get('visible') is not False and r.get('versions')]
|
||||
args.cache.mkdir(parents=True, exist_ok=True)
|
||||
with ThreadPoolExecutor(max_workers=6) as workers:
|
||||
plugins = list(workers.map(lambda item: inspect(item, args.cache), records))
|
||||
args.output.write_text(json.dumps({'checkedAtUtc': datetime.now(timezone.utc).isoformat(),
|
||||
'source': STORE, 'method': 'SHA-256-verified official archives; static inspection only; plugin code was not executed.',
|
||||
'plugins': plugins}, indent=2) + '\n')
|
||||
print(f"Audited {len(plugins)} official plugins; {sum('auditError' in p for p in plugins)} errors. {args.output}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the documentation from the same digest-scoped data used by the store."""
|
||||
import argparse
|
||||
from collections import Counter
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def cell(value):
|
||||
if isinstance(value, list):
|
||||
value = ', '.join(str(v) for v in value) or 'none detected'
|
||||
return str(value).replace('|', '|').replace('\n', ' ').replace('<', '<')
|
||||
|
||||
|
||||
def render(data):
|
||||
counts = Counter(p['status'] for p in data['plugins'])
|
||||
lines = ['# Official Decky store compatibility with Framey', '',
|
||||
f"Snapshot: {data['checkedAtUtc']}; {len(data['plugins'])} current visible official plugins from {data['source']}.", '',
|
||||
'This table and both store views use [`ui/framey-compatibility.json`](../ui/framey-compatibility.json). '
|
||||
'Results apply only to the exact store ID, version and SHA-256. A changed digest/version is **unverified**. '
|
||||
'Regenerate this document with `python3 scripts/framey-compatibility-doc.py`; use `--check` in review.', '',
|
||||
f"**Works: {counts['works']} · Partial: {counts['partial']} · Broken: {counts['broken']} · N/A: {counts['na']}.**", '',
|
||||
'- **Works** requires verified useful plugin behavior with its real backend. None qualifies from this audit.',
|
||||
'- **Partial** means a limited frontend/settings surface initialized or rendered. Backend-dependent functionality remains unverified; it is not an end-to-end success claim.',
|
||||
'- **Broken** means a concrete archive, frontend or Steam dependency blocks the audited version. An artificial backend/network denial alone is not evidence that the real backend is broken.',
|
||||
'- **N/A** means the plugin targets hardware or a display/UI surface that Framey does not provide. The store disables installation for these versions.', '',
|
||||
'## Verification limits', '',
|
||||
f"All exact archives were hash-checked and statically inspected. The local Chromium host used Framey commit `{data['hostCommit']}`, React 18.3.1 and an opaque `allow-scripts` iframe. "
|
||||
'Each frontend was attempted for 1.2 seconds, including React effects. All plugin backend calls, fetches and websockets were denied. '
|
||||
'Some plugins attempt mutating actions on mount; none reached a backend. Initialization (`ready`) does not guarantee React rendered, and delayed errors/actions may occur after the observation window. '
|
||||
'The JSON preserves initializer errors, capped console observations and attempted method names.', '',
|
||||
'The optional full-catalog fake-Frame audit exercises the real HTTP/SSH installer, receipt, frontend bytes and removal, '
|
||||
'or expected rejection. It uses a private disposable Compose project with all plugin backend execution stubbed. '
|
||||
'It checks archive transport and safeguards, not Python/native execution. Run it with:', '',
|
||||
'```sh',
|
||||
'python3 scripts/framey-audit-store.py --cache /tmp/framey-archives --output /tmp/framey-observations.json',
|
||||
'python3 scripts/framey-audit-fake.py --cache /tmp/framey-archives --output /tmp/framey-fake/results.json',
|
||||
'python3 scripts/framey-audit-browser.py --cache /tmp/framey-archives',
|
||||
'```', '',
|
||||
'Open the browser audit URL in local Chromium, run `runAudit()` and inspect/download `results`. All backend/network actions remain denied.', '',
|
||||
'The current published digests are pinned in the JSON; if the store changed, retain/download those exact archives for reproduction. '
|
||||
'Audit downloads permit larger ZIPs for inspection; production installer limits remain 50 MiB ZIP / 150 MiB unpacked / 30 MiB per member.', '',
|
||||
'After the VR job released the shared lock, this audit acquired it and used a private temporary Decky 3.3.0 ARM64 user service. '
|
||||
'Read-only ProtonDB `get_setting` and Steam Status `get_settings` calls succeeded through the live Steam CEF websocket. '
|
||||
'The service and private base were removed and the lock released; normal managed services stayed inactive, battery was 100 → 99, '
|
||||
'and no power setting was changed. [Redacted results](evidence/framey-official-backend-2026-10-25.json) '
|
||||
'record exactly what ran. All other official backends remain unexecuted. '
|
||||
'The separate VR job proves Frame Control/sample compositor rendering, not any official plugin. '
|
||||
'No worn-VR official-plugin result is claimed. '
|
||||
'Earlier focused checks cover [ProtonDB](evidence/framey-browser-2026-10-25.md), '
|
||||
'[Steam Status/cache](evidence/framey-cache-browser-2026-10-25.md), '
|
||||
'[Storage Cleaner](evidence/framey-api-v1-browser-2026-10-25.md), and '
|
||||
'[legacy MusicControl](evidence/framey-legacy-2026-10-25.md).', '',
|
||||
'## Every current plugin', '',
|
||||
'The API column shows manifest / actual bundled frontend API. Many manifests say v1 while their bundle connects API2. '
|
||||
'Missing manifest API is labelled legacy; an unrecognized bundle is unknown. Globals and hardware are static signals, not exhaustive dependencies.', '',
|
||||
'| Plugin / version | Manifest / bundle API | Status | Reason | Verified by |',
|
||||
'| --- | --- | --- | --- | --- |']
|
||||
for p in data['plugins']:
|
||||
lines.append('| '+ ' | '.join(map(cell, [f"{p['id']}: {p['name']} {p['version']}",
|
||||
f"{p['api']} / {p['frontendApi']}", 'N/A' if p['status']=='na' else p['status'], p['reason'], p['verification']]))+' |')
|
||||
lines += ['', '## API, UI and backend inventory', '',
|
||||
'DFL includes exports imported through decky-frontend-lib / @decky/ui. Steam stores and router/toaster/QAM signals are listed separately. '
|
||||
'No signal means the static scan did not detect one, not that no dependency exists. Native binaries are classified by ELF machine or Windows MZ header; '
|
||||
'Python files and source links are preserved in the JSON.', '',
|
||||
'| Plugin | SteamClient methods | DFL / @decky/ui exports | Other Steam globals / UI surfaces | Backend | Hardware / OS signals |',
|
||||
'| --- | --- | --- | --- | --- | --- |']
|
||||
for p in data['plugins']:
|
||||
native = sorted({n['arch'] for n in p['native']})
|
||||
lines.append('| '+' | '.join(map(cell, [p['name'],p['steamClient'],p['dfl'],p['globals']+p.get('uiSurfaces',[]),
|
||||
p['backend']+('; '+', '.join(native) if native else ''),p['hardware']]))+' |')
|
||||
lines += ['', '## Native payload portability', '',
|
||||
'All published Linux native payloads in this snapshot are x86_64; no published aarch64 payload was found. '
|
||||
'The three Windows DLLs belong to Decky-Framegen. The installer still rejects these archives. '
|
||||
'Apart from the explicitly described LocalSend experiment, ARM64 builds were not attempted: the constraints below remain unresolved.', '',
|
||||
'| Plugin | Native payloads | ARM64 result / remaining blocker |', '| --- | --- | --- |']
|
||||
for p in data['plugins']:
|
||||
if p['native']:
|
||||
binaries = [n['file'].removeprefix(p['folder']+'/') for n in p['native']]
|
||||
if len(binaries)>5:
|
||||
binaries = binaries[:5]+[f"… {len(binaries)} total; full list in JSON"]
|
||||
lines.append('| '+' | '.join(map(cell,[p['name'],binaries,p['nativePort']]))+' |')
|
||||
lines += ['', '## Evidence and maintenance', '',
|
||||
'The canonical JSON contains full archive hashes, native paths, source URLs, frontend observations and fake-Frame outcomes. '
|
||||
'Store compatibility is advisory except known installation/hardware blockers; removal always remains available. '
|
||||
'A panel retains its compatibility reason after the initializer reports ready. Upstream fixes require a new digest-scoped audit. '
|
||||
'Do not mark a plugin works based on archive installation, fake backend replies or initializer completion.', '']
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--check', action='store_true')
|
||||
args = parser.parse_args()
|
||||
output = ROOT/'docs/framey-compatibility.md'
|
||||
expected = render(json.loads((ROOT/'ui/framey-compatibility.json').read_text(encoding='utf-8')))
|
||||
if args.check:
|
||||
if not output.exists() or output.read_text(encoding='utf-8') != expected:
|
||||
raise SystemExit('Compatibility documentation differs from canonical store data')
|
||||
else:
|
||||
output.write_text(expected, encoding='utf-8')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Optional full-catalog archive/transport audit; no official backend executes."""
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import threading
|
||||
import unittest
|
||||
import zipfile
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
from harness import FrameTestCase, ROOT, Server, finished, ok, require, ssh
|
||||
import frame_decky_store
|
||||
|
||||
require()
|
||||
|
||||
|
||||
@unittest.skipUnless(os.environ.get('FRAMEY_AUDIT_CACHE'), 'optional full catalog audit cache not mounted')
|
||||
class CatalogAudit(FrameTestCase):
|
||||
def backend(self):
|
||||
# Only the fake service/CEF stub executes. Official Python/native code
|
||||
# copied by the installer is never imported or launched by this harness.
|
||||
source = '''import json,pathlib
|
||||
h=pathlib.Path.home();b=h/'.local/share/frame-control/decky/.frame-control';b.mkdir(parents=True,exist_ok=True)
|
||||
(b/'receipt.json').write_text(json.dumps({'managedBy':'Frame Control','version':'v3.3.0'}))
|
||||
(b.parent/'plugins').mkdir(exist_ok=True)
|
||||
u=h/'.config/systemd/user/frame-control-decky.service';u.parent.mkdir(parents=True,exist_ok=True);u.write_text('[Service]')'''
|
||||
ssh('python3 -c '+shlex.quote(source))
|
||||
ssh('mkdir -p ~/.local/share/frame-control/headset/bundle/ui')
|
||||
result = subprocess.run(['ssh', 'frame', 'cat > ~/.local/share/frame-control/headset/bundle/ui/frame_steam.py'],
|
||||
input=(ROOT/'ui/frame_steam.py').read_bytes(), capture_output=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
records = json.loads((ROOT/'ui/framey-compatibility.json').read_text())['plugins']
|
||||
cache = Path(os.environ['FRAMEY_AUDIT_CACHE'])
|
||||
|
||||
class Fixture(BaseHTTPRequestHandler):
|
||||
def log_message(self, *_):
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == '/plugins':
|
||||
body = json.dumps([{'id': p['id'], 'name': p['name'], 'tags': p['tags'],
|
||||
'versions': [{'name': p['version'], 'hash': p['sha256']}]}
|
||||
for p in records]).encode()
|
||||
else:
|
||||
path = cache / self.path.rsplit('/', 1)[-1]
|
||||
if path.suffix != '.zip' or path.name not in {p['sha256']+'.zip' for p in records}:
|
||||
self.send_error(404)
|
||||
return
|
||||
body = path.read_bytes()
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
try:
|
||||
self.wfile.write(body)
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass # Installer stops reading at its archive size limit.
|
||||
|
||||
fixture = ThreadingHTTPServer(('127.0.0.1', 0), Fixture)
|
||||
cls.addClassCleanup(fixture.server_close)
|
||||
cls.addClassCleanup(fixture.shutdown)
|
||||
threading.Thread(target=fixture.serve_forever, daemon=True).start()
|
||||
old = {k: os.environ.get(k) for k in ('FRAMEY_STORE_URL', 'FRAMEY_CDN_URL')}
|
||||
def restore():
|
||||
Server.stop()
|
||||
for key, value in old.items():
|
||||
if value is None:
|
||||
os.environ.pop(key, None)
|
||||
else:
|
||||
os.environ[key] = value
|
||||
cls.addClassCleanup(restore)
|
||||
os.environ['FRAMEY_STORE_URL'] = f'http://127.0.0.1:{fixture.server_port}/plugins'
|
||||
os.environ['FRAMEY_CDN_URL'] = f'http://127.0.0.1:{fixture.server_port}/versions/'
|
||||
Server.stop()
|
||||
super().setUpClass()
|
||||
|
||||
def test_all_current_official_archives(self):
|
||||
self.backend()
|
||||
records = json.loads((ROOT/'ui/framey-compatibility.json').read_text())['plugins']
|
||||
cache = Path(os.environ['FRAMEY_AUDIT_CACHE'])
|
||||
catalog = {p['id']: p for p in ok('GET', '/api/decky/store')['plugins']}
|
||||
self.assertEqual(set(catalog), {p['id'] for p in records})
|
||||
results = []
|
||||
for plugin in records:
|
||||
with self.subTest(plugin=plugin['name']):
|
||||
data = (cache/(plugin['sha256']+'.zip')).read_bytes()
|
||||
self.assertEqual(hashlib.sha256(data).hexdigest(), plugin['sha256'])
|
||||
validation = None
|
||||
try:
|
||||
folder = frame_decky_store.archive_info(data, plugin['sha256'], plugin['name'])
|
||||
except ValueError as error:
|
||||
validation = str(error)
|
||||
blocked = catalog[plugin['id']]['root'] or plugin['blockInstall'] or validation
|
||||
job = finished(ok('POST', '/api/decky/plugin', {'action': 'install', 'id': plugin['id'],
|
||||
'sha256': plugin['sha256'], 'acknowledged': True}))
|
||||
record = {'id': plugin['id'], 'sha256': plugin['sha256'],
|
||||
'archiveValidation': validation or 'accepted', 'result': 'blocked' if blocked else 'installed-removed'}
|
||||
if blocked:
|
||||
self.assertTrue(job.get('error'), job)
|
||||
record['reason'] = job['error']
|
||||
self.assertFalse(any(p['storeId'] == plugin['id'] for p in ok('GET', '/api/decky')['plugins']))
|
||||
else:
|
||||
self.assertFalse(job.get('error'), job)
|
||||
installed = f'~/.local/share/frame-control/decky/plugins/{folder}'
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
||||
entry = folder+'/dist/index.js'
|
||||
if entry in archive.namelist():
|
||||
expected = hashlib.sha256(archive.read(entry)).hexdigest()
|
||||
self.assertEqual(ssh(f'sha256sum {installed}/dist/index.js').split()[0], expected)
|
||||
found = next(p for p in ok('GET', '/api/decky')['plugins'] if p['storeId'] == plugin['id'])
|
||||
self.assertEqual((found['version'], found['sha256']), (plugin['version'], plugin['sha256']))
|
||||
removed = finished(ok('POST', '/api/decky/plugin', {'action': 'remove', 'id': plugin['id'], 'acknowledged': True}))
|
||||
self.assertFalse(removed.get('error'), removed)
|
||||
self.assertFalse(any(p['storeId'] == plugin['id'] for p in ok('GET', '/api/decky')['plugins']))
|
||||
results.append(record)
|
||||
Path(os.environ.get('FRAMEY_AUDIT_OUTPUT', '/tmp/framey-catalog-result.json')).write_text(json.dumps(results, indent=2)+'\n')
|
||||
@@ -67,7 +67,7 @@ ctx.mockApi=async path => path==='/api/decky' ?
|
||||
Promise.reject(Error(`unexpected ${path}`));
|
||||
vm.runInContext('api=mockApi',ctx);
|
||||
await vm.runInContext('refreshDecky()',ctx);
|
||||
assert.equal(el('deckyInstalled').children[0].children[2].disabled,false);
|
||||
assert.equal(el('deckyInstalled').children[0].children.find(child=>child.textContent==='Remove').disabled,false);
|
||||
el('deckySearch').value='';el('deckyStoreFilter').value='all';
|
||||
vm.runInContext('showDeckyStore()',ctx);
|
||||
assert.equal(el('deckyStoreList').children[1].children[1].disabled,true);
|
||||
@@ -117,3 +117,17 @@ await vm.runInContext('checkDeckyJob()',ctx);
|
||||
assert.match(el('deckyJobStatus').textContent,/app restarted and lost this job result/);
|
||||
assert.equal(el('deckyJobCheck').hidden,true);
|
||||
assert.equal(session.has('frameyPluginJob'),false);
|
||||
|
||||
// Known hardware blockers remain visible, cannot install, and keep their reason in the opened panel.
|
||||
ctx.hardware={id:900,name:'Hardware only',version:'1.0',description:'Fan control',root:false,
|
||||
compatibility:{status:'na',reason:'Not supported on Frame: Deck fan hardware.',verification:['static archive','local browser'],blockInstall:true}};
|
||||
el('deckySearch').value='';el('deckyStoreFilter').value='all';
|
||||
vm.runInContext('deckyStore=[hardware];deckyState={running:true,plugins:[{...hardware,storeId:900}]};showDeckyStore()',ctx);
|
||||
const hardwareRow=el('deckyStoreList').children[0];
|
||||
assert.equal(hardwareRow.children[1].disabled,true);
|
||||
assert.match(hardwareRow.children[0].children[0].textContent,/N\/A: Not supported on Frame/);
|
||||
el('deckyStoreFilter').value='compatible';vm.runInContext('showDeckyStore()',ctx);
|
||||
assert.equal(el('deckyStoreList').children.length,0);
|
||||
vm.runInContext('openFrameyPanel(900,"Hardware only")',ctx);
|
||||
assert.match(el('frameyCompatibility').textContent,/Deck fan hardware/);
|
||||
assert.match(el('frameyCompatibility').textContent,/local browser/);
|
||||
@@ -181,6 +181,27 @@ class StoreTransaction(unittest.TestCase):
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertFalse(self.folder.exists())
|
||||
|
||||
def test_remote_apply_rechecks_plugin_name_under_transaction_lock(self):
|
||||
data, _ = archive()
|
||||
other = self.folder.parent / 'Other'
|
||||
other.mkdir()
|
||||
(other / 'plugin.json').write_text('{"name":"Example"}')
|
||||
result = self.apply('install', data)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn('different plugin with this name', result.stderr)
|
||||
self.assertFalse(self.folder.exists())
|
||||
self.assertEqual((other / 'plugin.json').read_text(), '{"name":"Example"}')
|
||||
self.assertEqual(list(self.folder.parent.glob('.frame-control-stage-*')), [])
|
||||
|
||||
(other / 'plugin.json').write_text('{"name":"Distinct"}')
|
||||
self.assertEqual(self.apply('install', data).returncode, 0)
|
||||
(other / 'plugin.json').write_text('{"name":"Example"}')
|
||||
result = self.apply('update', data)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn('different plugin with this name', result.stderr)
|
||||
self.assertTrue(self.folder.exists())
|
||||
self.assertEqual(list(self.folder.parent.glob('.frame-control-stage-*')), [])
|
||||
|
||||
|
||||
class StoreHost(unittest.TestCase):
|
||||
def test_reviewed_archive_changed_before_install_does_not_download(self):
|
||||
@@ -219,6 +240,36 @@ class StoreHost(unittest.TestCase):
|
||||
'install', 42)
|
||||
network.assert_not_called()
|
||||
|
||||
def test_update_rejects_name_taken_by_another_plugin_before_download(self):
|
||||
state = {'managed': True, 'running': True, 'plugins': [
|
||||
{'storeId': 42, 'folder': 'Example', 'name': 'Old Name', 'version': '1.0'},
|
||||
{'storeId': 43, 'folder': 'Other', 'name': 'Example', 'version': '1.0'}]}
|
||||
catalog = [{'id': 42, 'name': 'Example', 'root': False,
|
||||
'sha256': 'a' * 64, 'version': '2.0'}]
|
||||
with patch.object(frame_decky_store.frame_decky, 'status', return_value=state), \
|
||||
patch.object(frame_decky_store, 'catalog', return_value=catalog), \
|
||||
patch.object(frame_decky_store.urllib.request, 'urlopen',
|
||||
side_effect=AssertionError('No download expected')):
|
||||
with self.assertRaisesRegex(ValueError, 'different plugin with this name'):
|
||||
frame_decky_store.change(lambda *args, **kwargs: self.fail('No headset write expected'),
|
||||
'update', 42)
|
||||
|
||||
def test_update_allows_its_own_existing_name(self):
|
||||
data, digest = archive()
|
||||
state = {'managed': True, 'running': True, 'plugins': [
|
||||
{'storeId': 42, 'folder': 'Example', 'name': 'Example', 'version': '1.0'}]}
|
||||
catalog = [{'id': 42, 'name': 'Example', 'root': False,
|
||||
'sha256': digest, 'version': '2.0'}]
|
||||
calls = []
|
||||
with patch.object(frame_decky_store.frame_decky, 'status', return_value=state), \
|
||||
patch.object(frame_decky_store, 'catalog', return_value=catalog), \
|
||||
patch.object(frame_decky_store.urllib.request, 'urlopen', return_value=io.BytesIO(data)):
|
||||
result = frame_decky_store.change(lambda command, **kwargs: calls.append(command) or '{}',
|
||||
'update', 42)
|
||||
self.assertEqual(result, {})
|
||||
self.assertEqual(len(calls), 3) # Upload, apply, and temporary archive cleanup.
|
||||
self.assertIn('update', calls[1])
|
||||
|
||||
def test_owned_plugin_can_be_removed_without_store_network(self):
|
||||
state = {'managed': True, 'running': True, 'plugins':
|
||||
[{'storeId': 42, 'folder': 'Example', 'name': 'Example', 'version': '1.0'}]}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""A compatibility claim must never transfer to a different published archive."""
|
||||
import sandbox # noqa: F401
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import subprocess
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]/'ui'))
|
||||
import framey_compatibility as compatibility
|
||||
import frame_decky_store
|
||||
import frame_decky
|
||||
|
||||
|
||||
class Compatibility(unittest.TestCase):
|
||||
def test_table_matches_store_data_and_complete_browser_and_fake_evidence(self):
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
subprocess.run([sys.executable, str(root/'scripts/framey-compatibility-doc.py'), '--check'], check=True)
|
||||
records = json.loads(compatibility.DATA.read_text())['plugins']
|
||||
self.assertEqual(len({p['id'] for p in records}), len(records))
|
||||
for record in records:
|
||||
self.assertEqual(record['browser']['id'], record['id'])
|
||||
self.assertEqual(record['fakeFrame']['sha256'], record['sha256'])
|
||||
self.assertIn(record['status'], ('works', 'partial', 'broken', 'na'))
|
||||
|
||||
def test_claims_require_exact_id_version_and_digest(self):
|
||||
records = json.loads(compatibility.DATA.read_text())['plugins']
|
||||
for plugin in records:
|
||||
with self.subTest(plugin=plugin['name']):
|
||||
expected = compatibility.lookup(plugin['id'], plugin['sha256'], plugin['version'])
|
||||
self.assertEqual(expected['status'], plugin['status'])
|
||||
for plugin_id, digest, version in (
|
||||
(plugin['id'], '0'*64, plugin['version']),
|
||||
(plugin['id'], plugin['sha256'], plugin['version']+'-new'),
|
||||
(99999, plugin['sha256'], plugin['version']),
|
||||
):
|
||||
result = compatibility.lookup(plugin_id, digest, version)
|
||||
self.assertEqual(result['status'], 'unverified')
|
||||
self.assertFalse(result['blockInstall'])
|
||||
|
||||
def test_installed_receipt_uses_its_digest_not_current_store_version(self):
|
||||
record = json.loads(compatibility.DATA.read_text())['plugins'][0]
|
||||
installed = {'plugins': [{'storeId': record['id'], 'version': record['version'],
|
||||
'sha256': record['sha256']},
|
||||
{'storeId': record['id'], 'version': record['version'], 'sha256': '0'*64}]}
|
||||
result = frame_decky.status(lambda *a, **k: json.dumps(installed))
|
||||
self.assertEqual(result['plugins'][0]['compatibility']['status'], record['status'])
|
||||
self.assertEqual(result['plugins'][1]['compatibility']['status'], 'unverified')
|
||||
|
||||
def test_hardware_block_precedes_any_download_or_ssh_mutation(self):
|
||||
with patch.object(frame_decky, 'status', return_value={'managed': True, 'running': True, 'plugins': []}), \
|
||||
patch.object(frame_decky_store, 'catalog', return_value=[{'id': 42, 'name': 'Hardware only',
|
||||
'sha256': 'a'*64, 'root': False, 'compatibility': {'blockInstall': True, 'reason': 'Not supported on Frame'}}]), \
|
||||
patch.object(frame_decky_store.urllib.request, 'urlopen') as download:
|
||||
def no_ssh(*a, **k):
|
||||
self.fail('must not mutate headset for known hardware blocker')
|
||||
with self.assertRaisesRegex(ValueError, 'Not supported on Frame'):
|
||||
frame_decky_store.change(no_ssh, 'install', 42)
|
||||
download.assert_not_called()
|
||||
|
||||
def test_missing_audit_data_is_unverified(self):
|
||||
with patch.object(compatibility, 'DATA', Path('/nonexistent/framey-audit.json')):
|
||||
self.assertEqual(compatibility.lookup(12, 'a'*64, '1.0')['status'], 'unverified')
|
||||
+8
-2
@@ -4,6 +4,7 @@ import json
|
||||
import re
|
||||
import secrets
|
||||
import urllib.request
|
||||
import framey_compatibility
|
||||
|
||||
|
||||
REPO = "SteamDeckHomebrew/decky-loader"
|
||||
@@ -38,7 +39,8 @@ def plugins(folder):
|
||||
found.append({'folder': child.name, 'name': str(data.get('name', child.name))[:80],
|
||||
'author': str(data.get('author', ''))[:80],
|
||||
'storeId': owned.get('storeId') if isinstance(owned.get('storeId'), int) else None,
|
||||
'version': str(owned.get('version', ''))[:40]})
|
||||
'version': str(owned.get('version', ''))[:40],
|
||||
'sha256': str(owned.get('sha256', ''))[:64]})
|
||||
except (ValueError, OSError, UnicodeError): pass
|
||||
return found
|
||||
os_release = {}
|
||||
@@ -154,7 +156,11 @@ finally:
|
||||
|
||||
|
||||
def status(ssh):
|
||||
return json.loads(ssh('python3 -', stdin=PROBE, timeout=15))
|
||||
result = json.loads(ssh('python3 -', stdin=PROBE, timeout=15))
|
||||
for plugin in result.get('plugins', []):
|
||||
plugin['compatibility'] = framey_compatibility.lookup(
|
||||
plugin.get('storeId'), plugin.get('sha256'), plugin.get('version'))
|
||||
return result
|
||||
|
||||
|
||||
def release_binary():
|
||||
|
||||
+26
-4
@@ -13,6 +13,7 @@ import urllib.request
|
||||
import zipfile
|
||||
|
||||
import frame_decky
|
||||
import framey_compatibility
|
||||
|
||||
|
||||
STORE_URL = 'https://plugins.deckbrew.xyz/plugins'
|
||||
@@ -65,7 +66,10 @@ def catalog():
|
||||
counts = {}
|
||||
for item in result:
|
||||
counts[item['id']] = counts.get(item['id'], 0) + 1
|
||||
return [item for item in result if counts[item['id']] == 1]
|
||||
unique = [item for item in result if counts[item['id']] == 1]
|
||||
for item in unique:
|
||||
item['compatibility'] = framey_compatibility.lookup(item['id'], item['sha256'], item['version'])
|
||||
return unique
|
||||
|
||||
|
||||
def archive_info(data, expected_hash, expected_name):
|
||||
@@ -171,6 +175,19 @@ try:
|
||||
z.extractall(stage)
|
||||
unpacked=stage/folder
|
||||
if not unpacked.is_dir(): raise RuntimeError('Missing plugin folder')
|
||||
# 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')
|
||||
for peer in base.iterdir():
|
||||
if peer==target or peer.is_symlink() or not peer.is_dir(): continue
|
||||
manifest=peer/'plugin.json'
|
||||
if manifest.is_symlink() or not manifest.is_file(): continue
|
||||
try:
|
||||
if manifest.stat().st_size>16384: continue
|
||||
peer_data=json.loads(manifest.read_text())
|
||||
if isinstance(peer_data,dict) and peer_data.get('name')==proposed_name:
|
||||
raise RuntimeError('A different plugin with this name is already installed')
|
||||
except (OSError,ValueError,UnicodeError): continue
|
||||
(unpacked/marker).write_text(json.dumps({'storeId':int(plugin_id),'sha256':digest,'version':version})+'\n')
|
||||
# Stop even a failed unit before removal to cancel any pending auto-restart.
|
||||
if running or action=='remove': service('stop','frame-control-decky.service')
|
||||
@@ -222,9 +239,14 @@ def change(ssh, action, plugin_id, report=lambda stage, percent=None: None, revi
|
||||
raise ValueError('The official store version changed; refresh the store and review it again')
|
||||
if plugin['root']:
|
||||
raise ValueError('Root plugins are not supported by the Frame user service')
|
||||
existing_name = [p for p in state['plugins'] if p.get('name') == plugin['name']]
|
||||
if action == 'install' and existing_name:
|
||||
raise ValueError('A plugin with this name is already installed')
|
||||
if plugin.get('compatibility', {}).get('blockInstall'):
|
||||
raise ValueError(plugin['compatibility']['reason'])
|
||||
# Decky routes backend calls by plugin name. A store update may rename
|
||||
# its plugin, so check names for updates as well as fresh installs.
|
||||
existing_name = [p for p in state['plugins'] if p.get('name') == plugin['name']
|
||||
and (action == 'install' or p.get('storeId') != plugin_id)]
|
||||
if existing_name:
|
||||
raise ValueError('A different plugin with this name is already installed')
|
||||
if action == 'update' and len(installed) != 1:
|
||||
raise ValueError('Only an installed Frame Control store plugin can be updated')
|
||||
data = None
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,28 @@
|
||||
"""Digest-scoped observations, never a trust or execution guarantee."""
|
||||
import json
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
DATA = Path(__file__).with_name('framey-compatibility.json')
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def _records(path):
|
||||
return json.loads(path.read_text())['plugins']
|
||||
|
||||
|
||||
def lookup(plugin_id, digest, version):
|
||||
unknown = {'status': 'unverified', 'reason': 'This exact store version has not been audited on Frame.',
|
||||
'verification': [], 'blockInstall': False}
|
||||
if type(plugin_id) is not int or not isinstance(digest, str) or not isinstance(version, str):
|
||||
return unknown
|
||||
try:
|
||||
records = _records(DATA)
|
||||
matches = [p for p in records if p['id'] == plugin_id and p['sha256'] == digest and p['version'] == version]
|
||||
if len(matches) != 1:
|
||||
return unknown
|
||||
record = matches[0]
|
||||
return {key: record[key] for key in ('status', 'reason', 'verification', 'blockInstall')}
|
||||
except (OSError, ValueError, KeyError, TypeError):
|
||||
return unknown
|
||||
+11
-6
@@ -50,10 +50,11 @@ iframe{display:block;width:100%;height:560px;background:#0d1925;border:1px solid
|
||||
<p id="deckyJobStatus" role="status" aria-live="polite"></p><progress id="deckyJobProgress" max="100"></progress>
|
||||
<div class="row"><button id="deckyJobCheck" class="secondary" hidden>Check result</button></div></div>
|
||||
<div id="deckyStorePanel" hidden><div class="row" style="margin:16px 0"><label>Find a plugin <input id="deckySearch" type="search" placeholder="Name, author or description"></label>
|
||||
<label>Show <select id="deckyStoreFilter"><option value="all">All plugins</option><option value="compatible">Without root requirement</option><option value="installed">Installed plugins</option></select></label></div>
|
||||
<label>Show <select id="deckyStoreFilter"><option value="all">All plugins</option><option value="compatible">No known install blocker</option><option value="installed">Installed plugins</option></select></label></div>
|
||||
<p id="deckyStoreSummary" role="status" aria-live="polite"></p><div id="deckyStoreList" class="stack"></div>
|
||||
<div class="row" style="margin-top:14px"><button id="deckyStorePrevious" class="secondary">Previous</button><span id="deckyStorePage"></span><button id="deckyStoreNext" class="secondary">Next</button></div></div>
|
||||
<div id="frameyPanel" hidden><div class="row" style="margin-top:16px"><strong id="frameyTitle"></strong><button id="frameyClose" class="secondary">Close plugin panel</button></div>
|
||||
<p id="frameyCompatibility"></p>
|
||||
<p id="frameyPanelStatus" role="status">Loading plugin panel…</p>
|
||||
<iframe id="frameyFrame" title="Framey Decky plugin panel" sandbox="allow-scripts allow-popups" referrerpolicy="no-referrer"></iframe></div>
|
||||
<h2>Extensions</h2><p>Local additions run only when opened in the isolated panel below. They can read status but cannot change headset settings through the extension API.</p>
|
||||
@@ -103,6 +104,7 @@ async function refreshGames(){const list=byId('games');try{const data=await api(
|
||||
catch(error){byId('actionStatus').textContent=error.message;quit.disabled=false;}};
|
||||
row.append(name,quit);list.append(row);}if(!data.running.length)list.textContent='No games are running.';
|
||||
}catch(error){list.textContent=error.message;}}
|
||||
function compatibilityText(plugin){const c=plugin.compatibility;return c ? `${c.status === 'na' ? 'N/A' : c.status}: ${c.reason}${c.verification?.length ? ' Verified: '+c.verification.join(', ')+'.' : ''}` : 'Unverified on Frame';}
|
||||
let deckyState=null, deckyStore=[], deckyStoreIndex=0, deckyStoreRequest=0,
|
||||
activeFrameyId=null, frameySession=null, deckyBusy=false;
|
||||
async function refreshDecky(){try{deckyState=await api('/api/decky');
|
||||
@@ -112,6 +114,7 @@ async function refreshDecky(){try{deckyState=await api('/api/decky');
|
||||
const list=byId('deckyInstalled');list.replaceChildren();
|
||||
for(const plugin of deckyState.plugins||[]){const row=document.createElement('div');row.className='block';
|
||||
const name=document.createElement('strong');name.textContent=`${plugin.name} ${plugin.version||''}`;row.append(name);
|
||||
const note=document.createElement('p');note.textContent=compatibilityText(plugin);row.append(note);
|
||||
if(plugin.storeId){const open=document.createElement('button');open.className='secondary';open.textContent='Open panel';
|
||||
open.onclick=()=>openFrameyPanel(plugin.storeId,plugin.name);row.append(open);
|
||||
const button=document.createElement('button');button.className='danger';button.textContent='Remove';
|
||||
@@ -130,7 +133,7 @@ function showDeckyStore(){const list=byId('deckyStoreList');list.replaceChildren
|
||||
const q=byId('deckySearch').value.trim().toLowerCase();
|
||||
const filter=byId('deckyStoreFilter').value;
|
||||
const found=deckyStore.filter(p=>(!q||`${p.name} ${p.author||''} ${p.description}`.toLowerCase().includes(q))&&
|
||||
(filter==='all'||(filter==='compatible'&&!p.root)||(filter==='installed'&&(deckyState?.plugins||[]).some(x=>x.storeId===p.id))));
|
||||
(filter==='all'||(filter==='compatible'&&!p.root&&!p.compatibility?.blockInstall)||(filter==='installed'&&(deckyState?.plugins||[]).some(x=>x.storeId===p.id))));
|
||||
const size=20, pages=Math.max(1,Math.ceil(found.length/size));
|
||||
deckyStoreIndex=Math.min(deckyStoreIndex,pages-1);
|
||||
byId('deckyStoreSummary').textContent=`${found.length} plugin${found.length===1?'':'s'} found`;
|
||||
@@ -140,12 +143,13 @@ function showDeckyStore(){const list=byId('deckyStoreList');list.replaceChildren
|
||||
if(!found.length)list.textContent='No plugins match this search.';
|
||||
for(const plugin of found.slice(deckyStoreIndex*size,(deckyStoreIndex+1)*size)){
|
||||
const row=document.createElement('div');row.className='block game';
|
||||
const label=document.createElement('span');label.textContent=`${plugin.name} ${plugin.version}${plugin.author?' · '+plugin.author:''} · ${plugin.description}`;
|
||||
const label=document.createElement('div');label.textContent=`${plugin.name} ${plugin.version}${plugin.author?' · '+plugin.author:''} · ${plugin.description}`;
|
||||
const button=document.createElement('button');button.textContent='Install';
|
||||
const installed=(deckyState?.plugins||[]).find(p=>p.storeId===plugin.id);
|
||||
if(installed)button.textContent=installed.version===plugin.version?'Reinstall':'Update';
|
||||
button.disabled=deckyBusy||!deckyState?.running||plugin.root;
|
||||
if(plugin.root)label.textContent+=' · Needs root; not supported on Frame';
|
||||
button.disabled=deckyBusy||!deckyState?.running||plugin.root||!!plugin.compatibility?.blockInstall;
|
||||
const note=document.createElement('p');note.textContent=compatibilityText(plugin);label.append(note);
|
||||
if(plugin.root && !plugin.compatibility?.blockInstall)label.append(' · Needs root; not supported on Frame');
|
||||
button.onclick=()=>deckyChange(installed?'update':'install',plugin);row.append(label,button);list.append(row);}}
|
||||
function deckyJobView(title,message,percent,failed=false){
|
||||
byId('deckyJobPanel').hidden=false;byId('deckyJobTitle').textContent=title;
|
||||
@@ -191,7 +195,8 @@ async function checkDeckyJob(){let saved;try{saved=JSON.parse(sessionStorage.get
|
||||
finally{deckyBusy=false;await refreshDecky();}}
|
||||
byId('deckyJobCheck').onclick=checkDeckyJob;
|
||||
function openFrameyPanel(id,name){
|
||||
if(!(deckyState?.plugins||[]).some(p=>p.storeId===id))return;
|
||||
const installed=(deckyState?.plugins||[]).find(p=>p.storeId===id);if(!installed)return;
|
||||
byId('frameyCompatibility').textContent=compatibilityText(installed);
|
||||
activeFrameyId=id;frameySession=`${Date.now()}-${Math.random()}`;byId('frameyTitle').textContent=name;
|
||||
byId('frameyPanelStatus').textContent='Loading plugin panel…';
|
||||
byId('frameyFrame').src=`/framey/panel/${id}?session=${encodeURIComponent(frameySession)}#${frameySession}`;
|
||||
|
||||
+4
-2
@@ -2189,6 +2189,7 @@ $('extensionClose').onclick = () => {
|
||||
$('extensionFrame').removeAttribute('src');
|
||||
$('extensionPanel').hidden = true;
|
||||
};
|
||||
function deckyCompatibilityText(plugin) {const c=plugin.compatibility;return c ? `${c.status === 'na' ? 'N/A' : c.status}: ${c.reason}${c.verification?.length ? ' Verified: '+c.verification.join(', ')+'.' : ''}` : 'Unverified on Frame';}
|
||||
let deckyInfo = null;
|
||||
let deckyCatalog = [];
|
||||
async function loadDecky() {
|
||||
@@ -2200,7 +2201,7 @@ async function loadDecky() {
|
||||
const managedList=$('deckyManagedList');managedList.replaceChildren();
|
||||
for(const plugin of d.plugins||[]){if(!plugin.storeId)continue;
|
||||
const row=document.createElement('div');row.className='hint';
|
||||
const label=document.createElement('span');label.textContent=`${plugin.name} ${plugin.version||''}`;
|
||||
const label=document.createElement('span');label.textContent=`${plugin.name} ${plugin.version||''} · ${deckyCompatibilityText(plugin)}`;
|
||||
const remove=document.createElement('button');remove.className='small danger';remove.textContent='Remove plugin';
|
||||
remove.disabled=!d.managed||!$('deckyAck').checked;
|
||||
remove.onclick=()=>changeDeckyPlugin('remove',{id:plugin.storeId,name:plugin.name});
|
||||
@@ -2226,8 +2227,9 @@ function showDeckyStore() {
|
||||
const description = document.createElement('span'); description.textContent = ` ${plugin.description}`;
|
||||
const entry = installed.find(p => p.storeId === plugin.id);
|
||||
const unmanaged = installed.some(p => p.name === plugin.name && p.storeId !== plugin.id);
|
||||
const reason = plugin.root ? 'Needs root; not supported by the Frame user service.' : unmanaged ? 'Existing plugin is not managed by Frame Control.' : '';
|
||||
const reason = plugin.root ? 'Needs root; not supported by the Frame user service.' : plugin.compatibility?.blockInstall ? plugin.compatibility.reason : unmanaged ? 'Existing plugin is not managed by Frame Control.' : '';
|
||||
row.append(title, description);
|
||||
const compatibility=document.createElement('p');compatibility.textContent=deckyCompatibilityText(plugin);row.append(compatibility);
|
||||
if (reason) { const note=document.createElement('small'); note.textContent=` ${reason}`; row.append(note); }
|
||||
else {
|
||||
const button=document.createElement('button'); button.className='small';
|
||||
|
||||
Reference in new issue
Block a user