mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-27 00:02:12 +01:00
Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4967b13dca | ||
|
|
04cde721c8 | ||
|
|
1ddb6278b8 | ||
|
|
cbc20f0933 | ||
|
|
b8d57eb6bd | ||
|
|
4252d22975 | ||
|
|
5438e1d9d0 | ||
|
|
9418a340eb | ||
|
|
a8bf01d660 | ||
|
|
5e3f9559c7 | ||
|
|
a31b4a426c | ||
|
|
ff465accc5 | ||
|
|
d9c2cc3f27 | ||
|
|
910d847573 | ||
|
|
89f986b91c | ||
|
|
e711945c5a | ||
|
|
3ca8de985e | ||
|
|
035c59ee11 | ||
|
|
c687bcc71f | ||
|
|
b6f45b287f | ||
|
|
a77434d91b | ||
|
|
e07fa15592 | ||
|
|
1f5b37a755 | ||
|
|
77c54c0b01 | ||
|
|
d38af757ec | ||
|
|
211f4fb148 | ||
|
|
72fd779170 | ||
|
|
286e692147 | ||
|
|
fe51d29eb4 | ||
|
|
4b47c384e6 | ||
|
|
7baafc9a18 | ||
|
|
b0d8bf3968 | ||
|
|
b05a666e50 |
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"version": "0.4.46",
|
||||
"version": "0.4.48",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frame-control",
|
||||
"version": "0.4.46",
|
||||
"version": "0.4.48",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^44.4.5",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"productName": "Frame Control",
|
||||
"version": "0.4.46",
|
||||
"version": "0.4.48",
|
||||
"description": "Desktop app for managing a Valve Steam Frame over SSH",
|
||||
"private": true,
|
||||
"main": "main.js",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Chunked Framey frontend audit route, 26 October 2026
|
||||
|
||||
The local 116-plugin Chromium audit served chunked modules at a route that
|
||||
omitted the store receipt, while the production panel requested
|
||||
`/framey/module/<id>/<receipt>/<file>`. The audit parsed that receipt as part
|
||||
of the file path and returned 404. This made its result for chunked frontends
|
||||
weaker than the separate targeted panel checks.
|
||||
|
||||
The audit now accepts the production URL shape only when the plugin ID,
|
||||
receipt and JavaScript path are valid, the receipt equals the catalog SHA-256,
|
||||
and the cached archive bytes match that SHA-256. Invalid IDs, receipts, paths
|
||||
and changed archive bytes fail before the module is served. The production
|
||||
Framey panel and installer are unchanged.
|
||||
|
||||
## Exact archived browser check
|
||||
|
||||
Pinned official Deck Shelves 2.0.0 (store ID 135, SHA-256
|
||||
`b566280ad38fd3ecd584b5b46a855f301539eb9a86063834c64b7045f7063f91`)
|
||||
ran through the repaired audit in an opaque Chromium iframe. Its entry module
|
||||
and all eight JavaScript chunks returned HTTP 200 at receipt-bound URLs. The
|
||||
frontend reported ready and called `get_settings`; the audit denied that
|
||||
backend call and a Steam library read. Plugin network access remained blocked
|
||||
by the audit policy. The previous
|
||||
catalog browser observation had failed to resolve `./chunks/...` from a blob
|
||||
URL. This check verifies the current audit route and frontend initializer,
|
||||
not the official backend or a live shelf.
|
||||
|
||||
The focused route regression covers entry and chunk reads plus wrong receipt,
|
||||
path traversal, malformed ID and changed archive bytes. The catalog's
|
||||
**4 works / 50 partial / 51 broken / 11 N/A** statuses are unchanged. Deck
|
||||
Shelves remains partial; installed settings, home-shelf behavior, worn VR and
|
||||
physical controller input remain unverified.
|
||||
@@ -4,7 +4,17 @@ The exact official Deck Shelves 2.0.0 archive (store ID 135, SHA-256 `b566280ad3
|
||||
|
||||
Framey now serves JavaScript files from the managed plugin's `dist` directory using hierarchical module URLs. Its opaque `allow-scripts` iframe still has no Frame UI key. Local Chromium received HTTP 200 for the entry and eight chunks, reported the initializer ready, and rendered the settings panel with “Enable shelves on Home,” “Create default shelves,” “Behavior,” and “Shelves.” The browser audit denied the attempted `get_settings` backend call, Steam library read and plugin network access. No backend method executed.
|
||||
|
||||
The catalog moves this exact version from broken to **partial** for visible local settings only. The plugin's main feature injects shelves into Steam Home; Framey has no verified Steam Home patch target. Settings writes, official backend behavior, real Frame module serving, wearer-visible VR, and physical controller input remain unverified. The [compatibility table](../framey-compatibility.md) and store views retain these limits.
|
||||
The catalog moves this exact version from broken to **partial** for visible local settings. The plugin's main feature injects shelves into Steam Home; Framey has no verified Steam Home patch target. Actual Decky RPC, real Frame settings, wearer-visible VR, and physical controller input remain unverified. The [compatibility table](../framey-compatibility.md) and store views retain these limits.
|
||||
|
||||
## Isolated official backend settings check
|
||||
|
||||
I verified the same archive digest before loading its unchanged `Deck-Shelves/main.py` in local Python. A stub `decky` module supplied only a logger; `DECKY_PLUGIN_SETTINGS_DIR` and `DECKY_HOME` pointed inside a temporary directory. No Decky service, headset, Steam API, or network call was involved. Calling `_main`, `get_settings`, and `set_settings` returned the default disabled state, saved one shelf named `Local Test`, and returned that shelf from a fresh `Plugin` instance. The temporary directory was removed after the check.
|
||||
|
||||
```json
|
||||
{"archiveSha256":"b566280ad38fd3ecd584b5b46a855f301539eb9a86063834c64b7045f7063f91","defaultEnabled":false,"defaultShelves":0,"saveResult":true,"reopenedEnabled":true,"reopenedShelfTitle":"Local Test","settingsFileExists":true}
|
||||
```
|
||||
|
||||
This proves only the archive's Python settings logic under those local conditions. It does not prove that Decky 3.3.0 can dispatch these methods on the ARM64 Frame, that the opaque panel can save settings through the production bridge, or that a shelf appears on Steam Home or in VR.
|
||||
|
||||
## Exact source real Frame gates
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Emuchievements ARM64 hash helper feasibility, 26 October 2026
|
||||
|
||||
The exact official Emuchievements 2.0.5 archive (store ID 28, SHA-256
|
||||
`98dcb443e81b9f9011cfe77c69eb659869c8e2df2df8b0a443df66cbfc2f5a89`)
|
||||
contains an x86_64 `Emuchievements/bin/hash`. Frame Control still blocks
|
||||
that archive on ARM64; this experiment did not install the plugin.
|
||||
|
||||
The upstream source commit
|
||||
[`8567e94befffc943434b2adb842487274815f8bb`](https://github.com/EmuDeck/Emuchievements/commit/8567e94befffc943434b2adb842487274815f8bb)
|
||||
declares version 2.0.5. Its `src/py/main.py` matches the published ZIP's
|
||||
`Emuchievements/main.py` byte for byte (SHA-256
|
||||
`d4e72f136c94e48919575937ec4190f28864cc259eb111ef47f0df99f5912645`).
|
||||
The pinned build dependencies at that commit are libarchive
|
||||
`b3be94ba4f36943745e64a6a0a3c21c2d1281d3c`, libchdr
|
||||
`a20e04c12c0c20048bf771da6df7aa2e51f5e0cb`, and rcheevos
|
||||
`85a3209398e576d79170f569fcddf277640f3f06`.
|
||||
|
||||
On a separate Linux host, the version-matched source and submodules built an
|
||||
x86_64 `hash` with CMake. It and the official x86_64 binary both returned
|
||||
`ff78ca6c21ac11504dbddbd5d4e7524a` for one synthetic `.rom` file. An
|
||||
isolated Debian cross-build using `gcc-aarch64-linux-gnu` and
|
||||
`g++-aarch64-linux-gnu` produced an AArch64 binary with SHA-256
|
||||
`9c7aedb80bdd64941aa5dd9b41f9bb8471411c812e04c7ee7d60486779d99997`.
|
||||
It links to `liblzma.so.5`, `libstdc++.so.6`, `libgcc_s.so.1`, and `libc.so.6`.
|
||||
|
||||
A real Frame test acquired `/tmp/frame-test.lock` atomically with a unique
|
||||
owner before reading the device. SteamVR activity was **3** (standby),
|
||||
proximity raw **3.0625**, and battery **100%**. The ARM64 helper ran against
|
||||
the same synthetic ROM in a private `/tmp` directory, exited 0, and returned
|
||||
the same hash. The owned directory and lock were removed. No normal service,
|
||||
power setting, Steam library, real ROM, or installed plugin was changed.
|
||||
The separate host's source/build directory was also removed after the test.
|
||||
|
||||
This proves only a single-file helper path. The official ZIP remains blocked
|
||||
because it still bundles x86_64 bytes. A release-ready port needs a packaged
|
||||
ARM64 helper with GPL-3.0 source and license compliance, format-specific hash
|
||||
coverage (including archives and CHD), a managed plugin transaction, actual
|
||||
Decky backend and frontend checks, and cleanup. RetroAchievements login,
|
||||
real ROM scanning, wearer-visible VR, and physical controller use remain
|
||||
unverified.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Game Theme Music local panel check, 26 October 2026
|
||||
|
||||
The exact official Game Theme Music 1.7.1-1 archive (store ID 52, SHA-256 `9ce293ab4db212c03c6b96dfbc5e4d92e874d33387463f53223773921f75a80e`) passed the existing fake Frame install/remove audit. Its prior local Chromium run stopped before initialization with `Cannot read properties of null (reading 'type')`: the frontend dereferenced the result of `DFL.fakeRenderComponent` while looking for Steam's Library Context Menu.
|
||||
|
||||
Framey's isolated host now returns an inert local component type from that lookup. The patch API remains a no-op and does not expose or modify a Steam React tree. In the repository's opaque Chromium audit, the unchanged archive reported initializer ready and rendered a Settings view containing Music Volume, Default Muted, Use yt-dlp, Invidious Instance, Download Music, Overrides and Extras. The audit observed a `get_setting` request, but denied the backend call. It also denied plugin network requests; a YouTube/Invidious fetch failed as expected. The bounded root-text observation came from the test iframe and contained only these public UI labels.
|
||||
|
||||
The catalog therefore moves this digest from **broken** to **partial** for the visible local settings panel. No theme was played or injected into a Steam game page. Music search, playback, downloads, settings persistence, live ARM64 backend behavior, wearer-visible VR and physical controller input remain unverified. The earlier failed browser observation remains in the canonical JSON alongside this follow-up so the change in evidence is explicit.
|
||||
@@ -0,0 +1,807 @@
|
||||
{
|
||||
"timeUtc": "2026-10-26T21:15:13.918421+00:00",
|
||||
"plugins": [
|
||||
{
|
||||
"id": 12,
|
||||
"name": "MusicControl",
|
||||
"version": "1.1.6",
|
||||
"sha256": "246b89fd653c60a735b1cc401d1b0937d5cb969eea9024561661ff741f081d62",
|
||||
"backend": "Python",
|
||||
"frontendApi": "legacy"
|
||||
},
|
||||
{
|
||||
"id": 111,
|
||||
"name": "Music Control",
|
||||
"version": "1.1.7",
|
||||
"sha256": "3f56a3f56034b277ea74a2c37cc84133b69e382aa6bcb0b43876776aac737ed9",
|
||||
"backend": "Python",
|
||||
"frontendApi": "legacy"
|
||||
}
|
||||
],
|
||||
"regressions": {
|
||||
"removedBoundMethodShim": {
|
||||
"result": "Plugin._get_dbus_env() missing 1 required positional argument: 'self'",
|
||||
"success": false
|
||||
},
|
||||
"realFrameSubprocessDiagnostic": {
|
||||
"uid": 1000,
|
||||
"ldLibrary": "<Decky PyInstaller extraction directory>",
|
||||
"results": [
|
||||
{
|
||||
"shell": false,
|
||||
"code": 0,
|
||||
"hasArray": true,
|
||||
"stderr": ""
|
||||
},
|
||||
{
|
||||
"shell": true,
|
||||
"code": 1,
|
||||
"hasArray": false,
|
||||
"stderr": "/bin/sh: /usr/lib/libc.so.6: version `GLIBC_2.42' not found (required by <Decky PyInstaller extraction directory>/libtinfo.so.6)\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"realFrame": {
|
||||
"scope": "real Frame ARM64 Decky3.3; only discovery and selected-player read, no player controls",
|
||||
"runtime": {
|
||||
"version": "v3.3.0",
|
||||
"sha256": "8ef5dcf36cbbbb6e97201a6baed5d79c4cc44ec27789fc9af1c5b6ae8f0b25b6"
|
||||
},
|
||||
"before": {
|
||||
"arch": "aarch64",
|
||||
"uid": 1000,
|
||||
"decky": "failed",
|
||||
"panel": "inactive",
|
||||
"artLinkPresent": false,
|
||||
"imagesParentPresent": true,
|
||||
"battery": [
|
||||
100
|
||||
]
|
||||
},
|
||||
"installs": [
|
||||
{
|
||||
"id": 12,
|
||||
"receipt": {
|
||||
"storeId": 12,
|
||||
"sha256": "246b89fd653c60a735b1cc401d1b0937d5cb969eea9024561661ff741f081d62",
|
||||
"version": "1.1.6",
|
||||
"compatShims": [
|
||||
"music-control-system-dbus-env"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 111,
|
||||
"receipt": {
|
||||
"storeId": 111,
|
||||
"sha256": "3f56a3f56034b277ea74a2c37cc84133b69e382aa6bcb0b43876776aac737ed9",
|
||||
"version": "1.1.7",
|
||||
"compatShims": [
|
||||
"music-control-system-dbus-env"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"calls": [
|
||||
{
|
||||
"id": 12,
|
||||
"method": "sp_list_media_players",
|
||||
"success": true,
|
||||
"playerCount": 0
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"method": "sp_get_media_player",
|
||||
"response": {
|
||||
"result": "org.mpris.MediaPlayer2.spotify",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 111,
|
||||
"method": "sp_list_media_players",
|
||||
"success": true,
|
||||
"playerCount": 0
|
||||
},
|
||||
{
|
||||
"id": 111,
|
||||
"method": "sp_get_media_player",
|
||||
"response": {
|
||||
"result": "org.mpris.MediaPlayer2.spotify",
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"cleanup": {
|
||||
"baseAbsent": true,
|
||||
"unitAbsent": true,
|
||||
"artLinkAbsent": true
|
||||
},
|
||||
"after": {
|
||||
"arch": "aarch64",
|
||||
"uid": 1000,
|
||||
"decky": "failed",
|
||||
"panel": "inactive",
|
||||
"artLinkPresent": false,
|
||||
"imagesParentPresent": true,
|
||||
"battery": [
|
||||
100
|
||||
]
|
||||
},
|
||||
"lockReleased": true
|
||||
},
|
||||
"isolatedRuntime": {
|
||||
"version": "v3.3.0",
|
||||
"architecture": "amd64",
|
||||
"sha256": "e80d03e641d1766a33a817cb27ab8d2e0a4633685e5d27e4de948a6687526181",
|
||||
"isolation": "Docker network none; no host audio/D-Bus/home/device mounts; dropped capabilities; private dbus-daemon and silent synthetic MPRIS service; archives and scripts read-only"
|
||||
},
|
||||
"backendFixtureChecks": [
|
||||
{
|
||||
"id": 12,
|
||||
"version": "1.1.6",
|
||||
"calls": [
|
||||
{
|
||||
"method": "sp_list_media_players",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "org.mpris.MediaPlayer2.FrameySilent",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_set_media_player",
|
||||
"kwargs": {
|
||||
"player": "org.mpris.MediaPlayer2.FrameySilent"
|
||||
},
|
||||
"response": {
|
||||
"result": null,
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_get_media_player",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "org.mpris.MediaPlayer2.FrameySilent",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_identity",
|
||||
"kwargs": {
|
||||
"orgPath": "org.mpris.MediaPlayer2.FrameySilent"
|
||||
},
|
||||
"response": {
|
||||
"result": "Player",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "get_meta_data",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "trackid|/org/framey/TestTrack/1\nlength|180000000\ntitle|Silent Test Track\nartist|Fixture Artist\nalbum|Framey Test Album\n",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_track_status",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "Paused",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_track_progress",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "12000000",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_can_seek",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "true",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_get_volume",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "0.4",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_test_volume_control",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "true",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_play",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_track_status",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "Playing",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_pause",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_track_status",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "Paused",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_next",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_previous",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_seek",
|
||||
"kwargs": {
|
||||
"amount": 2000000
|
||||
},
|
||||
"response": {
|
||||
"result": "",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_track_progress",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "14000000",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_set_position",
|
||||
"kwargs": {
|
||||
"position": 45000000,
|
||||
"trackid": "/org/framey/TestTrack/1"
|
||||
},
|
||||
"response": {
|
||||
"result": "",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_track_progress",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "45000000",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_set_volume",
|
||||
"kwargs": {
|
||||
"volume": 0.25
|
||||
},
|
||||
"response": {
|
||||
"result": "method return time=1793049278.417118 sender=:1.0 -> destination=:1.19 serial=20 reply_serial=2\n",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_get_volume",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "0.25",
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"independentFixtureState": {
|
||||
"PlaybackStatus": "Paused",
|
||||
"Position": 45000000,
|
||||
"Volume": 0.25,
|
||||
"next": 1,
|
||||
"previous": 1,
|
||||
"writes": [
|
||||
[
|
||||
"PlayPause"
|
||||
],
|
||||
[
|
||||
"Pause"
|
||||
],
|
||||
[
|
||||
"Next"
|
||||
],
|
||||
[
|
||||
"Previous"
|
||||
],
|
||||
[
|
||||
"Seek",
|
||||
2000000
|
||||
],
|
||||
[
|
||||
"SetPosition",
|
||||
45000000
|
||||
],
|
||||
[
|
||||
"Volume",
|
||||
0.25
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 111,
|
||||
"version": "1.1.7",
|
||||
"calls": [
|
||||
{
|
||||
"method": "sp_list_media_players",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "org.mpris.MediaPlayer2.FrameySilent",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_set_media_player",
|
||||
"kwargs": {
|
||||
"player": "org.mpris.MediaPlayer2.FrameySilent"
|
||||
},
|
||||
"response": {
|
||||
"result": null,
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_get_media_player",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "org.mpris.MediaPlayer2.FrameySilent",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_identity",
|
||||
"kwargs": {
|
||||
"orgPath": "org.mpris.MediaPlayer2.FrameySilent"
|
||||
},
|
||||
"response": {
|
||||
"result": "Player",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "get_meta_data",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "trackid|/org/framey/TestTrack/1\nlength|180000000\ntitle|Silent Test Track\nartist|Fixture Artist\nalbum|Framey Test Album",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_track_status",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "Paused",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_track_progress",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "12000000",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_can_seek",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "true",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_get_volume",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "0.4",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_test_volume_control",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "true",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_play",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "method return time=1793049473.137015 sender=:1.0 -> destination=:1.12 serial=13 reply_serial=2\n",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_track_status",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "Playing",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_pause",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "method return time=1793049473.340963 sender=:1.0 -> destination=:1.14 serial=15 reply_serial=2\n",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_track_status",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "Paused",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_next",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "method return time=1793049473.547273 sender=:1.0 -> destination=:1.16 serial=17 reply_serial=2\n",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_previous",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "method return time=1793049473.649558 sender=:1.0 -> destination=:1.17 serial=18 reply_serial=2\n",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_seek",
|
||||
"kwargs": {
|
||||
"amount": 2000000
|
||||
},
|
||||
"response": {
|
||||
"result": "method return time=1793049473.752790 sender=:1.0 -> destination=:1.18 serial=19 reply_serial=2\n",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_track_progress",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "14000000",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_set_position",
|
||||
"kwargs": {
|
||||
"position": 45000000,
|
||||
"trackid": "/org/framey/TestTrack/1"
|
||||
},
|
||||
"response": {
|
||||
"result": "method return time=1793049473.965468 sender=:1.0 -> destination=:1.20 serial=21 reply_serial=2\n",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_track_progress",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "45000000",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_set_volume",
|
||||
"kwargs": {
|
||||
"volume": 0.25
|
||||
},
|
||||
"response": {
|
||||
"result": "method return time=1793049474.175137 sender=:1.0 -> destination=:1.22 serial=23 reply_serial=2\n",
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "sp_get_volume",
|
||||
"kwargs": {},
|
||||
"response": {
|
||||
"result": "0.25",
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"independentFixtureState": {
|
||||
"PlaybackStatus": "Paused",
|
||||
"Position": 45000000,
|
||||
"Volume": 0.25,
|
||||
"next": 1,
|
||||
"previous": 1,
|
||||
"writes": [
|
||||
[
|
||||
"Volume",
|
||||
0.01
|
||||
],
|
||||
[
|
||||
"Volume",
|
||||
0.4
|
||||
],
|
||||
[
|
||||
"PlayPause"
|
||||
],
|
||||
[
|
||||
"Pause"
|
||||
],
|
||||
[
|
||||
"Next"
|
||||
],
|
||||
[
|
||||
"Previous"
|
||||
],
|
||||
[
|
||||
"Seek",
|
||||
2000000
|
||||
],
|
||||
[
|
||||
"SetPosition",
|
||||
45000000
|
||||
],
|
||||
[
|
||||
"Volume",
|
||||
0.25
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"browserChecks": [
|
||||
{
|
||||
"id": 12,
|
||||
"scope": "opaque local Chromium + real amd64 Decky3.3 + private silent MPRIS",
|
||||
"checks": [
|
||||
{
|
||||
"rendered": "Music Control\nCurrently Playing\nSilent Test Track\nFixture Artist\nPlayback Volume\nPlayer\nInfo"
|
||||
},
|
||||
{
|
||||
"clickedButton": 0,
|
||||
"independentReadback": {
|
||||
"PlaybackStatus": "Paused",
|
||||
"Position": 45000000,
|
||||
"Volume": 0.25,
|
||||
"next": 1,
|
||||
"previous": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"clickedButton": 2,
|
||||
"independentReadback": {
|
||||
"PlaybackStatus": "Paused",
|
||||
"Position": 45000000,
|
||||
"Volume": 0.25,
|
||||
"next": 2,
|
||||
"previous": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"clickedButton": 1,
|
||||
"independentReadback": {
|
||||
"PlaybackStatus": "Playing",
|
||||
"Position": 45000000,
|
||||
"Volume": 0.25,
|
||||
"next": 2,
|
||||
"previous": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"clickedButton": 1,
|
||||
"independentReadback": {
|
||||
"PlaybackStatus": "Paused",
|
||||
"Position": 45000000,
|
||||
"Volume": 0.25,
|
||||
"next": 2,
|
||||
"previous": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"slider": 0,
|
||||
"value": 0.5,
|
||||
"independentReadback": {
|
||||
"PlaybackStatus": "Paused",
|
||||
"Position": 90000000,
|
||||
"Volume": 0.25,
|
||||
"next": 2,
|
||||
"previous": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"slider": 1,
|
||||
"value": 60,
|
||||
"independentReadback": {
|
||||
"PlaybackStatus": "Paused",
|
||||
"Position": 90000000,
|
||||
"Volume": 0.6,
|
||||
"next": 2,
|
||||
"previous": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"infoRoute": "Music Control\nBack to plugin\nMusicControl\nInfoUsageTroubleshooting\nMusicControl allows you to control any media playback that is currently running that implements the MPRIS dbus interface.\nExamples of application that are supported:\nSpotify\nCider\nChrome / Firefox\nStrawberry\nOnly the flatpak versions of these applications have been tested, and not all of them might support the full feature set, so it is recommended to use the discover store to install the applications."
|
||||
},
|
||||
{
|
||||
"reopened": "Music Control\nCurrently Playing\nSilent Test Track\nFixture Artist\nPlayer\nInfo",
|
||||
"independentReadback": {
|
||||
"PlaybackStatus": "Paused",
|
||||
"Position": 90000000,
|
||||
"Volume": 0.6,
|
||||
"next": 2,
|
||||
"previous": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 111,
|
||||
"scope": "opaque local Chromium + real amd64 Decky3.3 + private silent MPRIS",
|
||||
"checks": [
|
||||
{
|
||||
"rendered": "Music Control\nCurrently Playing\nSilent Test Track\nFixture Artist\nPlayback Volume\nPlayer\nInfo"
|
||||
},
|
||||
{
|
||||
"clickedButton": 0,
|
||||
"independentReadback": {
|
||||
"PlaybackStatus": "Paused",
|
||||
"Position": 45000000,
|
||||
"Volume": 0.25,
|
||||
"next": 1,
|
||||
"previous": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"clickedButton": 2,
|
||||
"independentReadback": {
|
||||
"PlaybackStatus": "Paused",
|
||||
"Position": 45000000,
|
||||
"Volume": 0.25,
|
||||
"next": 2,
|
||||
"previous": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"clickedButton": 1,
|
||||
"independentReadback": {
|
||||
"PlaybackStatus": "Playing",
|
||||
"Position": 45000000,
|
||||
"Volume": 0.25,
|
||||
"next": 2,
|
||||
"previous": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"clickedButton": 1,
|
||||
"independentReadback": {
|
||||
"PlaybackStatus": "Paused",
|
||||
"Position": 45000000,
|
||||
"Volume": 0.25,
|
||||
"next": 2,
|
||||
"previous": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"slider": 0,
|
||||
"value": 0.5,
|
||||
"independentReadback": {
|
||||
"PlaybackStatus": "Paused",
|
||||
"Position": 90000000,
|
||||
"Volume": 0.25,
|
||||
"next": 2,
|
||||
"previous": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"slider": 1,
|
||||
"value": 60,
|
||||
"independentReadback": {
|
||||
"PlaybackStatus": "Paused",
|
||||
"Position": 90000000,
|
||||
"Volume": 0.6,
|
||||
"next": 2,
|
||||
"previous": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"infoRoute": "Music Control\nBack to plugin\nMusicControl\nInfoUsageTroubleshooting\nMusicControl allows you to control any media playback that is currently running that implements the MPRIS dbus interface.\nExamples of application that are supported:\nSpotify\nCider\nChrome / Firefox\nStrawberry\nOnly the flatpak versions of these applications have been tested, and not all of them might support the full feature set, so it is recommended to use the discover store to install the applications."
|
||||
},
|
||||
{
|
||||
"reopened": "Music Control\nCurrently Playing\nSilent Test Track\nFixture Artist\nPlayer\nInfo",
|
||||
"independentReadback": {
|
||||
"PlaybackStatus": "Paused",
|
||||
"Position": 90000000,
|
||||
"Volume": 0.6,
|
||||
"next": 2,
|
||||
"previous": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"limitations": [
|
||||
"Real Frame had no running MPRIS players. No real media transport or volume writes ran.",
|
||||
"The private fixture emits no audio and does not prove playback in a supported media app.",
|
||||
"Album art, physical controller, VR keyboard and worn VR were not tested.",
|
||||
"Both upstream backends truncate multiword Identity to its final word.",
|
||||
"Music Control 1.1.7 automatically probes player volume by writing 0.01 and restoring it; this ran only against the private fixture.",
|
||||
"Existing installed copies require remove/reinstall or an explicit managed update."
|
||||
],
|
||||
"validation": {
|
||||
"pythonSuite": {
|
||||
"run": 1639,
|
||||
"skipped": 16,
|
||||
"passed": true
|
||||
},
|
||||
"fakeFrame": {
|
||||
"total": 116,
|
||||
"results": {
|
||||
"blocked": 55,
|
||||
"installed-removed": 61
|
||||
},
|
||||
"backendExecution": false
|
||||
},
|
||||
"currentStore": {
|
||||
"checkedAtUtc": "2026-10-26T21:17:39.609721+00:00",
|
||||
"plugins": 116,
|
||||
"allPinnedDigestsUnchanged": true
|
||||
},
|
||||
"independentCleanupAudit": "MPRIS_CLEANUP_AUDIT_PASS: shared lock, test services/runtime directories and owned cache link absent",
|
||||
"pinnedHttpSshFlow": {
|
||||
"command": "scripts/e2e.sh test_framey",
|
||||
"passed": 2,
|
||||
"seconds": 32.59,
|
||||
"scope": "pinned official archive receipt assertions and sample install/update/remove safeguards"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
# MusicControl / Music Control runtime checks, 26 October 2026
|
||||
|
||||
Both official entries remain **partial**. Their pure-Python backends need a
|
||||
user-session MPRIS player and `dbus-send`, rather than Deck-specific hardware or
|
||||
an x86 binary. [Results](framey-mpris-runtime-2026-10-26.json) pin both archives
|
||||
and the actual Decky 3.3.0 executables used.
|
||||
|
||||
Two runtime failures were reproduced and repaired:
|
||||
|
||||
- The #335 `music-control-bound-methods` transformation was incorrect. Neither
|
||||
manifest declares `api_version`, so Decky uses `module.Plugin` and explicitly
|
||||
supplies the class as `self`. The earlier test used `module.Plugin()` and
|
||||
therefore diagnosed a fixture error as an upstream bug. In actual Decky,
|
||||
the transformation breaks discovery with `_get_dbus_env() missing ... self`
|
||||
and transport with `_sp_player_dbus() missing ... parameters`. Managed installs
|
||||
now retain the original class calls. This supersedes the earlier
|
||||
[bound-method evidence](framey-music-control-bound-methods-2026-10-26.md).
|
||||
- On Frame OS 0.5.6, MusicControl 1.1.6's `/bin/sh` subprocess inherits Decky's
|
||||
PyInstaller library path. Its bundled `libtinfo.so.6` needs `GLIBC_2.42`, which
|
||||
the Frame's libc lacks. A private diagnostic backend confirmed the same
|
||||
read-only D-Bus command fails with a shell and succeeds without one. Both
|
||||
exact-archive backends now restore `LD_LIBRARY_PATH_ORIG` (or an empty path)
|
||||
only in their copied D-Bus subprocess environment. The Decky process keeps
|
||||
its original environment. The receipt records `music-control-system-dbus-env`.
|
||||
|
||||
The patch is gated by store ID, SHA-256 and folder, and checks the expected
|
||||
source before editing. Existing installed copies need **remove/reinstall** or
|
||||
an explicit managed update; a desktop update alone does not rewrite them.
|
||||
The store still shows partial, with the same reason as the docs table.
|
||||
|
||||
Actual Decky 3.3.0 amd64 ran each repaired production-managed install separately
|
||||
in a container with no network, host audio, host D-Bus, home or device access.
|
||||
A private `dbus-daemon` hosted a silent synthetic MPRIS service. Discovery,
|
||||
selection, identity, metadata, status, progress, volume/CanSeek reads,
|
||||
play/pause, next/previous, seek, track position and volume writes passed through
|
||||
the real legacy websocket RPC envelope. The service's own state independently
|
||||
confirmed each write. The shared plugin folder prevents co-installation; each
|
||||
entry was tested in a fresh container.
|
||||
|
||||
Local headless Chromium loaded the shipped `headset.html` and exact official
|
||||
frontends inside their ordinary opaque `allow-scripts` iframe. The relay used
|
||||
actual Decky backends and allowed only the private fixture. Track/artist
|
||||
rendering, previous/next, play/pause, seek to 90 seconds, volume to 60%, Info
|
||||
navigation and close/reopen passed for both entries. Chromium retained its
|
||||
normal process isolation and sandboxed-origin restrictions. No external media
|
||||
or artwork request was permitted.
|
||||
|
||||
Under the shared `/tmp/frame-test.lock`, a removable private Decky 3.3.0 ARM64
|
||||
user service loaded each corrected managed install on the real Frame.
|
||||
Read-only `sp_list_media_players` returned an empty string successfully for
|
||||
both, and `sp_get_media_player` returned their default Spotify bus name.
|
||||
These final checks used Decky's direct loopback websocket through SSH; they
|
||||
do not claim the deployed Framey HTTP/CEF route. Earlier CEF attempts encountered
|
||||
transient reinjection failures. No player controls ran on the real Frame.
|
||||
The private units, runtimes and owned Steam image-cache link were removed;
|
||||
independent follow-up reads confirmed cleanup and lock release. Battery was
|
||||
100 → 100. Existing managed Decky remained failed and the headset panel inactive,
|
||||
as found. Power settings were not changed.
|
||||
|
||||
Remaining gaps: actual media-app playback/volume on ARM64, local-file album-art
|
||||
routing, physical controls and worn VR. Both backends truncate multiword player
|
||||
names to their last word. Music Control 1.1.7 automatically probes volume by
|
||||
writing 0.01 and restoring it; this was exercised only on the silent fixture.
|
||||
|
||||
Validation: full Python suite **1,639 tests, 16 skipped**; focused managed-store
|
||||
suite **23/23**; shared frontend action checks; all **116** store digests unchanged.
|
||||
The complete fake-Frame audit passed: **61 installed/removed, 55 rejected**
|
||||
for documented safeguards; official backends are stubbed in that harness. The
|
||||
regression test uses Decky's actual legacy class convention for both entries,
|
||||
checks original/absent library-path restoration without changing the parent
|
||||
environment, and rejects transferring the patch to a different archive digest.
|
||||
|
||||
The pinned HTTP/SSH flow also passed `scripts/e2e.sh test_framey`: **2/2**
|
||||
tests in 32.590 seconds. Its receipt assertion covers the replacement shim for
|
||||
both Music Control entries; the old assertion caused the first hosted e2e
|
||||
failure and left a plugin installed, cascading into the following test.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Superseded: Music Control bound-method fixture
|
||||
|
||||
The #335 result was incorrect. Its test instantiated `module.Plugin()`, while
|
||||
actual Decky 3.3.0 uses the class `module.Plugin` for this legacy manifest and
|
||||
supplies the class as `self`. The published backend's explicit-self calls are
|
||||
correct in that runtime. Removing those arguments breaks actual Decky discovery
|
||||
and transport.
|
||||
|
||||
The follow-up [real runtime checks](framey-mpris-runtime-2026-10-26.md) reproduce
|
||||
the regression, remove the incorrect transformation and replace the test with
|
||||
the actual legacy calling convention. They also identify and repair a separate
|
||||
Frame OS system-command library-path failure. Existing copies installed with
|
||||
`music-control-bound-methods` must be removed/reinstalled or explicitly updated.
|
||||
Both official Music Control entries remain partial; the follow-up describes
|
||||
exactly what passed and what remains unverified.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Native plugin packaging blockers, 26 October 2026
|
||||
|
||||
This is a static, digest-scoped follow-up. Neither plugin was installed or
|
||||
run on the Frame. Their compatibility statuses remain **broken**.
|
||||
|
||||
## Decky Cloud Save 1.4.2, store ID 49
|
||||
|
||||
The official ZIP SHA-256 is
|
||||
`b311d967ddf1b0f0046d36929645855891a40173e3a93f6f0cd8b1576b04b411`.
|
||||
It was downloaded and hash-checked without executing any member. Of its 27
|
||||
members, `decky-cloud-save/bin/rclone` is a regular executable of **60,776,600
|
||||
bytes** with ELF machine **62 (x86_64)**. The installer permits at most 30 MiB
|
||||
per member, so this exact ZIP fails before native architecture validation.
|
||||
Raising that limit alone would still leave an x86_64 executable on the ARM64
|
||||
Frame. No cloud credentials, saves, reads or writes were exercised.
|
||||
|
||||
## Emuchievements 2.0.5, store ID 28
|
||||
|
||||
The official ZIP SHA-256 is
|
||||
`98dcb443e81b9f9011cfe77c69eb659869c8e2df2df8b0a443df66cbfc2f5a89`.
|
||||
The [earlier helper check](framey-emuchievements-arm-2026-10-26.md) proved
|
||||
one synthetic ROM hash on ARM64, without installing the plugin. The upstream
|
||||
source commit `8567e94befffc943434b2adb842487274815f8bb` and its pinned
|
||||
libarchive, libchdr and rcheevos submodules were checked out separately for
|
||||
this audit. The source is GPL-3.0. An isolated Debian ARM64 cross-build made a
|
||||
2.6 MB helper (`c8794558a0d75294930f969a426e7d2ac13a27e3c1ff9bd8aae093e31596b9bd`),
|
||||
but it was **not shipped or run on ROM archives**. A complete source bundle
|
||||
without Git metadata compressed to about 8 MB; source and license distribution
|
||||
would be part of any release-ready port.
|
||||
|
||||
The proposed source has an additional blocker. `backend/src/util.cpp` builds
|
||||
an extraction destination by appending each raw archive member pathname to
|
||||
an owned `/tmp/tmpdir.*` prefix, then passes it to libarchive's disk writer.
|
||||
It neither rejects `..`/absolute paths nor enables libarchive's secure path
|
||||
flags. `backend/src/main.cpp` later removes the extracted file's parent path.
|
||||
With an untrusted archive, those paths may leave the intended temporary
|
||||
directory. The plugin's Python backend also invokes `bin/hash` through a
|
||||
shell with a path argument. A port needs bounded path validation, secure
|
||||
extraction/cleanup, safe subprocess arguments, format coverage and actual
|
||||
Decky/backend tests before its official ZIP can be transformed or installed.
|
||||
|
||||
The official archive still contains an x86_64 helper. RetroAchievements
|
||||
login, real ROM scanning, a packaged ARM64 backend, wearer-visible VR and
|
||||
physical controller use remain unverified.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Shotty Python compatibility, 26 October 2026
|
||||
|
||||
The exact official Shotty 0.1.0 archive (store ID 53, SHA-256 `ab7f6618b264a0cb766525e788cef6c3e1478c0fef98a701225e5e8af441e2fe`) imports `click.get_app_dir` at startup but never calls it. The archive includes an empty `py_modules` directory and no `click` package. A prior actual Decky 3.3.0 amd64 backend check stopped at `ModuleNotFoundError: click`; its local frontend separately starts with Framey's read-only Steam library snapshot.
|
||||
|
||||
Frame Control now writes a small Linux `click.get_app_dir` compatibility module into **only this exact digest's** managed `Shotty/py_modules` folder during staging. Decky Loader adds each plugin's `py_modules` folder to that plugin's Python search path. The staging transaction records `shotty-click-app-dir` in the receipt and removes the whole staged folder on failure. A different Shotty digest receives no module. The official ZIP bytes and their verified hash remain unchanged.
|
||||
|
||||
An isolated transaction test installed the pinned archive, imported its unchanged `main.py` using the staged module, and called its `aggregate_all` method with a synthetic Steam screenshot under a temporary `HOME`. It copied one fixture image into that temporary home's `Pictures/Screenshots/Local Test Game` folder; reopening the copied bytes matched the source. Removal deleted the managed folder. A second synthetic Shotty archive with another digest installed without the module. The test neither started the plugin's continuous screenshot rescuer nor touched a person's Steam library or screenshots.
|
||||
|
||||
This supports **partial** compatibility for the local frontend and a narrow backend copy path. Actual Decky 3.3.0 ARM64 plugin startup, gamescope screenshot rescue, real screenshot aggregation, VR panel visibility, and physical controller use remain unverified. No headset lock was acquired or headset state changed for this check.
|
||||
@@ -0,0 +1,46 @@
|
||||
# v0.4.47 candidate gate, 26 October 2026
|
||||
|
||||
This candidate contains Game Theme Music's locally rendered Settings view
|
||||
(#330) and a digest-scoped Python import module for the official Shotty 0.1.0
|
||||
archive (#331). The 116-plugin table is **4 works / 52 partial / 49 broken /
|
||||
11 N/A**. Game Theme Music is partial for its local Settings view; its
|
||||
backend, network and playback were denied in that audit. Shotty is partial
|
||||
for its local frontend and synthetic screenshot copy in isolated Python;
|
||||
actual ARM64 Decky startup and live screenshot rescue are unverified.
|
||||
|
||||
## Locked Frame candidate smoke
|
||||
|
||||
At 14:18 UTC, source commit `1f5b37a7552880b40868617ef22398fcd39b3f5d`
|
||||
ran `scripts/frame-smoke.sh` on the real Frame. The script acquired its unique
|
||||
owner in `/tmp/frame-test.lock` before reading status. Locked preflight found
|
||||
SteamVR activity **3** (standby), proximity raw **3.0625**, and battery
|
||||
**100%** on Frame OS 0.5.6 build `20261022.6153909`. Owned ARM64 and
|
||||
Windows/Proton test titles installed, launched, and were removed. The
|
||||
optional x86_64 title installed and was removed; launch was **skipped**
|
||||
because Steam Linux Runtime 4 was absent. The result was **12 passed /
|
||||
1 skipped / 0 failed**. Cleanup passed and the command exited 0, including
|
||||
release of its owned lock. Detailed results stay private in
|
||||
`tests/smoke/results/2026-10-26T141812Z-20261022.6153909.json`.
|
||||
|
||||
No wearer was confirmed. The interactive worn-check script was not run;
|
||||
the smoke did not wake the headset or test the Framey panel. Game Theme
|
||||
Music playback, Shotty live rescue, wearer-visible official-plugin UI,
|
||||
physical controllers, keyboard and optical comfort remain unverified.
|
||||
The hourly shared-lock worn check remains enabled.
|
||||
|
||||
## Publication boundary
|
||||
|
||||
At 14:23 UTC, the exact version-bumped candidate commit
|
||||
`e07fa155928d1609304e317d15e0c4ef4a977579` ran the same token-locked
|
||||
`scripts/frame-smoke.sh`.
|
||||
Preflight again found activity **3** (standby), proximity raw **3.0625** and
|
||||
battery **100%**. The result was **12 passed / 1 optional x86 launch skipped /
|
||||
0 failed**. Owned cleanup passed and the command exited 0 after releasing its
|
||||
lock. Its private result is
|
||||
`tests/smoke/results/2026-10-26T142321Z-20261022.6153909.json`.
|
||||
This paragraph is the only change after that smoke.
|
||||
|
||||
PR #331's combined-head CI, green release-PR and post-merge CI, tagged
|
||||
installer builds, all ten installer digests with `SHA256SUMS`, independent
|
||||
package checks, publisher dry run, and public `update.json` readback remain
|
||||
required.
|
||||
@@ -0,0 +1,62 @@
|
||||
# v0.4.47 tagged release, 26 October 2026
|
||||
|
||||
The release PR [#332](https://github.com/saphid/frame-control/pull/332)
|
||||
passed its exact-head checks and all four installer preview jobs, including
|
||||
Intel Mac. It merged at main commit
|
||||
`035c59ee1134aa158c3cd818e8dd35f822cbfc4b`. The seven-job post-merge
|
||||
main [checks run](https://github.com/saphid/frame-control/actions/runs/40342375912)
|
||||
passed, including iOS and fake-Frame e2e. Annotated tag `v0.4.47`
|
||||
dereferences to that main commit. The tagged
|
||||
[release workflow](https://github.com/saphid/frame-control/actions/runs/40344549980)
|
||||
passed its draft, Linux, Windows, Apple Silicon Mac, Intel Mac, and checksum
|
||||
jobs.
|
||||
|
||||
## Frame and package gates
|
||||
|
||||
The [candidate evidence](v0447-candidate-2026-10-26.md) records a locked
|
||||
exact-version real Frame smoke: **12 passed / one optional x86 launch skipped /
|
||||
zero failed**, with owned cleanup and lock release. The x86 launch needs Steam
|
||||
Linux Runtime 4, which was absent. The headset was in standby and no wearer was
|
||||
confirmed, so the interactive worn-check script was not run. No panel or
|
||||
physical-controller claim follows from the smoke.
|
||||
|
||||
The draft contained exactly ten installers and `SHA256SUMS`, each with a
|
||||
GitHub SHA-256 digest. An independently downloaded `SHA256SUMS` matched a
|
||||
fresh ten-line list regenerated from GitHub asset metadata byte for byte;
|
||||
its SHA-256 was
|
||||
`ab1a95e5d51333dfc962723a21b044da935e07c24cedbd430222dc482d725508`.
|
||||
`scripts/publish-release.sh --dry-run v0.4.47` passed the tag, installer,
|
||||
checksum, and update-manifest gates after release notes were attached.
|
||||
|
||||
Independent downloads passed these checks:
|
||||
|
||||
| Package | Result |
|
||||
| --- | --- |
|
||||
| Linux x86_64 AppImage | SHA-256 `e7c83cf9177d11de724caf40a829b5c97ac3d2f0a04df24bcb1e4107310f1f49`; launched under isolated Xvfb on lxso1, bundled server returned HTTP 200 with `FrameControl/1`, then stopped. |
|
||||
| Mac arm64 ZIP | SHA-256 `10092131b7fd43950e0dc436a51da1e41b42803d532a89363e2b71880b92b212`; ZIP CRC valid, bundle version 0.4.47, main Mach-O CPU arm64. |
|
||||
| Mac x64 ZIP | SHA-256 `24531b088e85d2e7e9174289b0b9532c78fb85e7a29775c24369c00bc61e5408`; ZIP CRC valid, bundle version 0.4.47, main Mach-O CPU x86_64. |
|
||||
| Windows x64 Setup | SHA-256 `a697a7e9042eb0d9545d99639e39ff53fbf993491c0728f59f038f21175b77cc`; PE/Nullsoft installer format. Tagged Windows CI checked packaged files and Python libraries. |
|
||||
| Linux arm64 DEB | SHA-256 `e0be95ffc714e71487889a7da4886676b75c993f86bb674d180dc3ea4cd4b3b6`; package version 0.4.47 and architecture arm64. |
|
||||
|
||||
The tagged Mac and Windows packages were not launched on physical machines
|
||||
in this release gate. Earlier physical Intel Mac acceptance covers older
|
||||
bytes.
|
||||
|
||||
## Publication and limits
|
||||
|
||||
`scripts/publish-release.sh v0.4.47` published
|
||||
[v0.4.47](https://github.com/saphid/frame-control/releases/tag/v0.4.47) at
|
||||
15:07:28 UTC. GitHub's latest-release API returned that tag with
|
||||
`draft=false`, `prerelease=false`, ten installers, `SHA256SUMS`, and
|
||||
`update.json`. A fresh public latest-download of `update.json` reported
|
||||
version 0.4.47, the stable release page, ten hashed installers, and the
|
||||
worn-VR limit in its notes.
|
||||
|
||||
The exact-version official catalog is **4 works / 52 partial / 49 broken /
|
||||
11 N/A** out of 116. Game Theme Music has a local Settings view; its music
|
||||
playback and live backend are unverified. Shotty's backend imported and
|
||||
copied a synthetic screenshot in isolated Python; live ARM64 Decky loading
|
||||
and screenshot rescue are unverified. The real Frame was in standby;
|
||||
official-plugin wearer visibility, physical controller usability, VR
|
||||
keyboard, and optical comfort remain unverified. The hourly shared-lock
|
||||
worn check remains enabled.
|
||||
@@ -0,0 +1,50 @@
|
||||
# v0.4.48 candidate gate, 26 October 2026
|
||||
|
||||
This candidate contains the exact-archive Music Control 1.1.7 Python call
|
||||
repair from [#335](https://github.com/saphid/frame-control/pull/335). All
|
||||
**11** hosted checks passed on its head
|
||||
`ff465accc508e40158c590eebffd4aec924503ac`, including fake-Frame e2e,
|
||||
Windows, macOS, Linux, ARM64 host libraries and iOS. The catalog remains
|
||||
**4 works / 52 partial / 49 broken / 11 N/A** out of 116. Music Control is
|
||||
partial: an isolated D-Bus command fixture proves dispatch after the repair;
|
||||
a live player and actual Decky ARM64 backend were not tested.
|
||||
|
||||
## Locked real Frame smoke
|
||||
|
||||
At 15:36 UTC, source head `ff465accc508e40158c590eebffd4aec924503ac`
|
||||
ran `scripts/frame-smoke.sh` on SteamOS 0.5.6 build `20261022.6153909`.
|
||||
The runner acquired its own unique `/tmp/frame-test.lock` owner before
|
||||
reading device status. Preflight found SteamVR activity **3** (standby),
|
||||
proximity raw **3.0625**, and battery **99%**. Owned ARM64 and Windows/Proton
|
||||
titles installed, launched and were removed. The optional x86_64 title
|
||||
installed and was removed; its launch was **skipped** because Steam Linux
|
||||
Runtime 4 was absent. The result was **12 passed / 1 skipped / 0 failed**.
|
||||
Owned cleanup passed and the command exited 0, including lock release.
|
||||
A separate fresh locked read found battery **100%**, activity **3** and no
|
||||
`fc_smoke_*` title directories; it released only its own lock. The private
|
||||
runner result is `2026-10-26T153655Z-20261022.6153909.json`.
|
||||
|
||||
At 15:42 UTC, exact version-bumped commit
|
||||
`a31b4a426cf4204e45fe218e204a26328aa0b1c9` ran the same token-locked
|
||||
smoke. Preflight found activity **3**, proximity raw **3.0625** and battery
|
||||
**100%**. It passed **12 / 1 optional x86 launch skipped / 0 failed**.
|
||||
Cleanup passed and the command exited 0 after releasing its lock. The
|
||||
private runner result is `2026-10-26T154258Z-20261022.6153909.json`.
|
||||
A fresh separately locked read found battery **100%**, activity **3** and
|
||||
no test-title directories, then released that lock. Merging current main
|
||||
into the candidate created commit
|
||||
`a8bf01d660cd9abb8d2a724626a06a0e7e6c2a6a`; its tree before this
|
||||
evidence document was byte-identical to the tested version-bumped commit
|
||||
(`d45c2465ddfcf877fe2c8bc30420d4719da5035e`).
|
||||
|
||||
No wearer was confirmed. Neither smoke woke the headset or ran the
|
||||
interactive worn-check script. Official-plugin wearer visibility, physical
|
||||
controllers, VR keyboard and optical comfort remain unverified. The hourly
|
||||
shared-lock worn check remains enabled.
|
||||
|
||||
## Remaining publication gates
|
||||
|
||||
The release PR's exact-head CI and installer previews, post-merge main CI,
|
||||
tagged builds with all ten installers and `SHA256SUMS`, independent package
|
||||
checks, publisher dry run and public `update.json` readback are required
|
||||
before publication.
|
||||
@@ -4,7 +4,7 @@ Snapshot: 2026-10-26T06:10:58.537029+00:00; 116 current visible official 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: 4 · Partial: 50 · Broken: 51 · N/A: 11.**
|
||||
**Works: 4 · Partial: 52 · Broken: 49 · N/A: 11.**
|
||||
|
||||
- **Works** requires verified useful plugin behavior with its real backend. Read the reason for the verified scope and remaining limits; it does not imply worn-VR verification.
|
||||
- **Partial** means only a subset or a local fixture scope was verified, often just frontend/settings initialization. The remaining backend/device functionality is unverified; read the reason for the exact scope.
|
||||
@@ -29,7 +29,7 @@ A [Free Loader duplicate-entry follow-up](evidence/framey-free-loader-duplicates
|
||||
|
||||
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. A [2026-10-26 follow-up](evidence/framey-events-backend-2026-10-26.json) verified default Notebook drawing/save/reopen across a real backend restart and Simple Timer start/cancel, countdown/completion events, a visible toast and persisted recents/subtle mode. Only private test settings were written. Simple Timer remains partial because its own expired-timer startup raises a missing `timer_task` error; sound/suspend are unsupported. A [local audio follow-up](evidence/framey-audio-backends-2026-10-26.md) exercised Volume Mixer, Volume Boost and Mixer through real Decky 3.3.0 amd64 backends on private silent virtual audio targets. Sliders, numeric volume entry, independent server readbacks and Mixer filter persistence across a runtime restart passed. An [ARM64 audio follow-up](evidence/framey-audio-arm64-2026-10-26.md) verified the unmodified official backends in actual Decky 3.3.0 on the Frame user audio session. Volume Mixer and Mixer now qualify as works for per-app volume: Chromium controls changed only a named silent stream, with independent Pulse readbacks. That run left Volume Boost partial because sink writes were denied. A [Volume Boost follow-up](evidence/framey-volume-boost-arm64-2026-10-26.md) now verifies linked/separate channel controls, re-link and reopen through the actual ARM64 backend. Independent Pulse reads confirmed 125%/150% gain writes while the default sink stayed muted; exact original volume/mute was restored. Volume Boost qualifies as works for sink controls. Audible output, Bluetooth playback and worn VR remain unverified. A [running-context follow-up](evidence/framey-running-context-2026-10-26.md) supplies detached running-app/main-app reads and relays actual Steam lifetime callbacks while a panel is open. Real Frame checks verified an empty list, registration/cleanup and the exact Discord Status ARM64 backend reporting no IPC connection. Nonempty games and transitions were synthetic fixtures; real game events, Discord activity and worn VR remain unverified. Discord Status moves to partial; no new works claim is made. A [library context follow-up](evidence/framey-library-context-2026-10-26.md) read 1,106 app overviews, 895 game IDs and the current public Steam ID from real Frame CEF under the shared lock. Local Chromium replayed this detached snapshot to render Deck Settings game selection, Steamback's empty save panel and PlayTime statistics/settings. Steamback and PlayTime used actual Decky 3.3.0 amd64 backends in an isolated container; their ARM64 backends and automatic game hooks remain unverified. Shotty's frontend now starts, but its published backend fails on missing `click`; it stays broken. A [chunked frontend follow-up](evidence/framey-deck-shelves-browser-2026-10-26.md) loaded Deck Shelves 2.0.0 through a managed JavaScript module route and rendered its settings in an opaque local Chromium panel. Its backend was denied; Steam Home shelves and worn VR remain unverified. A [local module lookup follow-up](evidence/framey-module-lookup-2026-10-26.md) supplies a finite registry of Framey-local controls and CSS. TabMaster QAM/docs now render; Steam library-tab editing remains unavailable. That pass exposed Junk-Store executable-script extraction failures; the follow-up below repairs them. SteamGridDB still requires Steam context-menu internals and DeckSP still fails its published MobX fallback. That pass tested TabMaster/Junk-Store in actual isolated amd64 Decky, without ARM64 or worn-VR coverage. A [script entrypoint follow-up](evidence/framey-script-entrypoints-2026-10-26.md) grants owner execute to shebang scripts and ZIP-declared executables without copying archive privilege/write bits. Junk-Store now passes static menu/backend calls in actual Decky 3.3.0 ARM64 and local Chromium Games/Epic navigation, back and reopen. It moves to partial: About still crashes on missing installed-app metadata, Legendary is absent, and login/game operations remain unverified. Existing installations require update/reinstall to repair permissions. A [read-only installed-metadata follow-up](evidence/framey-installed-context-2026-10-26.md) now repairs the About crash. The library snapshot copies only Steam-supplied installed booleans into detached metadata, preserving unknown values as undefined. Junk-Store About/Dependencies, Back and reopen render in local Chromium with actual Frame library data and the official ARM64 backend. It remains partial: dependency/login/game operations, RSS, the optional Line truncated
|
||||
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. A [2026-10-26 follow-up](evidence/framey-events-backend-2026-10-26.json) verified default Notebook drawing/save/reopen across a real backend restart and Simple Timer start/cancel, countdown/completion events, a visible toast and persisted recents/subtle mode. Only private test settings were written. Simple Timer remains partial because its own expired-timer startup raises a missing `timer_task` error; sound/suspend are unsupported. A [local audio follow-up](evidence/framey-audio-backends-2026-10-26.md) exercised Volume Mixer, Volume Boost and Mixer through real Decky 3.3.0 amd64 backends on private silent virtual audio targets. Sliders, numeric volume entry, independent server readbacks and Mixer filter persistence across a runtime restart passed. An [ARM64 audio follow-up](evidence/framey-audio-arm64-2026-10-26.md) verified the unmodified official backends in actual Decky 3.3.0 on the Frame user audio session. Volume Mixer and Mixer now qualify as works for per-app volume: Chromium controls changed only a named silent stream, with independent Pulse readbacks. That run left Volume Boost partial because sink writes were denied. A [Volume Boost follow-up](evidence/framey-volume-boost-arm64-2026-10-26.md) now verifies linked/separate channel controls, re-link and reopen through the actual ARM64 backend. Independent Pulse reads confirmed 125%/150% gain writes while the default sink stayed muted; exact original volume/mute was restored. Volume Boost qualifies as works for sink controls. Audible output, Bluetooth playback and worn VR remain unverified. A [running-context follow-up](evidence/framey-running-context-2026-10-26.md) supplies detached running-app/main-app reads and relays actual Steam lifetime callbacks while a panel is open. Real Frame checks verified an empty list, registration/cleanup and the exact Discord Status ARM64 backend reporting no IPC connection. Nonempty games and transitions were synthetic fixtures; real game events, Discord activity and worn VR remain unverified. Discord Status moves to partial; no new works claim is made. A [library context follow-up](evidence/framey-library-context-2026-10-26.md) read 1,106 app overviews, 895 game IDs and the current public Steam ID from real Frame CEF under the shared lock. Local Chromium replayed this detached snapshot to render Deck Settings game selection, Steamback's empty save panel and PlayTime statistics/settings. Steamback and PlayTime used actual Decky 3.3.0 amd64 backends in an isolated container; their ARM64 backends and automatic game hooks remain unverified. Shotty's frontend now starts, but its published backend fails on missing `click`; it stays broken. A [chunked frontend follow-up](evidence/framey-deck-shelves-browser-2026-10-26.md) loaded Deck Shelves 2.0.0 through a managed JavaScript module route and rendered its settings in an opaque local Chromium panel. Its backend was denied; Steam Home shelves and worn VR remain unverified. A [local module lookup follow-up](evidence/framey-module-lookup-2026-10-26.md) supplies a finite registry of Framey-local controls and CSS. TabMaster QAM/docs now render; Steam library-tab editing remains unavailable. That pass exposed Junk-Store executable-script extraction failures; the follow-up below repairs them. SteamGridDB still requires Steam context-menu internals and DeckSP still fails its published MobX fallback. That pass tested TabMaster/Junk-Store in actual isolated amd64 Decky, without ARM64 or worn-VR coverage. A [script entrypoint follow-up](evidence/framey-script-entrypoints-2026-10-26.md) grants owner execute to shebang scripts and ZIP-declared executables without copying archive privilege/write bits. Junk-Store now passes static menu/backend calls in actual Decky 3.3.0 ARM64 and local Chromium Games/Epic navigation, back and reopen. It moves to partial: About still crashes on missing installed-app metadata, Legendary is absent, and login/game operations remain unverified. Existing installations require update/reinstall to repair permissions. A [read-only installed-metadata follow-up](evidence/framey-installed-context-2026-10-26.md) now repairs the About crash. The library snapshot copies only Steam-supplied installed booleans into detached metadata, preserving unknown values as undefined. Junk-Store About/Dependencies, Back and reopen render in local Chromium with actual Frame library data and the official ARM64 backend. It remains partial: dependency/login/game operations, RSS, the optional Line truncated
|
||||
|
||||
## Every current plugin
|
||||
|
||||
@@ -41,7 +41,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 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 |
|
||||
| 12: MusicControl 1.1.6 | legacy / legacy | partial | Managed reinstall preserves Decky’s legacy calling convention and repairs the Frame OS shell library-path failure. Chromium transport/seek/volume against a silent MPRIS fixture and real ARM64 empty-player discovery pass; actual Frame media playback, album art and worn VR remain unverified. Reinstall existing copies to apply. | static archive, local browser, fake-Frame, isolated Decky 3.3.0 + MPRIS fixture, real Frame SSH |
|
||||
| 13: Pause Games 1.0.3 | 1 / 2 | partial | Settings and running-game list initialize in local Chromium with an explicit synthetic game; live Frame empty-list reads and lifetime registration/cleanup passed. Process pause/resume, automatic pausing and suspend integration remain unverified/unsupported; backend writes were denied. | 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. Steam UI sound lookup is present as explicitly unsupported; the frontend still requires Steam m_GamepadUIAudioStore.m_AudioPlaybackManager, which is unavailable in the isolated panel. | static archive, local browser, fake-Frame |
|
||||
@@ -65,10 +65,10 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 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 | partial | Panel opens using the real Frame current-user/library snapshot. Account setup and empty save-list reads pass through actual Decky 3.3.0 amd64 in isolation. ARM64 backend, supported-save discovery, automatic backups and restore remain unverified; no backup/restore ran. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 48: Quick Launch 1.2.1 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 49: Decky Cloud Save 1.4.2 | legacy / legacy | broken | Unsafe plugin archive member. | static archive, local browser, fake-Frame |
|
||||
| 49: Decky Cloud Save 1.4.2 | legacy / legacy | broken | The exact official ZIP contains a 60,776,600-byte x86_64 rclone member, above the 30 MiB member limit and unusable on ARM64. Installation remains blocked. | static archive, local browser, fake-Frame |
|
||||
| 50: Storage Cleaner 1.4.0 | 1 / 2 | partial | Empty-data panel rendered; cleanup was deliberately not run because it can remove local data. | static archive, local browser, fake-Frame |
|
||||
| 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 | Library snapshot fixes the frontend allApps crash. Actual published Decky 3.3.0 amd64 backend fails importing click.get_app_dir (ModuleNotFoundError); the archive does not bundle click. Screenshot aggregation/rescue was not run. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 52: Game Theme Music 1.7.1-1 | 1 / 2 | partial | An inert local component lets the exact frontend initialize and render its Settings view in opaque Chromium. Backend get_setting and plugin network were denied in the audit; Steam game-page theme injection has no verified target in Framey. Music search, playback, downloads, settings persistence and worn VR remain unverified. | static archive, local browser, fake-Frame |
|
||||
| 53: Shotty 0.1.0 | legacy / legacy | partial | The frontend starts with the read-only Frame library snapshot. Frame Control adds a digest-scoped click.get_app_dir compatibility module to the managed install; the exact backend then imports and copies a synthetic Steam screenshot into a temporary Pictures folder in isolated Python. Actual Decky 3.3.0 ARM64 loading, live screenshots/rescue, in-VR panel and physical controller use remain unverified. | static archive, local browser, fake-Frame, real Frame SSH, isolated Python backend |
|
||||
| 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 | works | Default-sink controls pass in local Chromium through real Decky 3.3.0 ARM64: linked slider/keyboard and numeric writes, separate channels, re-link and reopen, with independent Pulse readback including 125%/150% while muted. Exact original volume/mute restored. Audible output, Bluetooth sinks and worn VR unverified. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
|
||||
| 59: Web Browser 1.4.3 | legacy / legacy | broken | The exact frontend now initializes with a null Steam React-root lookup. The browser itself still requires Steam GamepadUI CDP injection and a backend WebSocket unavailable in Framey; no working browser tab was verified. | static archive, local browser, fake-Frame, focused local browser |
|
||||
@@ -119,7 +119,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 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 |
|
||||
| 111: Music Control 1.1.7 | legacy / legacy | partial | Managed reinstall preserves Decky’s legacy calling convention and repairs the Frame OS shell library-path failure. Chromium transport/seek/volume against a silent MPRIS fixture and real ARM64 empty-player discovery pass; actual Frame media playback, album art and worn VR remain unverified. Reinstall existing copies to apply. | static archive, local browser, fake-Frame, isolated Python backend, isolated Decky 3.3.0 + MPRIS fixture, real Frame SSH |
|
||||
| 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 |
|
||||
@@ -142,7 +142,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 132: SuggestMe 1.5.3 | 1 / 2 | partial | Initializer succeeds; recommendation backend and Steam library data unverified. | static archive, local browser, fake-Frame |
|
||||
| 133: Non-Steam Badges 0.1.0 | 1 / 2 | partial | Initializer succeeds; Non-Steam game badges have no supported Steam page injection target. | static archive, local browser, fake-Frame |
|
||||
| 134: Deck Progress Tracker 1.3.6 | 1 / 2 | partial | Initializer succeeds; Steam game/library statistics and page patches unverified. | static archive, local browser, fake-Frame |
|
||||
| 135: Deck Shelves 2.0.0 | 1 / 1 | partial | The exact chunked frontend now initializes and renders Deck Shelves settings in an opaque local Chromium panel. Backend calls were denied; Home-screen shelf injection has no verified Steam UI target in Framey. Settings writes, real backend behavior and worn VR are unverified. | static archive, local browser, fake-Frame |
|
||||
| 135: Deck Shelves 2.0.0 | 1 / 1 | partial | The exact chunked frontend renders settings in opaque local Chromium. The official Python backend saved a shelf and returned it after reopen with a stub Decky module and temporary settings directory. Actual Decky RPC, real Frame settings, Steam Home shelf injection and worn VR remain unverified. | static archive, local browser, fake-Frame, isolated Python backend |
|
||||
| 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 |
|
||||
@@ -164,7 +164,7 @@ DFL includes exports imported through decky-frontend-lib / @decky/ui. Steam stor
|
||||
| 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. |
|
||||
| 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 | User-session MPRIS player and dbus-send required; no Deck-specific hardware. |
|
||||
| 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. |
|
||||
@@ -242,7 +242,7 @@ DFL includes exports imported through decky-frontend-lib / @decky/ui. Steam stor
|
||||
| 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. |
|
||||
| Music Control | none detected | ButtonItem, DialogButton, Focusable, PanelSection, PanelSectionRow, Router, SidebarNavigation, SliderField, SteamSpinner, definePlugin, staticClasses | Router, routerHook, window.SP_REACT, local/Steam routes | Python | User-session MPRIS player and dbus-send required; no Deck-specific hardware. |
|
||||
| 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. |
|
||||
@@ -288,11 +288,11 @@ All published Linux native payloads in this snapshot are x86_64; no published aa
|
||||
| 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. |
|
||||
| Emuchievements | bin/hash | A source-matched ARM64 helper matched one synthetic ROM on the real Frame (docs/evidence/framey-emuchievements-arm-2026-10-26.md). The pinned upstream source proposed for packaging also has unsafe archive extraction and cleanup paths (docs/evidence/framey-native-port-blockers-2026-10-26.md); fix and test those before shipping an ARM64 binary. The official x86_64 ZIP remains blocked; format coverage, plugin backend/UI, login and worn VR are unverified. |
|
||||
| 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. |
|
||||
| Decky Cloud Save | bin/rclone | An ARM64 rclone artifact and safe package policy are needed; the official 60,776,600-byte x86_64 member fails archive validation. No cloud writes were run (docs/evidence/framey-native-port-blockers-2026-10-26.md). |
|
||||
| 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. |
|
||||
|
||||
@@ -14,6 +14,12 @@ action calls the backend, so it reads the persisted setting rather than a
|
||||
browser cache. A missing, damaged, or old non-boolean value reads as the
|
||||
caller's default;
|
||||
the next save atomically replaces the sample's settings file.
|
||||
The current file is `{"schema": 1, "enabled": true|false}`. An unversioned
|
||||
`{"enabled": true|false}` file from the first sample release still reads, and
|
||||
the next save writes schema 1. A file with another explicit schema returns a
|
||||
visible read or save error and remains untouched, so downgrading the sample
|
||||
cannot erase settings written by a newer version. Add a deliberate migration
|
||||
for each future schema instead of changing this guard silently.
|
||||
The panel also guards its own read and write calls synchronously. If two
|
||||
controller events arrive before React redraws a disabled control, the second
|
||||
event is ignored; after the request settles, a new change can be saved.
|
||||
|
||||
@@ -5,26 +5,41 @@ from pathlib import Path
|
||||
import tempfile
|
||||
import decky
|
||||
|
||||
SETTINGS_SCHEMA = 1
|
||||
|
||||
|
||||
def settings_file():
|
||||
# Decky provides this per-plugin path through its official Python module.
|
||||
return Path(decky.DECKY_PLUGIN_SETTINGS_DIR) / 'settings.json'
|
||||
|
||||
|
||||
def read_settings():
|
||||
try:
|
||||
stored = json.loads(settings_file().read_text(encoding='utf-8'))
|
||||
except (FileNotFoundError, json.JSONDecodeError, UnicodeDecodeError):
|
||||
return {}
|
||||
if not isinstance(stored, dict):
|
||||
return {}
|
||||
# The first sample release wrote {"enabled": bool} without a schema.
|
||||
# Keep reading it; the next save writes the current format.
|
||||
schema = stored.get('schema', SETTINGS_SCHEMA)
|
||||
if type(schema) is not int or schema != SETTINGS_SCHEMA:
|
||||
raise ValueError('Settings were written by an unsupported sample version')
|
||||
return stored
|
||||
|
||||
|
||||
class Plugin:
|
||||
async def get_setting(self, key: str, default=False):
|
||||
if key != 'enabled':
|
||||
raise ValueError('Unknown setting')
|
||||
try:
|
||||
stored = json.loads(settings_file().read_text(encoding='utf-8'))
|
||||
except (FileNotFoundError, json.JSONDecodeError, UnicodeDecodeError):
|
||||
return default
|
||||
value = stored.get(key) if isinstance(stored, dict) else None
|
||||
value = read_settings().get(key)
|
||||
return value if type(value) is bool else default
|
||||
|
||||
async def set_setting(self, key: str, value: bool):
|
||||
if key != 'enabled' or type(value) is not bool:
|
||||
raise ValueError('Expected an enabled boolean')
|
||||
# Refuse to erase settings from a newer plugin after a downgrade.
|
||||
read_settings()
|
||||
target = settings_file()
|
||||
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
temporary = None
|
||||
@@ -32,7 +47,7 @@ class Plugin:
|
||||
with tempfile.NamedTemporaryFile('w', encoding='utf-8', dir=target.parent,
|
||||
prefix='.settings-', delete=False) as output:
|
||||
temporary = Path(output.name)
|
||||
json.dump({'enabled': value}, output)
|
||||
json.dump({'schema': SETTINGS_SCHEMA, 'enabled': value}, output)
|
||||
output.write('\n')
|
||||
temporary.chmod(0o600)
|
||||
os.replace(temporary, target)
|
||||
|
||||
@@ -11,6 +11,7 @@ import io
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
import zipfile
|
||||
@@ -27,6 +28,10 @@ for(const key of ['error','warn']){const original=console[key];console[key]=(...
|
||||
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');}};
|
||||
addEventListener('message', event=>{
|
||||
if(event.source!==parent||event.data?.type!=='audit:inspect')return;
|
||||
parent.postMessage({type:'audit:snapshot',text:(document.getElementById('framey-root')?.innerText||'').slice(0,2000)},'*');
|
||||
});
|
||||
</script>'''
|
||||
|
||||
|
||||
@@ -41,6 +46,27 @@ def render_panel(record, data):
|
||||
return framey_panel.panel(record['id']).replace(
|
||||
b'<script id="framey-bundle"', PRELUDE.encode()+b'<script id="framey-bundle"')
|
||||
|
||||
|
||||
def read_module_asset(path, records, cache):
|
||||
"""Serve the same receipt-bound module URL shape as the installed panel."""
|
||||
prefix = '/framey/module/'
|
||||
if not path.startswith(prefix):
|
||||
raise ValueError('Invalid module path')
|
||||
parts = path[len(prefix):].split('/', 2)
|
||||
if len(parts) != 3 or not re.fullmatch(r'[1-9][0-9]*', parts[0]):
|
||||
raise ValueError('Invalid plugin ID')
|
||||
plugin_id, receipt, name = parts
|
||||
if not framey_panel.RECEIPT.fullmatch(receipt) or not framey_panel.MODULE_PATH.fullmatch(name):
|
||||
raise ValueError('Invalid module receipt or path')
|
||||
record = records[int(plugin_id)]
|
||||
if receipt != record['sha256']:
|
||||
raise ValueError('Plugin receipt changed')
|
||||
data = (cache/(receipt+'.zip')).read_bytes()
|
||||
if hashlib.sha256(data).hexdigest() != receipt:
|
||||
raise ValueError('Archive hash mismatch')
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
||||
return archive.read(record['folder']+'/dist/'+name)
|
||||
|
||||
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.
|
||||
@@ -54,6 +80,7 @@ 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==='audit:snapshot')active.rootText=String(data.text).slice(0,2000);
|
||||
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:events')event.source.postMessage({type:'framey:reply',id:data.id,result:{events:[],cursor:0}},'*');
|
||||
@@ -71,10 +98,15 @@ window.runAudit=async(ids,settleMs=1200)=>{
|
||||
if(!Number.isInteger(settleMs)||settleMs<1200||settleMs>10000)throw Error('Invalid audit settle time');
|
||||
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:[],
|
||||
active={id:plugin.id,name:plugin.name,ready:false,error:null,logs:[],methods:[],rootText:'',
|
||||
library:librarySnapshot?'snapshot replay':'unavailable',running:runningSnapshot?'snapshot replay':'unavailable'};
|
||||
progress.textContent=plugin.id+' '+plugin.name;panel.src='/panel/'+plugin.id;
|
||||
await new Promise(resolve=>setTimeout(resolve,settleMs));results.push(active);active=null;
|
||||
await new Promise(resolve=>setTimeout(resolve,settleMs));
|
||||
// Detached Steam snapshots may contain personal library data. Keep text
|
||||
// observations out of reports that replay either snapshot.
|
||||
if(!librarySnapshot&&!runningSnapshot)panel.contentWindow.postMessage({type:'audit:inspect'},'*');
|
||||
await new Promise(resolve=>setTimeout(resolve,80));
|
||||
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;
|
||||
@@ -134,15 +166,7 @@ def main():
|
||||
return
|
||||
elif path.startswith('/framey/module/'):
|
||||
try:
|
||||
plugin_id, name = path.removeprefix('/framey/module/').split('/', 1)
|
||||
if not framey_panel.MODULE_PATH.fullmatch(name):
|
||||
raise ValueError('Invalid module path')
|
||||
record = records[int(plugin_id)]
|
||||
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:
|
||||
body = archive.read(record['folder']+'/dist/'+name)
|
||||
body = read_module_asset(path, records, args.cache)
|
||||
content_type = 'text/javascript; charset=utf-8'
|
||||
module_asset = True
|
||||
except (KeyError, ValueError, OSError, zipfile.BadZipFile):
|
||||
|
||||
@@ -80,7 +80,12 @@ def render(data):
|
||||
'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).', '',
|
||||
'[legacy MusicControl](evidence/framey-legacy-2026-10-25.md). '
|
||||
'A [shared Music Control runtime follow-up](evidence/framey-mpris-runtime-2026-10-26.md) corrects the earlier bound-method fixture error: actual Decky uses the legacy class calling convention, so explicit-self calls must stay. '
|
||||
'Exact managed installs also restore the system library path for D-Bus subprocesses, repairing a real Frame OS libtinfo/glibc failure in MusicControl 1.1.6. '
|
||||
'Both official entries pass transport/seek/volume, Info and reopen in local Chromium with actual Decky 3.3.0 amd64 and a private silent MPRIS service. '
|
||||
'Actual ARM64 Decky on the Frame passes read-only empty-player discovery for both. Existing copies need remove/reinstall or an explicit managed update. '
|
||||
'Actual media playback on ARM64, artwork and worn VR remain unverified; both stay partial.', '',
|
||||
'## 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.', '',
|
||||
|
||||
@@ -192,6 +192,13 @@ u=h/'.config/systemd/user/frame-control-decky.service'; u.parent.mkdir(parents=T
|
||||
self.assertEqual(
|
||||
ssh(f'sha256sum {installed}/dist/{chunk}').split()[0],
|
||||
hashlib.sha256(expected_chunk).hexdigest())
|
||||
if plugin['id'] == 53:
|
||||
self.assertEqual(ssh(f'test -f {installed}/py_modules/click.py && echo yes').strip(), 'yes')
|
||||
receipt = json.loads(ssh(f'cat {installed}/.frame-control-store.json'))
|
||||
self.assertEqual(receipt['compatShims'], ['shotty-click-app-dir'])
|
||||
if plugin['id'] in (12, 111):
|
||||
receipt = json.loads(ssh(f'cat {installed}/.frame-control-store.json'))
|
||||
self.assertEqual(receipt['compatShims'], ['music-control-system-dbus-env'])
|
||||
job = finished(ok('POST', '/api/decky/plugin',
|
||||
{'action':'remove','id':plugin['id'],'acknowledged':True}))
|
||||
self.assertFalse(job.get('error'), job)
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Silent MPRIS runtime fixture
|
||||
|
||||
`player.py` implements the synthetic target used by the
|
||||
[Music Control runtime evidence](../../../docs/evidence/framey-mpris-runtime-2026-10-26.md).
|
||||
It produces no audio. It requires system Python `dbus`, `gi` and GLib, and writes
|
||||
its independent control state to `/tmp/mpris-state.json`.
|
||||
|
||||
Run it only in a disposable container with **no network, devices, host home,
|
||||
audio or D-Bus mounts**. Create a private session bus listening at
|
||||
`/run/user/<container UID>/bus`: both official backends hard-code that path.
|
||||
Set `DBUS_SESSION_BUS_ADDRESS` to that private socket for the fixture. Use a
|
||||
private home including `.local/share/Steam/steamui/images`, because the upstream
|
||||
plugins create an album-art symlink on startup. Never run these plugins against
|
||||
a real user's player during this test: ID111 probes volume automatically.
|
||||
|
||||
Install each pinned archive separately through the managed transaction, then
|
||||
start the published Decky 3.3.0 loader with private `UNPRIVILEGED_PATH` and
|
||||
`PRIVILEGED_PATH`. Do not instantiate the legacy Plugin class: invoke methods
|
||||
through Decky's `loader/call_legacy_plugin_method` route with its keyword object.
|
||||
The two official entries share the `MusicControl` folder, so cannot coexist.
|
||||
This fixture is opt-in and is not imported by ordinary unit tests.
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
"""Silent MPRIS target. Run only inside the private network/device-isolated container."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import dbus, dbus.service, dbus.mainloop.glib
|
||||
from gi.repository import GLib
|
||||
P='org.mpris.MediaPlayer2.Player'
|
||||
R='org.mpris.MediaPlayer2'
|
||||
D='org.freedesktop.DBus.Properties'
|
||||
TRACK='/org/framey/TestTrack/1'
|
||||
class Player(dbus.service.Object):
|
||||
def __init__(self,bus):
|
||||
self.name=dbus.service.BusName('org.mpris.MediaPlayer2.FrameySilent',bus=bus)
|
||||
super().__init__(bus,'/org/mpris/MediaPlayer2')
|
||||
self.state={'PlaybackStatus':'Paused','Position':12000000,'Volume':0.4,'next':0,'previous':0,'writes':[]}
|
||||
self.save()
|
||||
def save(self):Path('/tmp/mpris-state.json').write_text(json.dumps(self.state))
|
||||
@dbus.service.method(D,in_signature='ss',out_signature='v')
|
||||
def Get(self,interface,prop):
|
||||
if interface==R and prop=='Identity':return dbus.String('Framey Silent Player')
|
||||
if interface==P:
|
||||
if prop=='Metadata':return dbus.Dictionary({'mpris:trackid':dbus.ObjectPath(TRACK),'mpris:length':dbus.Int64(180000000),'xesam:title':dbus.String('Silent Test Track'),'xesam:artist':dbus.Array(['Fixture Artist'],signature='s'),'xesam:album':dbus.String('Framey Test Album')},signature='sv')
|
||||
if prop=='PlaybackStatus':return dbus.String(self.state[prop])
|
||||
if prop=='Position':return dbus.Int64(self.state[prop])
|
||||
if prop=='Volume':return dbus.Double(self.state[prop])
|
||||
if prop=='CanSeek':return dbus.Boolean(True)
|
||||
raise dbus.exceptions.DBusException('Unknown property')
|
||||
@dbus.service.method(D,in_signature='ssv',out_signature='')
|
||||
def Set(self,interface,prop,value):
|
||||
assert interface==P and prop=='Volume' and 0<=value<=1
|
||||
self.state['Volume']=float(value);self.state['writes'].append(['Volume',float(value)]);self.save()
|
||||
@dbus.service.method(P,in_signature='',out_signature='')
|
||||
def PlayPause(self):
|
||||
self.state['PlaybackStatus']='Playing' if self.state['PlaybackStatus']=='Paused' else 'Paused';self.state['writes'].append(['PlayPause']);self.save()
|
||||
@dbus.service.method(P,in_signature='',out_signature='')
|
||||
def Pause(self):
|
||||
self.state['PlaybackStatus']='Paused';self.state['writes'].append(['Pause']);self.save()
|
||||
@dbus.service.method(P,in_signature='',out_signature='')
|
||||
def Next(self):
|
||||
self.state['next']+=1;self.state['writes'].append(['Next']);self.save()
|
||||
@dbus.service.method(P,in_signature='',out_signature='')
|
||||
def Previous(self):
|
||||
self.state['previous']+=1;self.state['writes'].append(['Previous']);self.save()
|
||||
@dbus.service.method(P,in_signature='x',out_signature='')
|
||||
def Seek(self,offset):
|
||||
self.state['Position']+=int(offset);self.state['writes'].append(['Seek',int(offset)]);self.save()
|
||||
@dbus.service.method(P,in_signature='ox',out_signature='')
|
||||
def SetPosition(self,track,pos):
|
||||
assert str(track)==TRACK and 0<=pos<=180000000
|
||||
self.state['Position']=int(pos);self.state['writes'].append(['SetPosition',int(pos)]);self.save()
|
||||
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
|
||||
player=Player(dbus.SessionBus())
|
||||
print('READY silent MPRIS fixture',flush=True)
|
||||
GLib.MainLoop().run()
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+5
-2
@@ -1,14 +1,16 @@
|
||||
# Pinned Decky store test archives
|
||||
|
||||
These are unmodified ZIPs downloaded from the official Decky store CDN on
|
||||
2026-10-25. Each ZIP includes its upstream `LICENSE`. They are test data, not
|
||||
plugins shipped or installed by Frame Control. The fake Frame tests serve them
|
||||
2026-10-25, except `111.zip` downloaded on 2026-10-26. Each ZIP includes its
|
||||
upstream `LICENSE`. They are test data, not plugins shipped or installed by
|
||||
Frame Control. The fake Frame tests serve them
|
||||
from a local fixture so CI does not depend on the store being online.
|
||||
|
||||
| File | Official plugin | Version | Store SHA-256 |
|
||||
| --- | --- | --- | --- |
|
||||
| `7.zip` | CSS Loader | 2.1.3 | `99beaf1374af82408e6d056e0b0147e5fca544e1035fa7ab88419231697bc231` |
|
||||
| `12.zip` | MusicControl | 1.1.6 | `246b89fd653c60a735b1cc401d1b0937d5cb969eea9024561661ff741f081d62` |
|
||||
| `111.zip` | Music Control | 1.1.7 | `3f56a3f56034b277ea74a2c37cc84133b69e382aa6bcb0b43876776aac737ed9` |
|
||||
| `14.zip` | ProtonDB Badges | 1.3.4 | `90a3453ac02709cad64096e7539fd8141fa56f99ae3441fc24a4990dc9684358` |
|
||||
| `123.zip` | Steam Status | 1.2.3 | `9053308e04a74f44745b45b9f4f50af3673348d367e7b14bacc5677de707ac4e` |
|
||||
| `50.zip` | Storage Cleaner | 1.4.0 | `1afa1a6f129c5d419de0e0bf0f6db8d8e50385a5659179346e996031ea372591` |
|
||||
@@ -20,6 +22,7 @@ from a local fixture so CI does not depend on the store being online.
|
||||
The matching source projects are
|
||||
[CSS Loader](https://github.com/suchmememanyskill/SDH-CssLoader),
|
||||
[MusicControl](https://github.com/nikurasu/MusicControl),
|
||||
[Music Control](https://github.com/martinpl/music-control),
|
||||
[ProtonDB Badges](https://github.com/bschelst/protondb-decky),
|
||||
[Steam Status](https://github.com/bschelst/steamstatus-decky),
|
||||
[Storage Cleaner](https://github.com/mcarlucci/decky-storage-cleaner),
|
||||
|
||||
@@ -5,6 +5,8 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).parent / 'fixtures/framey-official'
|
||||
PLUGINS = (
|
||||
{'id': 12, 'name': 'MusicControl', 'version': '1.1.6', 'api_version': 'legacy', 'folder': 'MusicControl', 'sha256': '246b89fd653c60a735b1cc401d1b0937d5cb969eea9024561661ff741f081d62'},
|
||||
{'id': 111, 'name': 'Music Control', 'version': '1.1.7', 'api_version': 'legacy',
|
||||
'folder': 'MusicControl', 'sha256': '3f56a3f56034b277ea74a2c37cc84133b69e382aa6bcb0b43876776aac737ed9'},
|
||||
{'id': 14, 'name': 'ProtonDB Badges', 'version': '1.3.4', 'api_version': 2,
|
||||
'folder': 'protondb-decky', 'sha256': '90a3453ac02709cad64096e7539fd8141fa56f99ae3441fc24a4990dc9684358'},
|
||||
{'id': 123, 'name': 'Steam Status', 'version': '1.2.3', 'api_version': 2,
|
||||
@@ -17,6 +19,8 @@ PLUGINS = (
|
||||
'folder': 'hltb-for-deck', 'sha256': 'bd54a23b0b8ec0798aa590e5be236522a593529d7680e77fb888e2971bf3d558'},
|
||||
{'id': 36, 'name': 'SteamGridDB', 'version': '1.8.1', 'api_version': 1,
|
||||
'folder': 'decky-steamgriddb', 'sha256': 'e84f5b1f111af8862b06218479491fbf61036e1bfd9f16f93f9901e9b9ebc5e0'},
|
||||
{'id': 53, 'name': 'Shotty', 'version': '0.1.0', 'api_version': 'legacy',
|
||||
'folder': 'Shotty', 'sha256': 'ab7f6618b264a0cb766525e788cef6c3e1478c0fef98a701225e5e8af441e2fe'},
|
||||
{'id': 63, 'name': 'Free Loader', 'version': '1.6.0', 'api_version': 1,
|
||||
'folder': 'free-loader', 'sha256': '05208aee6f6ba824391d8ddc81b1168b2599a9711e7528cdaa789f6cc710b42a'},
|
||||
{'id': 135, 'name': 'Deck Shelves', 'version': '2.0.0', 'api_version': 1,
|
||||
|
||||
@@ -29,6 +29,10 @@ vm.runInContext(testSource,context);
|
||||
// No Steam React tree exists inside the isolated Framey iframe.
|
||||
assert.equal(window.DFL.getReactRoot({}),null);
|
||||
assert.equal(window.DFL.findInReactTree(window.DFL.getReactRoot(null),()=>true),null);
|
||||
const inertSteamComponent=window.DFL.fakeRenderComponent(undefined).type;
|
||||
assert.equal(inertSteamComponent.name,'MissingSteamComponent');
|
||||
assert.equal(window.DFL.fakeRenderComponent(undefined).type,inertSteamComponent);
|
||||
assert.equal(typeof window.DFL.afterPatch(inertSteamComponent.prototype,'render',()=>{}).unpatch,'function');
|
||||
// Official @decky/rollup bundles use the automatic JSX runtime, including keyed lists.
|
||||
const jsx=window.SP_JSX.jsx('p',{id:'message',children:'Saved'},'message-key');
|
||||
assert.equal(jsx.type,'p');
|
||||
|
||||
@@ -121,14 +121,14 @@ class StoreTransaction(unittest.TestCase):
|
||||
self.folder = home / '.local/share/frame-control/decky/plugins/Example'
|
||||
self.folder.parent.mkdir(parents=True)
|
||||
|
||||
def apply(self, action, data=None, expected='-'):
|
||||
def apply(self, action, data=None, expected='-', plugin_id=42, folder='Example', version='1.0'):
|
||||
token = secrets.token_hex(12)
|
||||
source = Path('/tmp/frame-control-decky-plugin-' + token)
|
||||
if data is not None:
|
||||
source.write_bytes(data)
|
||||
digest = hashlib.sha256(data or b'').hexdigest()
|
||||
try:
|
||||
return subprocess.run([sys.executable, '-', action, token, '42', digest, 'Example', '1.0', expected],
|
||||
return subprocess.run([sys.executable, '-', action, token, str(plugin_id), digest, folder, version, expected],
|
||||
input=frame_decky_store.APPLY, text=True, capture_output=True,
|
||||
env=self.env, timeout=15)
|
||||
finally:
|
||||
@@ -161,6 +161,168 @@ class StoreTransaction(unittest.TestCase):
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertTrue(external.exists())
|
||||
|
||||
def test_exact_shotty_archive_gets_isolated_click_import(self):
|
||||
from framey_official import PLUGINS, archive as official_archive
|
||||
plugin = next(p for p in PLUGINS if p['id'] == 53)
|
||||
data = official_archive(plugin)
|
||||
self.assertEqual(hashlib.sha256(data).hexdigest(), plugin['sha256'])
|
||||
shotty = self.folder.parent / plugin['folder']
|
||||
args = {'plugin_id': plugin['id'], 'folder': plugin['folder'], 'version': plugin['version']}
|
||||
result = self.apply('install', data, **args)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
receipt = json.loads((shotty / '.frame-control-store.json').read_text())
|
||||
self.assertEqual(receipt['sha256'], plugin['sha256'])
|
||||
self.assertEqual(receipt['compatShims'], ['shotty-click-app-dir'])
|
||||
compat = shotty / 'py_modules/click.py'
|
||||
self.assertTrue(compat.is_file())
|
||||
source = '''import asyncio, importlib.util, logging, sys, types
|
||||
from pathlib import Path
|
||||
folder = Path(sys.argv[1])
|
||||
sys.path.insert(0, str(folder/'py_modules'))
|
||||
decky = types.ModuleType('decky_plugin')
|
||||
decky.logger = logging.getLogger('shotty-test')
|
||||
decky.DECKY_PLUGIN_DIR = str(folder)
|
||||
sys.modules['decky_plugin'] = decky
|
||||
spec = importlib.util.spec_from_file_location('shotty', folder/'main.py')
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
assert module.Plugin.__name__ == 'Plugin'
|
||||
from click import get_app_dir
|
||||
assert get_app_dir('Shotty') == str(Path.home()/'.config/Shotty')
|
||||
photo = Path.home()/'.local/share/Steam/userdata/100/760/remote/42/screenshots/test.jpg'
|
||||
photo.parent.mkdir(parents=True)
|
||||
photo.write_bytes(b'local screenshot fixture')
|
||||
plugin = module.Plugin()
|
||||
assert asyncio.run(plugin.aggregate_all([[42, 'Local Test Game']])) == 1
|
||||
copied = Path.home()/'Pictures/Screenshots/Local Test Game/test.jpg'
|
||||
assert copied.read_bytes() == b'local screenshot fixture'
|
||||
'''
|
||||
imported = subprocess.run([sys.executable, '-c', source, str(shotty)],
|
||||
capture_output=True, text=True,
|
||||
env={**self.env, 'XDG_CONFIG_HOME': str(Path(self.tmp.name)/'.config')}, timeout=10)
|
||||
self.assertEqual(imported.returncode, 0, imported.stderr)
|
||||
result = self.apply('remove', expected=plugin['sha256'], **args)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertFalse(shotty.exists())
|
||||
stream = io.BytesIO()
|
||||
with zipfile.ZipFile(stream, 'w') as bundle:
|
||||
bundle.writestr('Shotty/plugin.json', json.dumps({'name': 'Shotty', 'flags': []}))
|
||||
bundle.writestr('Shotty/dist/index.js', 'export default {};' + ' ' * 1200)
|
||||
other = stream.getvalue()
|
||||
result = self.apply('install', other, **args)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertFalse((shotty / 'py_modules/click.py').exists())
|
||||
self.assertNotIn('compatShims', json.loads((shotty / '.frame-control-store.json').read_text()))
|
||||
|
||||
def test_legacy_music_control_keeps_decky_class_calling_convention(self):
|
||||
from framey_official import PLUGINS, archive as official_archive
|
||||
plugin = next(p for p in PLUGINS if p['id'] == 111)
|
||||
data = official_archive(plugin)
|
||||
self.assertEqual(hashlib.sha256(data).hexdigest(), plugin['sha256'])
|
||||
folder = self.folder.parent / plugin['folder']
|
||||
args = {'plugin_id': plugin['id'], 'folder': plugin['folder'], 'version': plugin['version']}
|
||||
result = self.apply('install', data, **args)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
receipt = json.loads((folder / '.frame-control-store.json').read_text())
|
||||
self.assertEqual(receipt['sha256'], plugin['sha256'])
|
||||
self.assertEqual(receipt['compatShims'], ['music-control-system-dbus-env'])
|
||||
self.assertNotIn('api_version', json.loads((folder / 'plugin.json').read_text()))
|
||||
self.assertIn('self._get_dbus_env(self)', (folder / 'main.py').read_text())
|
||||
|
||||
dbus = Path(self.tmp.name) / 'dbus-send'
|
||||
dbus.write_text('''#!/usr/bin/env python3
|
||||
import os, sys
|
||||
from pathlib import Path
|
||||
args = ' '.join(sys.argv[1:])
|
||||
assert os.environ['LD_LIBRARY_PATH'] == os.environ.get('LD_LIBRARY_PATH_ORIG', '')
|
||||
with Path(os.environ['MPRIS_CALLS']).open('a') as log: log.write(args + '\\n')
|
||||
if 'ListNames' in args:
|
||||
print('method return\\n array [\\n string "org.freedesktop.DBus"\\n string "org.mpris.MediaPlayer2.fixture"\\n ]')
|
||||
elif 'string:PlaybackStatus' in args:
|
||||
print('method return\\n variant string "Paused"')
|
||||
else:
|
||||
print('method return')
|
||||
''')
|
||||
dbus.chmod(0o700)
|
||||
calls = Path(self.tmp.name) / 'mpris-calls'
|
||||
source = '''import asyncio, importlib.util, os, sys, types
|
||||
from pathlib import Path
|
||||
decky = types.ModuleType('decky_plugin')
|
||||
sys.modules['decky_plugin'] = decky
|
||||
spec = importlib.util.spec_from_file_location('music_control', Path(sys.argv[1])/'main.py')
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
# Decky 3.3 uses the class for manifests with no api_version.
|
||||
plugin = module.Plugin
|
||||
async def check():
|
||||
assert await plugin.sp_list_media_players(plugin) == 'org.mpris.MediaPlayer2.fixture'
|
||||
await plugin.sp_set_media_player(plugin, 'org.mpris.MediaPlayer2.fixture')
|
||||
assert await plugin.sp_get_media_player(plugin) == 'org.mpris.MediaPlayer2.fixture'
|
||||
await plugin.sp_play(plugin)
|
||||
await plugin.sp_pause(plugin)
|
||||
await plugin.sp_next(plugin)
|
||||
await plugin.sp_previous(plugin)
|
||||
await plugin.sp_set_volume(plugin, 0.25)
|
||||
assert await plugin.sp_track_status(plugin) == 'Paused'
|
||||
asyncio.run(check())
|
||||
assert os.environ['LD_LIBRARY_PATH'] == 'FrameyBundledLibraries'
|
||||
'''
|
||||
called = subprocess.run([sys.executable, '-c', source, str(folder)],
|
||||
capture_output=True, text=True,
|
||||
env={**self.env, 'MPRIS_CALLS': str(calls), 'LD_LIBRARY_PATH': 'FrameyBundledLibraries',
|
||||
'LD_LIBRARY_PATH_ORIG': 'FrameySystemLibraries'}, timeout=10)
|
||||
self.assertEqual(called.returncode, 0, called.stderr)
|
||||
logged = calls.read_text()
|
||||
self.assertIn('org.freedesktop.DBus.ListNames', logged)
|
||||
self.assertIn('org.mpris.MediaPlayer2.Player.PlayPause', logged)
|
||||
self.assertIn('org.mpris.MediaPlayer2.Player.Pause', logged)
|
||||
self.assertIn('string:Volume variant:double:0.25', logged)
|
||||
self.assertIn('org.mpris.MediaPlayer2.fixture', logged)
|
||||
# An explicit same-version update replaces the previously shipped bad shim.
|
||||
main = folder / 'main.py'
|
||||
main.write_text(main.read_text().replace('self._get_dbus_env(self)', 'self._get_dbus_env()'))
|
||||
receipt['compatShims'] = ['music-control-bound-methods']
|
||||
(folder / '.frame-control-store.json').write_text(json.dumps(receipt))
|
||||
result = self.apply('update', data, expected=plugin['sha256'], **args)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
called = subprocess.run([sys.executable, '-c', source, str(folder)],
|
||||
capture_output=True, text=True,
|
||||
env={**self.env, 'MPRIS_CALLS': str(calls),
|
||||
'LD_LIBRARY_PATH': 'FrameyBundledLibraries'}, timeout=10)
|
||||
self.assertEqual(called.returncode, 0, called.stderr)
|
||||
result = self.apply('remove', expected=plugin['sha256'], **args)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertFalse(folder.exists())
|
||||
|
||||
older = next(p for p in PLUGINS if p['id'] == 12)
|
||||
old_data = official_archive(older)
|
||||
old_args = {'plugin_id': older['id'], 'folder': older['folder'], 'version': older['version']}
|
||||
result = self.apply('install', old_data, **old_args)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(json.loads((folder / '.frame-control-store.json').read_text())['compatShims'],
|
||||
['music-control-system-dbus-env'])
|
||||
self.assertNotIn('api_version', json.loads((folder / 'plugin.json').read_text()))
|
||||
self.assertIn('self._get_dbus_env(self)', (folder / 'main.py').read_text())
|
||||
old_env = {**self.env, 'MPRIS_CALLS': str(calls), 'LD_LIBRARY_PATH': 'FrameyBundledLibraries'}
|
||||
old_env.pop('LD_LIBRARY_PATH_ORIG', None)
|
||||
called = subprocess.run([sys.executable, '-c', source, str(folder)],
|
||||
capture_output=True, text=True, env=old_env, timeout=10)
|
||||
self.assertEqual(called.returncode, 0, called.stderr)
|
||||
result = self.apply('remove', expected=older['sha256'], **old_args)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
# A future archive at the same store ID must retain its own source.
|
||||
other = io.BytesIO()
|
||||
untouched = 'class Plugin: pass\n'
|
||||
with zipfile.ZipFile(other, 'w') as bundle:
|
||||
bundle.writestr('MusicControl/plugin.json', json.dumps({'name': 'Music Control', 'flags': []}))
|
||||
bundle.writestr('MusicControl/dist/index.js', 'export default {};' + ' ' * 1200)
|
||||
bundle.writestr('MusicControl/main.py', untouched)
|
||||
result = self.apply('install', other.getvalue(), **args)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual((folder / 'main.py').read_text(), untouched)
|
||||
self.assertNotIn('compatShims', json.loads((folder / '.frame-control-store.json').read_text()))
|
||||
|
||||
def test_installed_script_entrypoints_execute_without_zip_exec_bits(self):
|
||||
data, _ = archive(extra=('Example/scripts/start.py',
|
||||
b'#!/bin/sh\nprintf "Framey script: %s\\n" "$1"\n'))
|
||||
|
||||
@@ -5,6 +5,7 @@ import hashlib
|
||||
import importlib.util
|
||||
import io
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
|
||||
@@ -21,6 +22,7 @@ class ReceiptBoundAudit(unittest.TestCase):
|
||||
stream = io.BytesIO()
|
||||
with zipfile.ZipFile(stream, 'w') as archive:
|
||||
archive.writestr('Example/dist/index.js', bundle)
|
||||
archive.writestr('Example/dist/chunks/frontend.js', b'export const ready = true;')
|
||||
data = stream.getvalue()
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
record = {'id':42, 'folder':'Example', 'sha256':digest}
|
||||
@@ -30,3 +32,20 @@ class ReceiptBoundAudit(unittest.TestCase):
|
||||
self.assertIn(b'const report=(kind,message)', page)
|
||||
with self.assertRaisesRegex(ValueError, 'hash mismatch'):
|
||||
audit.render_panel({**record, 'sha256':'0'*64}, data)
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
cache = Path(directory)
|
||||
(cache/(digest+'.zip')).write_bytes(data)
|
||||
records = {42:record}
|
||||
route = f'/framey/module/42/{digest}/'
|
||||
self.assertEqual(audit.read_module_asset(route+'index.js', records, cache), bundle)
|
||||
self.assertEqual(audit.read_module_asset(route+'chunks/frontend.js', records, cache),
|
||||
b'export const ready = true;')
|
||||
for bad in (f'/framey/module/42/{"0"*64}/index.js',
|
||||
route+'chunks/../frontend.js', route+'other.json',
|
||||
f'/framey/module/042/{digest}/index.js'):
|
||||
with self.subTest(bad=bad), self.assertRaises((ValueError, KeyError)):
|
||||
audit.read_module_asset(bad, records, cache)
|
||||
(cache/(digest+'.zip')).write_bytes(data+b'changed')
|
||||
with self.assertRaisesRegex(ValueError, 'hash mismatch'):
|
||||
audit.read_module_asset(route+'index.js', records, cache)
|
||||
@@ -1,6 +1,7 @@
|
||||
"""The author example stores settings as a normal Decky user plugin."""
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -36,7 +37,8 @@ class SamplePlugin(unittest.TestCase):
|
||||
self.assertTrue(asyncio.run(first.set_setting('enabled', True)))
|
||||
second = backend(folder)
|
||||
self.assertTrue(asyncio.run(second.get_setting('enabled', False)))
|
||||
self.assertTrue((Path(folder) / 'settings.json').is_file())
|
||||
self.assertEqual(json.loads((Path(folder) / 'settings.json').read_text()),
|
||||
{'schema': 1, 'enabled': True})
|
||||
|
||||
def test_backend_rejects_unknown_keys_and_non_boolean_values(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
@@ -57,6 +59,43 @@ class SamplePlugin(unittest.TestCase):
|
||||
self.assertTrue(asyncio.run(plugin.set_setting('enabled', True)))
|
||||
self.assertTrue(asyncio.run(backend(folder).get_setting('enabled', False)))
|
||||
|
||||
def test_legacy_boolean_survives_restart_and_migrates_on_save(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / 'settings.json'
|
||||
path.write_text('{"enabled": true}\n')
|
||||
self.assertTrue(asyncio.run(backend(folder).get_setting('enabled', False)))
|
||||
self.assertEqual(json.loads(path.read_text()), {'enabled': True},
|
||||
'Reading must not rewrite a user setting')
|
||||
self.assertTrue(asyncio.run(backend(folder).set_setting('enabled', False)))
|
||||
self.assertEqual(json.loads(path.read_text()), {'schema': 1, 'enabled': False})
|
||||
self.assertFalse(asyncio.run(backend(folder).get_setting('enabled', True)))
|
||||
|
||||
def test_newer_schema_is_preserved_on_read_and_save(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / 'settings.json'
|
||||
original = '{"schema": 2, "enabled": true, "future": "keep"}\n'
|
||||
path.write_text(original)
|
||||
plugin = backend(folder)
|
||||
with self.assertRaisesRegex(ValueError, 'unsupported sample version'):
|
||||
asyncio.run(plugin.get_setting('enabled', False))
|
||||
with self.assertRaisesRegex(ValueError, 'unsupported sample version'):
|
||||
asyncio.run(plugin.set_setting('enabled', False))
|
||||
self.assertEqual(path.read_text(), original)
|
||||
self.assertEqual(list(Path(folder).glob('.settings-*')), [])
|
||||
|
||||
def test_failed_atomic_replace_keeps_previous_setting(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / 'settings.json'
|
||||
plugin = backend(folder)
|
||||
self.assertTrue(asyncio.run(plugin.set_setting('enabled', True)))
|
||||
previous = path.read_bytes()
|
||||
with patch('os.replace', side_effect=OSError('storage unavailable')):
|
||||
with self.assertRaisesRegex(OSError, 'storage unavailable'):
|
||||
asyncio.run(plugin.set_setting('enabled', False))
|
||||
self.assertEqual(path.read_bytes(), previous)
|
||||
self.assertEqual(list(Path(folder).glob('.settings-*')), [])
|
||||
self.assertTrue(asyncio.run(backend(folder).get_setting('enabled', False)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
+40
-1
@@ -208,6 +208,43 @@ try:
|
||||
entry.chmod(entry.stat().st_mode|0o100)
|
||||
unpacked=stage/folder
|
||||
if not unpacked.is_dir(): raise RuntimeError('Missing plugin folder')
|
||||
compat_shims=[]
|
||||
# Shotty 0.1.0 imports click.get_app_dir but never calls it. Its
|
||||
# official ZIP omits click and Decky's bundled Python lacks it.
|
||||
# Supply the Linux function only inside this exact plugin's
|
||||
# py_modules path; other plugins keep their original imports.
|
||||
if int(plugin_id)==53 and digest=='ab7f6618b264a0cb766525e788cef6c3e1478c0fef98a701225e5e8af441e2fe' and folder=='Shotty':
|
||||
compat=unpacked/'py_modules/click.py'
|
||||
if compat.exists(): raise RuntimeError('Shotty compatibility module collides with archive')
|
||||
compat.parent.mkdir(parents=True,exist_ok=True)
|
||||
compat.write_text(
|
||||
'"""Frame Control Linux compatibility for Shotty 0.1.0."""\n'
|
||||
'import os\n'
|
||||
'def get_app_dir(app_name, roaming=True, force_posix=False):\n'
|
||||
' if not isinstance(app_name, str) or not app_name:\n'
|
||||
' raise ValueError("app_name must be a nonempty string")\n'
|
||||
' base = os.environ.get("XDG_CONFIG_HOME")\n'
|
||||
' if not base or not os.path.isabs(base):\n'
|
||||
' base = os.path.join(os.path.expanduser("~"), ".config")\n'
|
||||
' return os.path.join(base, app_name)\n',encoding='utf-8')
|
||||
compat_shims.append('shotty-click-app-dir')
|
||||
# These legacy backends launch system dbus-send (1.1.6 via /bin/sh).
|
||||
# Decky's PyInstaller LD_LIBRARY_PATH selects bundled libtinfo built
|
||||
# against newer glibc than Frame OS. Restore its saved system path
|
||||
# only in the copied subprocess environment; retain legacy self calls.
|
||||
music_digests={
|
||||
12:'246b89fd653c60a735b1cc401d1b0937d5cb969eea9024561661ff741f081d62',
|
||||
111:'3f56a3f56034b277ea74a2c37cc84133b69e382aa6bcb0b43876776aac737ed9',
|
||||
}
|
||||
if digest==music_digests.get(int(plugin_id)) and folder=='MusicControl':
|
||||
main=unpacked/'main.py'
|
||||
source=main.read_text(encoding='utf-8')
|
||||
old=' env = os.environ.copy()\n'
|
||||
if source.count(old)!=1:
|
||||
raise RuntimeError('Music Control compatibility source changed')
|
||||
source=source.replace(old,old+' env["LD_LIBRARY_PATH"] = env.get("LD_LIBRARY_PATH_ORIG", "")\n')
|
||||
main.write_text(source,encoding='utf-8')
|
||||
compat_shims.append('music-control-system-dbus-env')
|
||||
# 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')
|
||||
@@ -221,7 +258,9 @@ try:
|
||||
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')
|
||||
record={'storeId':int(plugin_id),'sha256':digest,'version':version}
|
||||
if compat_shims: record['compatShims']=compat_shims
|
||||
(unpacked/marker).write_text(json.dumps(record)+'\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')
|
||||
try:
|
||||
|
||||
@@ -474,12 +474,14 @@
|
||||
"https://github.com/mirobouma/MusicControl.git"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "Legacy media controls initialize; player discovery and playback are unverified.",
|
||||
"reason": "Managed reinstall preserves Decky’s legacy calling convention and repairs the Frame OS shell library-path failure. Chromium transport/seek/volume against a silent MPRIS fixture and real ARM64 empty-player discovery pass; actual Frame media playback, album art and worn VR remain unverified. Reinstall existing copies to apply.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame"
|
||||
"fake-Frame",
|
||||
"isolated Decky 3.3.0 + MPRIS fixture",
|
||||
"real Frame SSH"
|
||||
],
|
||||
"browser": {
|
||||
"id": 12,
|
||||
@@ -506,7 +508,7 @@
|
||||
"folder": "MusicControl"
|
||||
},
|
||||
"backend": "Python",
|
||||
"hardware": "No specific hardware requirement detected statically; runtime requirements unverified.",
|
||||
"hardware": "User-session MPRIS player and dbus-send required; no Deck-specific hardware.",
|
||||
"uiSurfaces": [
|
||||
"local/Steam routes"
|
||||
],
|
||||
@@ -515,7 +517,9 @@
|
||||
"sha256": "246b89fd653c60a735b1cc401d1b0937d5cb969eea9024561661ff741f081d62",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
}
|
||||
},
|
||||
"compatShim": "music-control-system-dbus-env",
|
||||
"evidence": "docs/evidence/framey-mpris-runtime-2026-10-26.md"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
@@ -927,7 +931,7 @@
|
||||
"logs": [
|
||||
{
|
||||
"kind": "error",
|
||||
"message": "[Bluetooth] !! get_bluetooth_status \u2192 Error: All retries failed. Last error: Error: Read-only compatibility audit: backend method blocked"
|
||||
"message": "[Bluetooth] !! get_bluetooth_status → Error: All retries failed. Last error: Error: Read-only compatibility audit: backend method blocked"
|
||||
},
|
||||
{
|
||||
"kind": "error",
|
||||
@@ -935,7 +939,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "error",
|
||||
"message": "[Bluetooth] !! get_paired_devices_with_info \u2192 Error: All retries failed. Last error: Error: Read-only compatibility audit: backend method blocked"
|
||||
"message": "[Bluetooth] !! get_paired_devices_with_info → Error: All retries failed. Last error: Error: Read-only compatibility audit: backend method blocked"
|
||||
},
|
||||
{
|
||||
"kind": "error",
|
||||
@@ -1790,7 +1794,7 @@
|
||||
},
|
||||
"backend": "Python + native payload",
|
||||
"hardware": "No specific hardware requirement detected statically; runtime requirements unverified.",
|
||||
"nativePort": "The bin/hash helper needs an ARM64 build and cross-platform hashing tests; no ARM64 artifact is published.",
|
||||
"nativePort": "A source-matched ARM64 helper matched one synthetic ROM on the real Frame (docs/evidence/framey-emuchievements-arm-2026-10-26.md). The pinned upstream source proposed for packaging also has unsafe archive extraction and cleanup paths (docs/evidence/framey-native-port-blockers-2026-10-26.md); fix and test those before shipping an ARM64 binary. The official x86_64 ZIP remains blocked; format coverage, plugin backend/UI, login and worn VR are unverified.",
|
||||
"uiSurfaces": [
|
||||
"local/Steam routes",
|
||||
"toasts",
|
||||
@@ -3593,7 +3597,7 @@
|
||||
"https://github.com/GedasFX/decky-cloud-save.git"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "Unsafe plugin archive member.",
|
||||
"reason": "The exact official ZIP contains a 60,776,600-byte x86_64 rclone member, above the 30 MiB member limit and unusable on ARM64. Installation remains blocked.",
|
||||
"blockInstall": true,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -3628,7 +3632,7 @@
|
||||
},
|
||||
"backend": "Python + native payload",
|
||||
"hardware": "No specific hardware requirement detected statically; runtime requirements unverified.",
|
||||
"nativePort": "rclone needs an ARM64 artifact; this ZIP also fails safe member validation. Cloud writes were not exercised.",
|
||||
"nativePort": "An ARM64 rclone artifact and safe package policy are needed; the official 60,776,600-byte x86_64 member fails archive validation. No cloud writes were run (docs/evidence/framey-native-port-blockers-2026-10-26.md).",
|
||||
"uiSurfaces": [
|
||||
"local/Steam routes",
|
||||
"toasts",
|
||||
@@ -3795,8 +3799,8 @@
|
||||
"https://github.com/SteamDeckHomebrew/decky-loader",
|
||||
"https://github.com/yt-dlp/yt-dlp"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "Frontend audit failed: Cannot read properties of null (reading 'type').",
|
||||
"status": "partial",
|
||||
"reason": "An inert local component lets the exact frontend initialize and render its Settings view in opaque Chromium. Backend get_setting and plugin network were denied in the audit; Steam game-page theme injection has no verified target in Framey. Music search, playback, downloads, settings persistence and worn VR remain unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -3829,6 +3833,13 @@
|
||||
"sha256": "9ce293ab4db212c03c6b96dfbc5e4d92e874d33387463f53223773921f75a80e",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"renderFollowUp": {
|
||||
"evidence": "docs/evidence/framey-game-theme-music-2026-10-26.md",
|
||||
"mode": "exact archive in opaque local Chromium; backend and network denied",
|
||||
"frontendReady": true,
|
||||
"settingsRendered": true,
|
||||
"steamGamePagePatched": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -3875,14 +3886,15 @@
|
||||
"sourceURLs": [
|
||||
"https://github.com/safijari/Shotty"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "Library snapshot fixes the frontend allApps crash. Actual published Decky 3.3.0 amd64 backend fails importing click.get_app_dir (ModuleNotFoundError); the archive does not bundle click. Screenshot aggregation/rescue was not run.",
|
||||
"status": "partial",
|
||||
"reason": "The frontend starts with the read-only Frame library snapshot. Frame Control adds a digest-scoped click.get_app_dir compatibility module to the managed install; the exact backend then imports and copies a synthetic Steam screenshot into a temporary Pictures folder in isolated Python. Actual Decky 3.3.0 ARM64 loading, live screenshots/rescue, in-VR panel and physical controller use remain unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame",
|
||||
"real Frame SSH"
|
||||
"real Frame SSH",
|
||||
"isolated Python backend"
|
||||
],
|
||||
"browser": {
|
||||
"id": 53,
|
||||
@@ -3913,6 +3925,13 @@
|
||||
"evidence": "docs/evidence/framey-library-context-2026-10-26.json",
|
||||
"mode": "real Frame read-only CEF projection; local Chromium snapshot replay; scoped actual amd64 backends",
|
||||
"frontendReady": true
|
||||
},
|
||||
"backendFollowUp": {
|
||||
"evidence": "docs/evidence/framey-shotty-python-2026-10-26.md",
|
||||
"mode": "exact archive managed transaction and isolated Python import/copy with temporary HOME",
|
||||
"compatShim": "shotty-click-app-dir",
|
||||
"syntheticScreenshotCopied": true,
|
||||
"actualDeckyArm64": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -9567,12 +9586,15 @@
|
||||
"https://github.com/martinpl/music-control"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "Panel initializer succeeds with backend/network blocked; actual controls and backend remain unverified.",
|
||||
"reason": "Managed reinstall preserves Decky’s legacy calling convention and repairs the Frame OS shell library-path failure. Chromium transport/seek/volume against a silent MPRIS fixture and real ARM64 empty-player discovery pass; actual Frame media playback, album art and worn VR remain unverified. Reinstall existing copies to apply.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame"
|
||||
"fake-Frame",
|
||||
"isolated Python backend",
|
||||
"isolated Decky 3.3.0 + MPRIS fixture",
|
||||
"real Frame SSH"
|
||||
],
|
||||
"browser": {
|
||||
"id": 111,
|
||||
@@ -9599,7 +9621,7 @@
|
||||
"folder": "MusicControl"
|
||||
},
|
||||
"backend": "Python",
|
||||
"hardware": "No specific hardware requirement detected statically; runtime requirements unverified.",
|
||||
"hardware": "User-session MPRIS player and dbus-send required; no Deck-specific hardware.",
|
||||
"uiSurfaces": [
|
||||
"local/Steam routes"
|
||||
],
|
||||
@@ -9608,7 +9630,9 @@
|
||||
"sha256": "3f56a3f56034b277ea74a2c37cc84133b69e382aa6bcb0b43876776aac737ed9",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
}
|
||||
},
|
||||
"compatShim": "music-control-system-dbus-env",
|
||||
"evidence": "docs/evidence/framey-mpris-runtime-2026-10-26.md"
|
||||
},
|
||||
{
|
||||
"id": 112,
|
||||
@@ -12061,12 +12085,13 @@
|
||||
"https://github.com/sponsors/santojon"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "The exact chunked frontend now initializes and renders Deck Shelves settings in an opaque local Chromium panel. Backend calls were denied; Home-screen shelf injection has no verified Steam UI target in Framey. Settings writes, real backend behavior and worn VR are unverified.",
|
||||
"reason": "The exact chunked frontend renders settings in opaque local Chromium. The official Python backend saved a shelf and returned it after reopen with a stub Decky module and temporary settings directory. Actual Decky RPC, real Frame settings, Steam Home shelf injection and worn VR remain unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame"
|
||||
"fake-Frame",
|
||||
"isolated Python backend"
|
||||
],
|
||||
"browser": {
|
||||
"id": 135,
|
||||
@@ -12101,6 +12126,13 @@
|
||||
"mode": "exact archive in opaque local Chromium; backend and network denied",
|
||||
"frontendReady": true,
|
||||
"settingsRendered": true
|
||||
},
|
||||
"backendFollowUp": {
|
||||
"evidence": "docs/evidence/framey-deck-shelves-browser-2026-10-26.md",
|
||||
"mode": "exact archive Python with stub Decky module and temporary settings directory",
|
||||
"defaultSettingsRead": true,
|
||||
"shelfSaveReopen": true,
|
||||
"actualDeckyRpc": false
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+6
-1
@@ -390,6 +390,7 @@
|
||||
addPatch:()=>()=>{}, removePatch:()=>{}, addGlobalComponent:()=>{}, removeGlobalComponent:()=>{},
|
||||
};
|
||||
const noPatch = () => {const undo=()=>{};undo.unpatch=undo;return undo;};
|
||||
class MissingSteamComponent {}
|
||||
const showModal = element => {
|
||||
let resolveClosed;
|
||||
const ClosedPromise = new Promise(resolve=>{resolveClosed=resolve;});
|
||||
@@ -509,7 +510,11 @@
|
||||
for(const classes of classModules.values())
|
||||
for(const name of Object.keys(classes)) if(classes[name]===token)return name;
|
||||
},
|
||||
findSP:()=>null, fakeRenderComponent:()=>null,
|
||||
findSP:()=>null,
|
||||
// Some legacy panels dereference the type of a Steam-only component before
|
||||
// installing a patch. Give them an inert local type; no Steam tree is
|
||||
// rendered or patched in this isolated iframe.
|
||||
fakeRenderComponent:()=>({type:MissingSteamComponent}),
|
||||
appDetailsClasses: {}, appDetailsHeaderClasses: {},
|
||||
useParams: () => currentParams,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user