Compare commits

..
Author SHA1 Message Date
Alex Southwell 4da7220660 Merge pull request #364 from saphid/release/v0.4.51
Prepare Frame Control v0.4.51 candidate
2026-10-27 04:48:22 +00:00
SAPHID 728a4dbe9b Record v0.4.51 candidate smoke and remaining gates 2026-10-27 04:39:00 +00:00
SAPHID 91cf774b21 Prepare v0.4.51 with safe smoke proximity gate 2026-10-27 04:32:11 +00:00
Alex Southwell 560f3de563 Merge pull request #363 from saphid/feat/framey-localsend-managed-arm64
Adapt pinned LocalSend helper for ARM64 Framey installs
2026-10-27 04:30:20 +00:00
SAPHID 022d407ff9 Record bounded ARM64 LocalSend transfer in catalog 2026-10-27 04:21:35 +00:00
SAPHID 014f554012 Include LocalSend helper in desktop installers 2026-10-27 04:19:09 +00:00
SAPHID c847b7a3ca Align LocalSend evidence with native runtime checks 2026-10-27 04:17:33 +00:00
SAPHID ee13edabb8 Record isolated ARM64 LocalSend transfer on Frame 2026-10-27 04:15:40 +00:00
SAPHID fc030c6191 Merge remote-tracking branch 'origin/main' into feat/framey-localsend-managed-arm64 2026-10-27 04:08:43 +00:00
SAPHID 8990ddfb91 Adapt official LocalSend helper for managed ARM64 Framey 2026-10-27 04:08:34 +00:00
Alex Southwell af8fc63f3b Merge pull request #362 from saphid/feat/framey-localsend-arm64
Document official LocalSend source match for ARM64
2026-10-27 03:55:55 +00:00
SAPHID 9c39300185 Record isolated official LocalSend backend transfer 2026-10-27 03:48:09 +00:00
SAPHID 592988c212 Clarify LocalSend earlier build revision 2026-10-27 03:41:07 +00:00
SAPHID 89f5dc164e Merge remote-tracking branch 'origin/main' into feat/framey-localsend-arm64 2026-10-27 03:40:49 +00:00
SAPHID 13c689926d Document exact LocalSend source and ARM64 build provenance 2026-10-27 03:40:18 +00:00
Alex Southwell 1b143094d1 Merge pull request #361 from saphid/docs/v0450-release-verification
Record v0.4.50 release verification
2026-10-27 03:37:03 +00:00
SAPHID b2cc34576c Record v0.4.50 release verification 2026-10-27 03:21:19 +00:00
Alex Southwell 2797212d8c Merge pull request #360 from saphid/release/v0.4.50
Prepare Frame Control v0.4.50 candidate
2026-10-27 02:51:32 +00:00
SAPHID dab13fa876 Reject release jobs failed in earlier run attempts 2026-10-27 02:39:01 +00:00
SAPHID 36cef1096a Record v0.4.50 locked Frame smoke 2026-10-27 02:32:12 +00:00
SAPHID fcae5d66de Record v0.4.50 candidate gates and smoke limit 2026-10-27 02:06:18 +00:00
SAPHID a160043c63 Prepare Frame Control v0.4.50 candidate 2026-10-27 02:04:37 +00:00
Alex Southwell 64938443c8 Merge pull request #359 from saphid/feat/framey-store-request-recovery
Recover Framey store jobs after lost responses
2026-10-27 02:01:07 +00:00
Alex Southwell 1911923afa Merge pull request #358 from saphid/fix/framey-toggle-value
fix(framey): show saved values in standalone Decky toggles
2026-10-27 01:53:33 +00:00
Alex Southwell 2419279a97 Merge pull request #357 from saphid/feat/framey-progress-controls
Show indeterminate Framey plugin progress
2026-10-27 01:46:24 +00:00
Alex Southwell d958148c73 Merge pull request #354 from saphid/feat/framey-local-route-history
Return through local Framey panel routes
2026-10-27 01:39:05 +00:00
SAPHID a27e09e72c Merge remote-tracking branch 'origin/main' into feat/framey-store-request-recovery 2026-10-27 01:35:15 +00:00
Alex Southwell b3618690c2 Merge pull request #353 from saphid/feat/framey-sample-reset-modal
Show a reset confirmation in the Framey sample
2026-10-27 01:29:42 +00:00
SAPHID 030873703e Merge main and preserve Toggle and file-server runtime coverage 2026-10-27 01:28:29 +00:00
SAPHID c7979a862d Recover Framey store jobs after lost responses 2026-10-27 01:27:22 +00:00
Alex Southwell b963937227 Merge pull request #355 from saphid/feat/framey-fileserver-arm64
feat(framey): run DeckyFileServer backend on ARM64 without root
2026-10-27 01:23:04 +00:00
Alex Southwell 7053c40334 Merge pull request #356 from saphid/fix/panels-windows-oversize-response
Return reliable errors for oversized panel requests on Windows
2026-10-27 01:16:36 +00:00
SAPHID f8aff5496b fix(framey): honor saved values in standalone Decky toggles 2026-10-27 01:15:17 +00:00
SAPHID c0d8cb9dc4 Clarify Framey progress text placement 2026-10-27 00:49:03 +00:00
SAPHID 2888a45a30 Show indeterminate Framey plugin progress 2026-10-27 00:48:29 +00:00
SAPHID 92a84ea683 Return reliable errors for oversized panel requests on Windows 2026-10-27 00:35:50 +00:00
SAPHID 39aa8d56fb Return through local Framey panel routes 2026-10-27 00:28:47 +00:00
SAPHID 5097a9f999 Show a reset confirmation in the Framey sample 2026-10-27 00:08:04 +00:00
56 changed files with 1701 additions and 107 deletions

No files matched your search

+6
View File
@@ -20,6 +20,12 @@ const REQUIRED = [
"ui/vendor/deckyfileserver-1.2.0/GO-LICENSE.txt",
"ui/vendor/deckyfileserver-1.2.0/README.md",
"scripts/build-framey-fileserver.py",
"ui/vendor/localsend-0.4.5/aarch64.gz",
"ui/vendor/localsend-0.4.5/source.tar.gz",
"ui/vendor/localsend-0.4.5/LICENSE.txt",
"ui/vendor/localsend-0.4.5/GO-LICENSE.txt",
"ui/vendor/localsend-0.4.5/README.md",
"scripts/build-framey-localsend.py",
"ui/vendor/psutil-7.2.2/aarch64.whl",
"ui/vendor/psutil-7.2.2/x86_64.whl",
"ui/vendor/psutil-7.2.2/LICENSE.txt",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "frame-control",
"version": "0.4.49",
"version": "0.4.51",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frame-control",
"version": "0.4.49",
"version": "0.4.51",
"license": "MIT",
"devDependencies": {
"electron": "^44.4.5",
+8 -2
View File
@@ -1,7 +1,7 @@
{
"name": "frame-control",
"productName": "Frame Control",
"version": "0.4.49",
"version": "0.4.51",
"description": "Desktop app for managing a Valve Steam Frame over SSH",
"private": true,
"main": "main.js",
@@ -65,6 +65,11 @@
"to": "ui/vendor/deckyfileserver-1.2.0",
"filter": ["*.gz", "*.txt", "README.md"]
},
{
"from": "../ui/vendor/localsend-0.4.5",
"to": "ui/vendor/localsend-0.4.5",
"filter": ["*.gz", "*.txt", "README.md"]
},
{
"from": "../ui/vendor/psutil-7.2.2",
"to": "ui/vendor/psutil-7.2.2",
@@ -86,7 +91,8 @@
"obs-headset-source.py",
"frame_power_sample.py",
"frame_power_report.py",
"build-framey-fileserver.py"
"build-framey-fileserver.py",
"build-framey-localsend.py"
]
},
{
@@ -0,0 +1,42 @@
{
"helperSha256": "e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae",
"lockAcquired": true,
"action": "private_network_marker_only",
"before": {
"activity": 3,
"proximity": [
4.6875
],
"battery": [
72
],
"services": {
"frame-control-decky.service": "inactive",
"frame-control-headset.service": "inactive"
}
},
"transfer": {
"result": "private_arm64_transfer_passed",
"protocolVersion": "2.0",
"markerBytes": 38,
"markerSha256": "3e724efac20a4a600a93f7bdddd469b8287a6aaa2b7b433d838d8ef23e1a9c2c",
"network": "private-veth-no-default-route",
"helperSha256": "e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae"
},
"after": {
"activity": 3,
"proximity": [
4.6875
],
"battery": [
72
],
"services": {
"frame-control-decky.service": "inactive",
"frame-control-headset.service": "inactive"
}
},
"result": "private_arm64_transfer_passed",
"ownedDirectoryAbsent": true,
"ownLockReleased": true
}
@@ -0,0 +1,62 @@
# Decky Localsend managed ARM64 candidate, 27 October 2026
Official Decky Localsend **0.4.5**, store ID **125**, archive SHA-256
`e271ccb55eb23f918b99a69f79aec905df4c9b2ef7b23bbf4ec1c2d1a6f0c6b7`,
contains an x86_64 helper. Frame Control's proposed managed adaptation accepts
only that exact archive. The original x86 helper and Python backend hashes are
checked before a pinned static ARM64 helper replaces the binary. The headset
transaction checks the ARM64 SHA-256 and ELF machine again. It leaves the
official Python, frontend, manifest and BSD license unchanged, retains
managed update/remove receipts, and rejects changed archive digests.
[Source correspondence and local upstream transfer](framey-localsend-source-match-2026-10-27.md)
records the exact upstream plugin tag and Go commit. The native asset and
build recipe are [bundled](../../ui/vendor/localsend-0.4.5/README.md).
Go dependencies are checksum-pinned but not vendored. The Frame installer
does not download modules or run a compiler.
## Evidence so far
- The host transaction tests installed and removed the exact archive,
preserved its Python/LICENSE bytes, checked the managed receipt and
rejected a changed archive. The ARM64 branch independently wrote and
hashed the AArch64 ELF on x86 without executing it.
- The fake Frame managed install/remove flow passed with the exact official
archive and checked the installed helper digest. Its project containers,
volumes and network were absent after the run.
- The full local Python suite passed **1,669 tests with 16 skipped** after
rerunning with loopback socket access. The first attempt ran inside a
workspace sandbox that denied local test sockets across unrelated server
tests; it did not identify a LocalSend regression.
- A fresh local Chromium opaque iframe initialized the exact official frontend
with backend and network effects denied. Its visible text included Backend is
stopped, Device Name, Available Devices, File Upload, Receive History and
Configuration. The initializer reported ready and called only
`get_receive_history`, `get_backend_status` and `get_backend_config`.
This proves a visible initial panel with safe fallbacks, not working send,
receive or settings controls.
- The original x86 backend and helper completed one private no-route marker
transfer outside Decky. This does not prove the managed ARM64 binary or
actual Decky service.
- The published Decky 3.3.0 AMD64 and ARM64 runtime gate discovered the
adapted plugin and returned its stopped status and safe configuration through
public RPCs. Its helper remained stopped during that gate.
- A locked real Frame probe ran the rebuilt ARM64 helper with SHA-256
`e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae`
in a private network namespace with no default route. Its LocalSend 2.0
endpoint accepted and saved one 38-byte marker, and the saved bytes matched
the sent marker. The headset was in standby (SteamVR activity 3, proximity
raw 4.6875) with 72% battery before and after. Both normal Frame Control
services stayed inactive. The owned helper/process/files and shared lock
were removed. [Machine-readable result](framey-localsend-arm64-transfer-2026-10-27.json).
A useful frontend workflow still needs verification. The ARM64 marker test
exercised the helper in isolation, not the actual Decky plugin service, local
discovery, user-facing transfer, or installation on the headset. No worn
visibility or physical-controller claim follows from this candidate.
The proposed catalog status is **partial** for exact managed archive
installation and settings/status. Steam routes, local discovery, user-facing
transfer, clipboard/screenshot sharing and worn VR remain unverified. The
historical raw-archive audit fields still describe the earlier unadapted x86
ZIP; `managedNative` records the current digest-scoped path separately.
@@ -0,0 +1,55 @@
# Decky Localsend 0.4.5 source match and ARM64 build, 27 October 2026
The exact official store archive (ID 125, SHA-256
`e271ccb55eb23f918b99a69f79aec905df4c9b2ef7b23bbf4ec1c2d1a6f0c6b7`)
remains **blocked** on Frame: it carries an x86_64 `bin/localsend-core`.
No managed plugin install, transfer, or status promotion was performed.
The archive's `main.py` has SHA-256
`db03f9201fe50b75dea99a7a05b54ec3f352d1bdfe174115b0e569154198d8ad`,
identical byte for byte to `main.py` at the upstream Decky plugin tag `v0.45`
(`7c166bdc33e2a667527ffd511eb5b90d3ce0bd67`). That tag pins its Go
submodule to `ad28b8e9440e17e44b4735a6c4aa0512009cf333`. All 42 dependency
versions embedded in the official helper match that source's `go.mod`, which
declares Go 1.25.7.
Building that pinned Go commit with `CGO_ENABLED=0`, `GOOS=linux`,
`GOARCH=amd64`, and the flags `-trimpath -buildvcs=false -ldflags='-s -w'`
produced **byte-identical** official helper bytes: 15,241,400 bytes,
SHA-256 `d448d03861d578685e6c70ea3ace25eb34fca99cb9d4f98bf51ba23aa5cae843`.
This resolves the source correspondence gap left by the earlier build from
the later `66edc509` commit. The same pinned source and flags with
`GOARCH=arm64` produced a static AArch64 ELF of 11,731,128 bytes, SHA-256
`e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae`.
Two ARM64 builds matched byte for byte. The Go core is MIT licensed; the
separate Decky Python plugin is BSD-3-Clause.
A unique-owner atomic `/tmp/frame-test.lock` preflight on the real Frame read
SteamVR activity **1**, proximity raw **88.0625**, and battery **35%**.
Because the headset was active or wearer status was unknown, the probe did
not upload or execute the ARM64 helper, start a LocalSend service, or send
input. The task-owned directory was absent and only the owned lock was
released. This is **not** a worn visual or controller result.
The unmodified plugin backend's `_main` starts a fixed-path Unix notification
socket but does not start the Go network service. Its explicit `start_backend`
method starts the helper with a default all-interface setting, HTTPS and
auto-save disabled. A local follow-up ran that exact Python backend with the
official x86_64 helper in a disposable user/network namespace. A veth pair
provided only documentation-range private addresses and there was no default
route. A private Decky module stub supplied task-owned settings, runtime,
plugin and log directories; its notification socket was moved into that
directory. The backend's `start_backend`, `proxy_get` info request and
`proxy_post` prepare/upload methods passed. One 42-byte public marker
arrived in the private upload directory with independent SHA-256
`346373ca56ec52c3f47b8d41de2a6a8479d8ad10d2891ff3b0dc7ea9c3892a39`.
The backend stopped, its process and private directory were absent, and the
global notification socket was untouched. This verifies the upstream x86
backend/proxy/transfer contract locally, **not** Decky 3.3.0 discovery, ARM64
transfer, an installed Frame plugin or real-network transfer.
Before a managed ARM64 adaptation, verify actual Decky backend discovery/RPC,
the frontend and a private transfer with the exact-source ARM64 helper;
package the source, license and exact helper digest. The official x86_64
archive remains blocked until that work passes. Worn VR and physical
controller use remain unverified.
+308
View File
@@ -0,0 +1,308 @@
{
"date": "2026-10-27",
"scope": "Shared standalone Toggle value prop; no plugin-wide status promotion",
"affected": [
{
"id": 7,
"name": "CSS Loader",
"version": "2.1.3",
"sha256": "99beaf1374af82408e6d056e0b0147e5fca544e1035fa7ab88419231697bc231",
"status": "partial"
},
{
"id": 13,
"name": "Pause Games",
"version": "1.0.3",
"sha256": "4aefdfb0ef25dea9c6d9fd037731c8b99ee8a1be966c2f7d5b0602112ef29f62",
"status": "partial"
},
{
"id": 32,
"name": "AutoFlatpaks",
"version": "1.6.8",
"sha256": "344fce82cfb4800caccb4d6c0ec46e1ad6c9594a622d5baa0bb31286baf2d1a5",
"status": "broken"
},
{
"id": 60,
"name": "DeckyFileServer",
"version": "1.2.0",
"sha256": "7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0",
"status": "broken"
},
{
"id": 64,
"name": "Syncthing",
"version": "0.2.7",
"sha256": "27e47379c280c92253d5533435f57c33b332b215f866d2ac4493daf18ec0d1c4",
"status": "broken"
},
{
"id": 113,
"name": "Decky LSFG-VK",
"version": "0.12.2",
"sha256": "ec59fc8aceb4e1abe2066b3a0707ed6313cabc4421b7c17765397903e2f8fd26",
"status": "partial"
},
{
"id": 116,
"name": "Muon",
"version": "0.2.1",
"sha256": "a7659c79b74110476cc4458cb10530cdcb2539fa11bc8c759038a080eaa81049",
"status": "broken"
},
{
"id": 126,
"name": "Launch Options",
"version": "1.9.2",
"sha256": "381ac8b915402176cf768d1ccc1bc29a6b82ea9255675f0a30c0c81ea5427225",
"status": "broken"
},
{
"id": 129,
"name": "ShadowDeck",
"version": "1.1.11",
"sha256": "2b97b769a49ac2a45f546090fc96d07e09e146020104d708a0efdad990687f8b",
"status": "broken"
}
],
"browser": {
"passed": true,
"opaqueOrigin": "null",
"checks": [
"saved true renders checked",
"standalone Toggle has no empty field wrapper",
"native click writes boolean false and rerenders",
"second click writes boolean true",
"external false update renders unchecked",
"external true update renders checked",
"disabled Toggle stays inert",
"ToggleField checked contract preserved"
]
},
"publishedDeckyRuntime": {
"version": "v3.3.0",
"architecture": "x86_64",
"loaderSha256": "e80d03e641d1766a33a817cb27ab8d2e0a4633685e5d27e4de948a6687526181",
"totalProbes": 8,
"pauseGames": {
"id": 13,
"name": "Pause Games",
"version": "1.0.3",
"sha256": "4aefdfb0ef25dea9c6d9fd037731c8b99ee8a1be966c2f7d5b0602112ef29f62",
"shim": null,
"calls": [
{
"method": "load_settings",
"result": {}
},
{
"method": "save_setting",
"result": null
},
{
"method": "load_settings",
"result": {
"autoPause": true
}
},
{
"method": "save_setting",
"result": null
},
{
"method": "load_settings",
"result": {
"autoPause": false
}
},
{
"method": "save_setting",
"result": null
},
{
"method": "load_settings",
"result": {
"autoPause": true
}
},
{
"method": "add_no_auto_pause_set",
"result": null
},
{
"method": "load_settings",
"result": {
"autoPause": true,
"noAutoPauseSet": [
424242
]
}
},
{
"method": "remove_no_auto_pause_set",
"result": null
},
{
"method": "load_settings",
"result": {
"autoPause": true,
"noAutoPauseSet": []
}
}
],
"discovered": [
{
"name": "Pause Games",
"version": "1.0.3",
"load_type": 1,
"disabled": false
}
],
"result": "discovery and Toggle-backed private settings persistence passed; no process actions"
}
},
"realFrame": {
"timeUtc": "2026-10-27T01:10:45.197435+00:00",
"architecture": "aarch64",
"runtime": {
"version": "v3.3.0",
"sha256": "8ef5dcf36cbbbb6e97201a6baed5d79c4cc44ec27789fc9af1c5b6ae8f0b25b6"
},
"backend": {
"passed": true,
"discovered": [
{
"name": "Pause Games",
"version": "1.0.3",
"load_type": 1,
"disabled": false
}
],
"privateSettingsPersisted": true,
"calls": [
{
"method": "load_settings",
"result": {}
},
{
"method": "save_setting",
"result": null
},
{
"method": "load_settings",
"result": {
"autoPause": true
}
},
{
"method": "save_setting",
"result": null
},
{
"method": "load_settings",
"result": {
"autoPause": false
}
},
{
"method": "save_setting",
"result": null
},
{
"method": "load_settings",
"result": {
"autoPause": true,
"pauseBeforeSuspend": false,
"overlayPause": false,
"noAutoPauseSet": []
}
},
{
"method": "add_no_auto_pause_set",
"result": null
},
{
"method": "load_settings",
"result": {
"autoPause": true,
"pauseBeforeSuspend": false,
"overlayPause": false,
"noAutoPauseSet": [
424242
]
}
},
{
"method": "remove_no_auto_pause_set",
"result": null
},
{
"method": "load_settings",
"result": {
"autoPause": true,
"pauseBeforeSuspend": false,
"overlayPause": false,
"noAutoPauseSet": []
}
}
]
},
"batteryBefore": [
100
],
"batteryAfter": [
100
],
"originalServiceStatesPreserved": true,
"cleanup": {
"baseAbsent": true,
"unitAbsent": true,
"archiveAbsent": true
},
"lockReleased": true,
"independentCleanupAudit": {
"passed": true,
"privateBasesUnitsUploadsAbsent": true,
"privateUnitsNotLoaded": true
}
},
"fakeFrame": {
"blocked": 55,
"installed-removed": 61
},
"python": {
"ran": 1659,
"skipped": 16,
"result": "OK"
},
"unverified": [
"Other importer plugin backends",
"Process pause/resume",
"Automatic pausing/suspend",
"Physical controller/keyboard",
"Worn VR"
],
"affectedSnapshot": "Initial independent verification before merging main b963937; DeckyFileServer is partial in the merged catalog.",
"mergeValidation": {
"mainCommit": "b963937",
"catalog": {
"works": 6,
"partial": 51,
"broken": 48,
"na": 11
},
"python": {
"ran": 1663,
"skipped": 16,
"result": "OK"
},
"publishedDeckyRuntime": {
"version": "v3.3.0",
"architecture": "x86_64",
"probes": 10,
"result": "All passed, including Pause Games and DeckyFileServer positive/negative cases"
},
"hardwareRerun": false
}
}
+80
View File
@@ -0,0 +1,80 @@
# Shared standalone Toggle contract — 2026-10-27
Framey previously exported `DFL.Toggle` as `ToggleField`. Official standalone
toggles pass `value`, while ToggleField takes `checked`. A saved true setting
therefore appeared unchecked, and clicking it could send true again instead of
turning it off. Toggle now renders a standalone controlled checkbox from
`value`; ToggleField retains its `checked` contract. Both emit booleans and keep
disabled controls inert. No official archive or manifest is rewritten.
Nine exact catalog archives use this standalone contract: CSS Loader (7),
Pause Games (13), AutoFlatpaks (32), DeckyFileServer (60), Syncthing (64),
Decky LSFG-VK (113), Muon (116), Launch Options (126), and ShadowDeck (129).
These are affected API consumers, **not nine verified working plugins**.
Their existing root, native, Steam UI and runtime blockers still apply.
The catalog records this repair without promoting any status.
## Verification
- The Node regression fails against the old alias: true renders false. It
passes with the fix and checks true/false callbacks, disabled behavior and
the unchanged ToggleField contract.
- The pinned, unmodified Pause Games 1.0.3 frontend component renders both
persisted exclusion-list states and dispatches only its expected add/remove
settings callbacks. This VM fixture does not run effects or process actions.
- Local Chromium, React 18.3.1 and the production host in an opaque sandboxed
iframe pass eight DOM checks: saved true, standalone shape, false/true native
clicks and rerenders, external false/true updates, disabled input, and
ToggleField. This is a control fixture, not nine end-to-end plugin tests.
- Actual published Decky 3.3.0 on native amd64 discovers the exact Pause Games
archive and uses public API1 instance RPC to save/read true/false settings,
add/remove a synthetic exclusion and independently read its private settings
file. This is a permanent runtime probe alongside existing backend shims;
all eight probes, including historical negative cases, pass. Hosted CI also
runs this gate on native ARM64; local ARM64 evidence below is from the Frame.
- Under the shared headset lock, a bounded, removable private Decky 3.3.0 ARM64
user service repeats discovery and those safe settings operations through
the normal Steam CEF DeckyBackend bridge. The private file readback matches.
The harness never requests pause/resume/kill or suspend operations. Steam's
Decky frontend also populated its normal default setting keys during this
run; no running-game behavior is claimed.
- Fake-Frame audit: all 116 archives attempted, 61 installed/removed and 55
blocked by existing policy. Full Python suite: 1,659 tests, 16 skipped, OK.
The exact Pause Games archive SHA-256 is
`4aefdfb0ef25dea9c6d9fd037731c8b99ee8a1be966c2f7d5b0602112ef29f62`.
The official store refresh still contains 116 entries; all pinned IDs, versions
and digests are unchanged.
[Structured results](framey-toggle-2026-10-27.json) include scoped readbacks and
the nine version/digest pairs.
## Probe retries and cleanup
The first hardware adapter connected directly to Decky's single WebSocket and
reused a request ID also used by Steam CEF; it received an unrelated cached
reply. A second adapter retained a CEF connection across Decky's startup reload
and disconnected. Neither is a successful plugin verification. Both private
services and files were removed. The successful adapter waits for startup and
reattaches CEF during discovery, then serializes settings calls through the
normal DeckyBackend bridge.
All private services, bases and uploaded archives were removed, and the shared
lock was released. An independent locked audit confirms no matching unit is
loaded and no private base, unit file or upload remains. Original managed Decky
and panel states were preserved. Battery stayed 100 → 100 during the successful
run; no power setting changed.
No plugin gains a works claim. Real process control, other importer backends,
physical input and worn VR remain unverified. The original independent branch
had **6 works / 50 partial / 49 broken / 11 N/A**. After merging main's landed
DeckyFileServer ARM64 support, current totals are **6 works / 51 partial /
48 broken / 11 N/A**. Its managed-native receipt guard and both runtime probes
are preserved; its former Toggle display limitation is removed from the current
catalog reason. The initial verification counts above describe the original
branch; combined validation is recorded below.
Combined merge validation against main `b963937`: full Python suite passes
1,663 tests with 16 skips; all ten actual published Decky 3.3.0 native amd64
probes pass, including Pause Games settings and DeckyFileServer positive/negative
cases. Generated documentation matches the merged catalog. No hardware rerun
was needed for these fixture/catalog conflicts.
@@ -0,0 +1,76 @@
{
"tag": "v0.4.50",
"releaseId": 424993367,
"tagNameAtAudit": "v0.4.50",
"draftAtAudit": true,
"sha256sumsSha256": "85383104a8f08f49ffde24ac867f9c2ead41951e602600a9d22d58bc19bf06f5",
"assets": [
{
"name": "Frame-Control-mac-arm64.dmg",
"size": 173090558,
"sha256": "d402924394725bac2b28bef17c7945e2f7e4c3093227eace78a42ed80e864be9",
"udifTrailer": "ok"
},
{
"name": "Frame-Control-mac-arm64.zip",
"size": 172911785,
"sha256": "2583937fefdc315706018c1bf794b916726ee55b461e321c3334df21245a110d",
"zipCrc": "ok",
"members": 1666,
"bundleVersion": "0.4.50"
},
{
"name": "Frame-Control-mac-x64.dmg",
"size": 180139223,
"sha256": "2128c14b15dd1a2a59790c43d42640a25185562056796a3437910d6f88f996c8",
"udifTrailer": "ok"
},
{
"name": "Frame-Control-mac-x64.zip",
"size": 179998207,
"sha256": "ed684023ff087d6741e88d4ca4f8a47e67bec07255fc73fbd18a6bcaa42d14fe",
"zipCrc": "ok",
"members": 1662,
"bundleVersion": "0.4.50"
},
{
"name": "Frame-Control-Setup-x64.exe",
"size": 147348168,
"sha256": "7c46fcc759cefb9060e1dc2fce39682f6dad306f3666805f77ce707e16ef3e2c",
"peMachine": 332
},
{
"name": "Frame-Control-win-x64.zip",
"size": 194302082,
"sha256": "57e71304bae96dc8aab6b845a64c27ac428931d02bb41ae60b33b19fcd11193f",
"zipCrc": "ok",
"members": 1510
},
{
"name": "Frame-Control-linux-x86_64.AppImage",
"size": 175453095,
"sha256": "d2196b9a50d0777203e60721dd19c6edbb5c18a7639148f0a496d420e231b053",
"elfMachine": 62
},
{
"name": "Frame-Control-linux-arm64.AppImage",
"size": 167036625,
"sha256": "49370f9c27931bf6c28676d627dc34553550b544ab38b7f4be8e8f6dbc9b7c4b",
"elfMachine": 183
},
{
"name": "Frame-Control-linux-amd64.deb",
"size": 142209572,
"sha256": "430d057f234557f5f9dbb3e2cff3469829d041d0d7be191e70e6ef7de6663256",
"debVersion": "0.4.50",
"architecture": "amd64"
},
{
"name": "Frame-Control-linux-arm64.deb",
"size": 129291236,
"sha256": "6403f8caafcdd5832415547e146ce3886cf7358e0c3a8898f4ee6e9a0a7b05ea",
"debVersion": "0.4.50",
"architecture": "arm64"
}
]
}
@@ -0,0 +1,52 @@
# v0.4.50 candidate gate, 27 October 2026
The version bump at `a160043` follows the merged Windows panel-response repair
[#356](https://github.com/saphid/frame-control/pull/356) and the Framey changes
[#353](https://github.com/saphid/frame-control/pull/353),
[#354](https://github.com/saphid/frame-control/pull/354),
[#355](https://github.com/saphid/frame-control/pull/355),
[#357](https://github.com/saphid/frame-control/pull/357),
[#358](https://github.com/saphid/frame-control/pull/358), and
[#359](https://github.com/saphid/frame-control/pull/359).
The exact DeckyFileServer catalog entry is **partial**; the shared Toggle fix
does not promote any plugin status. Worn VR and physical controller behavior
remain unverified.
## Locked real Frame smoke
Early `scripts/frame-smoke.sh` attempts exited before any headset read or test
action because lxso2's Tailnet route was down and lxso1 lacked the authorized
SSH key and pinned host record. No lock was taken by those attempts. The final
run kept lxso2's existing private key and pinned Frame host key end to end,
using lxso1 only as a TCP relay. A task-owned localhost tunnel reached the
devkit endpoint after the runner acquired `/tmp/frame-test.lock`; no global
SSH, network or security setting was changed.
At **02:25:14 UTC**, `scripts/frame-smoke.sh` ran from candidate commit
`fcae5d6` on Frame OS **0.5.6**, build `20261022.6153909`. Its locked
read-only preflight found SteamVR activity **3 (standby)**, proximity raw
**3.25** and battery **100%**. The result was **12 passed / one optional
x86_64 launch skipped / zero failed**. ARM64 and Windows/Proton test titles
installed, launched and were removed. The x86_64 title installed and was
removed, but Steam Linux Runtime 4 was absent, so no x86_64 launch is claimed.
Both the initial leftover sweep and final owned cleanup passed; the runner
released only its lock. The private result is
`/tmp/frame-control-v0450-smoke-results/2026-10-27T022514Z-20261022.6153909.json`
(SHA-256 `7b8d88aafb4459bf882edeb10f65c021c5aa25ad9ed2755faf04e3b4a95c1632`).
A separate read-only audit acquired a fresh unique lock after the smoke. It
found activity **3**, proximity raw **3.25**, battery **100%**, no exact reserved
`fc_smoke_*` or `fc-smoke-*` title directories or metadata, and no processes
owned by those paths. That audit released its own lock. The local tunnel was
stopped; the isolated lxso1 checkout and temporary SSH files were removed.
There was no wearer confirmation, wake command or worn interaction test.
## Remaining release gates
The candidate needs green exact-head PR CI, e2e, native amd64/ARM64 Decky
runtime checks and all four installer previews. After the candidate merges,
its exact main commit needs green CI and e2e. The tagged release then needs
every installer build, ten assets, matching checksums, release verification,
independent package checks and publisher dry run. A failed, cancelled, skipped,
pending or unverified release gate holds publication. The current published
release remains v0.4.49.
+75
View File
@@ -0,0 +1,75 @@
# v0.4.50 tagged release verification, 27 October 2026
The annotated `v0.4.50` tag dereferences to merged main commit
`2797212d8c95311deb8ba73043d0f90ba10bc962`. Its tree exactly matched
the reviewed [candidate PR #360](https://github.com/saphid/frame-control/pull/360)
head `dab13fa8763b4f836cd49d398d152124655ea8dc`. The candidate includes
the Framey and Windows changes listed in the public release notes and the
publisher gate that checks every workflow run attempt.
## Exact commit and tagged build gates
- [Main checks](https://github.com/saphid/frame-control/actions/runs/40440862876)
completed successfully on the tag commit: seven of seven jobs, including
e2e, iOS, Windows, macOS, Linux and the aggregate gate.
- [Native Decky runtime](https://github.com/saphid/frame-control/actions/runs/40440862867)
completed successfully on that commit: AMD64 and ARM64 actual Decky 3.3.0
discovery and safe probes.
- An exact-main [installer preview](https://github.com/saphid/frame-control/actions/runs/40441532929)
built Windows, Linux, Apple Silicon Mac and Intel Mac installers successfully
before tagging. Its draft and checksum jobs were inapplicable to the manual
preview and skipped by their tag-only conditions; they were not release gates.
- The [exact-tag release workflow](https://github.com/saphid/frame-control/actions/runs/40442228066)
then passed all six jobs on its first attempt: draft, four platform builds,
and checksums. No tagged job failed, was cancelled, skipped or overridden.
The draft contained exactly ten installer assets and `SHA256SUMS` before
publication. Every installer was independently downloaded by GitHub asset ID,
one file at a time. Its downloaded size and SHA-256 matched GitHub's asset
metadata and the downloaded `SHA256SUMS`; the latter's SHA-256 was
`85383104a8f08f49ffde24ac867f9c2ead41951e602600a9d22d58bc19bf06f5`.
The [sanitized audit result](v0450-assets-2026-10-27.json) records all ten
names, sizes, hashes and package checks. The audit also checked both Mac ZIP
CRCs and bundle version `0.4.50`, both DMG UDIF trailers, the Windows ZIP
CRC and PE installer header, both AppImage ELF architectures, and both DEB
version and architecture fields. The Windows NSIS installer itself has a
32-bit PE launcher; tagged Windows CI installed its x64 payload and checked
packaged resources and Python libraries.
The independently downloaded Linux x86_64 AppImage had SHA-256
`d2196b9a50d0777203e60721dd19c6edbb5c18a7639148f0a496d420e231b053`.
Its first local launch exited inside the workspace sandbox before the server
answered. The same bytes then passed outside that sandbox under isolated Xvfb:
HTTP 200 with `FrameControl/1`, followed by a stopped server. The launcher
used a temporary profile and disabled telemetry and update checks. Tagged
Linux CI also launched its own built AppImage. No physical Mac or Windows
launch of these exact installers was performed.
## Real Frame and publication
The [candidate evidence](v0450-candidate-2026-10-27.md) records a unique-lock
real Frame smoke on the version-bumped source: **12 passed, one optional
x86_64 launch skipped, zero failed**. Steam Linux Runtime 4 was absent, so no
x86_64 launch is claimed. A separate fresh-lock cleanup audit found no owned
test title directories, metadata or processes; both locks were released.
SteamVR activity was 3 (standby), proximity raw 3.25 and battery 100%.
There was no wearer confirmation, wake command or worn interaction test.
The private smoke result's SHA-256 is
`7b8d88aafb4459bf882edeb10f65c021c5aa25ad9ed2755faf04e3b4a95c1632`.
`scripts/publish-release.sh --dry-run v0.4.50` passed the exact main,
native runtime and tagged release jobs, all ten installer hashes, regenerated
`SHA256SUMS`, draft notes and update-manifest checks. The real publisher
repeated those gates, attached `update.json`, and published
[v0.4.50](https://github.com/saphid/frame-control/releases/tag/v0.4.50) at
**03:19:21 UTC**. GitHub's latest-release API returned `draft=false`,
`prerelease=false`, ten installers, `SHA256SUMS` and `update.json`. A fresh
public latest-download of `update.json` reported version `0.4.50`, the
correct release page, ten hashes matching the independent downloads, and the
worn-headset limit in its notes.
The exact-version official Decky catalog is **6 works / 51 partial /
48 broken / 11 N/A**. These statuses have the scoped evidence in
[the compatibility table](../framey-compatibility.md). Worn headset
visibility, physical controller input, VR keyboard and optical comfort remain
unverified. The hourly shared-lock worn check remains enabled.
@@ -0,0 +1,53 @@
# v0.4.51 candidate gate, 27 October 2026
The source candidate `91cf774` follows green main merge commit
`560f3de5631ce2dca641c68a5cba7b2dd1d2660f`. The main
[checks](https://github.com/saphid/frame-control/actions/runs/40450100545)
and [native Decky runtime](https://github.com/saphid/frame-control/actions/runs/40450100462)
passed on that exact merge commit. This candidate bumps the app and lockfile
to 0.4.51 and requires standby-range proximity before the release smoke can
change test titles.
Since v0.4.50, [#362](https://github.com/saphid/frame-control/pull/362)
recorded the pinned official LocalSend source match, and
[#363](https://github.com/saphid/frame-control/pull/363) added an exact-digest
managed ARM64 helper, desktop/headset packaging, and a scoped **partial**
compatibility entry. Its PR head passed e2e, native Decky AMD64/ARM64, and all
four installer previews. The catalog is **6 works / 52 partial / 47 broken /
11 N/A**. A private no-route ARM64 helper marker transfer passed on the real
Frame, but the managed Decky service on Frame, user-facing transfer, local
discovery, Steam routes, worn VR and physical controllers remain unverified.
## Locked real Frame smoke
At 04:33:21 UTC, `scripts/frame-smoke.sh` ran from source candidate `91cf774`
with a unique-owner atomic `/tmp/frame-test.lock`. Its read-only preflight
found Frame OS 0.5.6 build `20261022.6153909`, SteamVR activity **3
(standby)**, proximity raw **4.6875**, and battery **88%**. It passed **12
steps**, skipped one optional x86_64 launch, and failed **zero**. ARM64 and
Windows/Proton test titles installed, launched and were removed. The x86_64
title installed and was removed; Steam Linux Runtime 4 was absent, so no
x86_64 launch is claimed. The runner's owned cleanup passed and it released
only its lock. The private result is
`/tmp/frame-control-v0451-smoke-results/2026-10-27T043321Z-20261022.6153909.json`
(SHA-256 `c2b9611e072fd08109c31f4aa05a4ba6b93de82f532a7d87a4a1c6dcd52024a3`).
A separate read-only audit acquired a fresh unique lock after the smoke. It
found activity **3**, proximity raw **4.6875**, battery **91%** while charging,
no exact reserved smoke title files or metadata, and no processes owned by
those title paths. It released its own lock. The private audit result is
`/tmp/frame-control-v0451-cleanup-audit.json` (SHA-256
`3953072bb5457f4fef2cb766d83f65f844896fc7893d2479a166cb9325f72b48`).
No power setting or security state was changed. No wearer was confirmed and no
worn panel or controller interaction was tested; the hourly worn check remains
enabled.
## Remaining release gates
The candidate needs green exact-head PR CI, e2e, native Decky runtime and all
four installer previews. After the candidate merges, its exact main commit
needs green CI and e2e. The tagged release then needs every installer build,
ten assets, matching checksums, release verification, independent package
checks and publisher dry run. A failed, cancelled, skipped, pending or
unverified release gate holds publication. The current published release
remains v0.4.50.
+6 -2
View File
@@ -64,7 +64,8 @@ none of these frontend controls has passed a worn-headset check yet.
| `PanelSection`, rows, buttons, toggles, dropdowns | Local touch-sized controls; disabled controls stay inert during a backend call | Local browser and host unit test |
| `TextField`, `SliderField`, grouped `Dropdown` options | Native text, range and select controls in the panel; disabled state, numeric slider callback and basic input types | Host unit test; worn VR use unverified |
| Scrollable panel content | Visible Scroll plugin up/down buttons move the last region reached by a trusted pointer, or the document if that region disappears. Hold either button to repeat; keyboard activation still moves one step. Plugins may use ordinary CSS overflow containers. | Parent/iframe message tests and a real Frame synthetic OpenVR pointer hold; physical controller scroll and hold remain unverified |
| `Tabs`, `SidebarNavigation`, spinners, dialog/menu wrappers, progress bars | Local tab navigation and ordinary HTML controls; no Steam navigation or controller focus tree | Host tests and catalog frontend smoke checks in local Chromium |
| `Tabs`, `SidebarNavigation`, spinners, dialog/menu wrappers | Local tab navigation and ordinary HTML controls; no Steam navigation or controller focus tree | Host tests and catalog frontend smoke checks in local Chromium |
| `ProgressBar`, `ProgressBarItem`, `ProgressBarWithInfo` | Native progress control; `indeterminate` omits its value so it animates as unfinished, and `sOperationText` appears with the bar. Estimated completion time is not shown. | Host contract tests; official plugin progress behavior in worn VR remains unverified. |
| `SteamUIStore` / `DFL.Router.RunningApps`, `MainRunningApp` | Bounded detached Steam CEF reads while the panel is open; refresh about every two seconds; no MobX observation. Failed reads clear stale context. | Locked real Frame empty-list reads; nonempty local Chromium fixtures. Real running-game selection unverified. |
| `GameSessions.RegisterForAppLifetimeNotifications` | Actual Steam callbacks, bounded queue and disposable `.unregister()` handle; no events inferred from snapshot changes; lease cleanup after lost panels | Real Frame registration/poll/close; launch/exit, retry, expiry and duplicate tests use explicit fixtures. Real game transitions unverified. |
| Other `SteamClient` subscriptions | Explicit inert registration methods return disposable handles; no fabricated power/input/game-action events. Mutating Steam/power methods are unavailable. | Host contract tests; safe fallbacks only |
@@ -76,7 +77,7 @@ none of these frontend controls has passed a worn-headset check yet.
| `localStorage` | Synchronous, per-plugin cache backed by the Frame Control panel origin. Up to 64 keys, 64 KiB per value and 128 KiB total; `clear()` removes only this plugin's cache. Falls back to memory if panel storage is denied. | Parent/iframe round-trip and reopen in the local host test; real headset persistence remains unverified. |
| `SteamClient.Storage.GetJSON` / `SetObject` | Uses the same bounded per-plugin cache; a missing key rejects with `Not found`. It cannot read Steam account or global storage. | Host contract tests and local Chromium settings views; headset persistence after reopen remains unverified. |
| IndexedDB / localForage | IndexedDB is deliberately absent inside the opaque iframe so feature detection chooses the bounded localStorage driver. Plugins requiring IndexedDB directly remain unsupported. | HLTB official frontend in local Chromium: its previous `IDBFactory.open` SecurityError disappeared. No live HLTB data request was run. |
| `routerHook.addRoute` / `removeRoute`, `Navigation.Navigate` / `NavigateBack`, `useParams` | Local routes inside this plugin panel, with named segments such as `/plugin/game/:appid/:view?`; `useParams()` reads the current segment values | Host unit test; VR navigation unverified |
| `routerHook.addRoute` / `removeRoute`, `Navigation.Navigate` / `NavigateBack`, `useParams` | Local routes inside this plugin panel, with named segments such as `/plugin/game/:appid/:view?`; `useParams()` reads the current segment values. Back walks up to 32 local page transitions and skips routes removed since navigation. | Host unit test; VR navigation unverified |
| `routerHook.addPatch`, global components | No-op; cannot patch Steam routes | Not applicable |
| `findModule`, `findModuleChild`, `findModuleExport`, class lookup and module mapping | Searches a finite registry of Framey controls and CSS maps. Unknown Steam modules return `undefined` or an empty mapping. The optional UI-audio export cannot play sound. | Host contract tests; pinned TabMaster 2.15.1 QAM and documentation render in local Chromium. Steam library editing and worn VR remain unverified. |
| `Navigation.CloseSideMenus` | Returns to this plugin's main panel | Pinned HLTB for Deck 2.0.12 shallow settings view in local host test. Live HLTB search remains unverified. |
@@ -90,6 +91,9 @@ Frame Control store; only paths registered by that same plugin can navigate.
Optional route segments must be at the end. Framey accepts local path segments
only; query strings, external URLs, Steam library routes and duplicate parameter
names are rejected. Removing an active route returns to the plugin panel.
`Navigation.NavigateBack()` returns to the preceding local page, then the
plugin root. `Navigation.CloseSideMenus()` returns directly to the root and
clears that panel's history. Neither operation navigates Steam's own UI.
Toasts stay inside the panel, so keep essential errors in the plugin UI too.
Treat module lookup results as Framey-local exports. Do not depend on Steam
webpack IDs, function-source signatures, or Steam-only CSS class names; a
+15 -15
View File
@@ -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: 6 · Partial: 51 · Broken: 48 · N/A: 11.**
**Works: 6 · Partial: 52 · Broken: 47 · 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. That initial pass left Simple Timer 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, RSSLine 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. That initial pass left Simple Timer 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, RSSLine truncated
## Every current plugin
@@ -38,11 +38,11 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| Plugin / version | Manifest / bundle API | Status | Reason | Verified by |
| --- | --- | --- | --- | --- |
| 1: XR Gaming 1.6.2 | 1 / 2 | N/A | Not supported on Frame: Requires supported external XR glasses and the Breezy/XRLinuxDriver stack; it does not target Steam Frame displays. | static archive, local browser, fake-Frame |
| 7: CSS Loader 2.1.3 | legacy / legacy | partial | Panel initialization succeeds; CSS themes patch Steam UI and cannot theme the isolated VR shell. | static archive, local browser, fake-Frame |
| 7: CSS Loader 2.1.3 | legacy / legacy | partial | Panel initialization succeeds; CSS themes patch Steam UI and cannot theme the isolated VR shell. Shared Toggle value rendering is repaired; other listed blockers remain. | 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 | 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 |
| 13: Pause Games 1.0.3 | 1 / 2 | partial | Settings and a synthetic running-game list render locally. Actual Decky 3.3.0 discovery, private settings writes/readback and exclusion-list persistence pass on amd64 and ARM64 Frame; standalone Toggle now honors its value prop. Live empty-list/lifetime checks passed earlier. Real process pause/resume, automatic pausing, suspend integration and worn VR remain unverified/unsupported. | static archive, local browser, fake-Frame, real Frame SSH |
| 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 |
| 18: Bluetooth 3.1.1 | 1 / 2 | partial | Controls render with fixture replies; actual Bluetooth discovery, labels and connections remain unverified. | static archive, local browser, fake-Frame |
@@ -55,7 +55,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 27: AutoSuspend 2.2.0 | legacy / legacy | broken | Frontend audit failed: Cannot read properties of null (reading '6'). | static archive, local browser, fake-Frame |
| 28: Emuchievements 2.0.5 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: Emuchievements/bin/hash. | static archive, local browser, fake-Frame |
| 30: TunnelDeck 1.0.4 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
| 32: AutoFlatpaks 1.6.8 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
| 32: AutoFlatpaks 1.6.8 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. Shared Toggle value rendering is repaired; other listed blockers remain. | static archive, local browser, fake-Frame |
| 35: MoonDeck 1.13.0 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: moondeck/python/externals/yarl/_quoting_c.cpython-313-x86_64-linux-gnu.so. Frontend initializes; native CPython wheels and executables in the published archive are x86_64. | static archive, local browser, fake-Frame |
| 36: SteamGridDB 1.8.1 | 1 / 2 | broken | Local module mapping is available; frontend still requires the Steam LibraryContextMenu implementation, which has no target in the isolated Framey panel. | static archive, local browser, fake-Frame |
| 37: Discord Status 1.5.0 | 1 / 2 | partial | Disconnected panel renders; running-activity construction accepts a synthetic game in local Chromium. Exact official is_connected returned false in real Decky 3.3.0 ARM64. Empty live running-list reads and lifetime registration/cleanup passed. Real game transitions, Discord shortcut detection/connection/activity, local custom artwork and worn VR remain unverified/unsupported. | static archive, local browser, fake-Frame, real Frame SSH |
@@ -72,11 +72,11 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 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 |
| 60: DeckyFileServer 1.2.0 | 1 / 2 | partial | Managed install supplies a native ARM64 helper and removes the exact archive's unnecessary root request. Real Decky 3.3.0 on Frame passes private read-only HTTPS listing/download and upload denial. Frontend address/picker and thumbnail-toggle display limitations remain; use HTTPS and a preconfigured directory. Uploads, generated thumbnails and worn VR are unverified. | static archive, local browser, fake-Frame, real Decky 3.3.0 amd64, real Frame SSH |
| 60: DeckyFileServer 1.2.0 | 1 / 2 | partial | Managed install supplies a native ARM64 helper and removes the exact archive's unnecessary root request. Real Decky 3.3.0 on Frame passes private read-only HTTPS listing/download and upload denial. Frontend address/picker limitations remain; use HTTPS and a preconfigured directory. Uploads, generated thumbnails and worn VR are unverified. Shared Toggle value rendering is repaired; other listed blockers remain. | static archive, local browser, fake-Frame, real Decky 3.3.0 amd64, real Frame SSH |
| 61: TabMaster 2.15.1 | 1 / 2 | partial | Local Chromium rendered the QAM and navigable Overview/Filters documentation with an actual Decky 3.3.0 amd64 backend. Library-tab editing remains unavailable: Steam login/library patches and MobX reactions have no target. ARM64 and worn VR unverified. | static archive, local browser, fake-Frame |
| 62: TS3 QuickAccess 1.2.0 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: ts3-qs4sd/bin/ts3-qs4sd.so. | static archive, local browser, fake-Frame |
| 63: Free Loader 1.6.0 | 1 / 2 | partial | The exact archive's duplicate names contain identical bytes and differ only in execute bits; the guarded installer now accepts them, and a private local install/remove transaction passed. The current opaque Chromium host renders Free Games but cannot load deals with backend/network denied. Exact official Python settings and empty local-deals methods passed in a no-network container with a Decky stub. Real Decky, feed refresh, notifications and worn VR remain unverified. | static archive, local browser, fake-Frame pinned install/remove, local exact-archive transaction, isolated Python backend |
| 64: Syncthing 0.2.7 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: decky-syncthing/bin/decky-syncthing-watchdog. | static archive, local browser, fake-Frame |
| 64: Syncthing 0.2.7 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: decky-syncthing/bin/decky-syncthing-watchdog. Shared Toggle value rendering is repaired; other listed blockers remain. | static archive, local browser, fake-Frame |
| 65: Decky Terminal 0.5.3 | 1 / 2 | partial | Local routes initialize after parameter-route support; no shell commands were run. | static archive, local browser, fake-Frame |
| 66: PlayTime 3.2.5 | 1 / 2 | partial | Statistics and routed settings render with a detached real Frame library/user snapshot and actual Decky 3.3.0 amd64 backend in isolation. User selection and empty statistics reads pass. Running snapshots are now exposed, but its private MobX observation is unsupported; automatic recording and ARM64 backend remain unverified. | static archive, local browser, fake-Frame, real Frame SSH |
| 67: Battery Tracker 0.2.0 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
@@ -121,10 +121,10 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 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 | 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 |
| 113: Decky LSFG-VK 0.12.2 | 1 / 2 | partial | Initializer succeeds; runtime LSFG/Windows DLL downloads and Vulkan integration are not ARM64-validated. Shared Toggle value rendering is repaired; other listed blockers remain. | static archive, local browser, fake-Frame |
| 114: Network Sentinel 1.0.4 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
| 115: Decky Clash 1.3.0 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
| 116: Muon 0.2.1 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
| 116: Muon 0.2.1 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. Shared Toggle value rendering is repaired; other listed blockers remain. | static archive, local browser, fake-Frame |
| 117: DeckyParty File Manager 1.0.0 | legacy / legacy | partial | Initializer succeeds; privileged file operations and backend are unverified. No file changes were run. | static archive, local browser, fake-Frame |
| 118: Speed Test 1.0.2 | 1 / 2 | partial | Initializer succeeds; automatic bandwidth tests were blocked by the audit network policy. | static archive, local browser, fake-Frame |
| 119: Decky Clipboard 1.0.1 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-clipboard/bin/xclip. | static archive, local browser, fake-Frame |
@@ -132,11 +132,11 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 121: DeckPass 1.1.1 | 1 / 2 | partial | Initializer succeeds; KeePass Flatpak discovery and secrets integration were not exercised. | static archive, local browser, fake-Frame |
| 123: Steam Status 1.2.3 | 2 / 2 | partial | Settings/public status rendered locally; cache errors fixed. Read-only get_settings RPC returned actual Decky 3.3.0 defaults on ARM64 Frame. Persistence, live backend diagnostics and VR controls unverified. | static archive, local browser, fake-Frame, real Frame SSH |
| 124: AlarMe 1.6.3 | 1 / 2 | partial | Silent labeled timer create/ticks/pause/cancel, preset save/cancel-confirm/delete and stopwatch start/lap/pause/reset/reopen pass in local Chromium with the official Decky 3.3.0 ARM64 backend. Audio, alarms/reminders, suspend inhibition, Steam overlays and worn VR remain unverified; notifications require the panel to stay open. | static archive, local browser, fake-Frame, real Frame SSH |
| 125: Decky Localsend 0.4.5 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-localsend/bin/localsend-core. | static archive, local browser, fake-Frame |
| 126: Launch Options 1.9.2 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-launch-options/bin/hello. Frontend audit failed: Cannot convert undefined or null to object. | static archive, local browser, fake-Frame |
| 125: Decky Localsend 0.4.5 | 1 / 2 | partial | Exact managed 0.4.5 archive replaces its x86 helper with a pinned ARM64 build. Published Decky 3.3.0 ARM64 stopped status/configuration RPCs and a private real-Frame ARM64 helper marker transfer passed. The managed service, user-facing discovery/transfer, Steam routes and worn VR remain unverified. | exact archive and source hashes, local Chromium initial panel, host managed transaction, fake Frame managed install/remove, private x86 backend transfer, published Decky 3.3.0 AMD64/ARM64 stopped status/configuration RPCs, private real Frame ARM64 helper marker transfer |
| 126: Launch Options 1.9.2 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-launch-options/bin/hello. Frontend audit failed: Cannot convert undefined or null to object. Shared Toggle value rendering is repaired; other listed blockers remain. | static archive, local browser, fake-Frame |
| 127: Decky Translator 0.9.0 | 1 / 2 | partial | Initializer succeeds; x86_64 download and hidraw/controller assumptions need an ARM64 audit. OCR/translation not exercised. | static archive, local browser, fake-Frame |
| 128: Quick User Switcher 1.0.0 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
| 129: ShadowDeck 1.1.11 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
| 129: ShadowDeck 1.1.11 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. Shared Toggle value rendering is repaired; other listed blockers remain. | static archive, local browser, fake-Frame |
| 130: Games Prefix Manager 1.0.1 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
| 131: Download All 1.1.0 | 1 / 2 | broken | Initializer succeeds; Steam download registration is absent, so the primary action is unavailable. | static archive, local browser, fake-Frame |
| 132: SuggestMe 1.5.3 | 1 / 2 | partial | Initializer succeeds; recommendation backend and Steam library data unverified. | static archive, local browser, fake-Frame |
@@ -279,11 +279,11 @@ DFL includes exports imported through decky-frontend-lib / @decky/ui. Steam stor
## Native payload portability
All published Linux native payloads in this snapshot are x86_64; no published aarch64 payload was found. The three Windows DLLs belong to Decky-Framegen. Except for the exact managed DeckyFileServer adaptation below, the installer still rejects these archives. Apart from LocalSend and DeckyFileServer, ARM64 builds were not attempted: the constraints below remain unresolved.
All published Linux native payloads in this snapshot are x86_64; no published aarch64 payload was found. The three Windows DLLs belong to Decky-Framegen. Except for the exact managed DeckyFileServer and Decky Localsend adaptations below, the installer still rejects these archives. Apart from LocalSend and DeckyFileServer, ARM64 builds were not attempted: the constraints below remain unresolved.
A [DeckyFileServer ARM64 follow-up](evidence/framey-fileserver-arm64-2026-10-27.md) ships an offline, digest-checked GPL helper build, corresponding vendored source and a build recipe. Only its exact managed archive can replace the x86 helper and remove its unnecessary root flag. Actual Decky 3.3.0 discovery/RPC and real Frame HTTPS private-fixture listing/download pass; uploads stay disabled. Hostname-DNS failures and empty optional arguments are repaired. Decky 3.3.0 still exposes `decky.HOME`; that call is unchanged. The plugin moves from broken to partial: its frontend advertises HTTP/steamdeck URLs and needs a Steam folder picker. Use HTTPS and a preconfigured directory. Uploads, generated thumbnails and worn VR remain unverified.
[LocalSend ARM64 build/runtime evidence](evidence/framey-localsend-arm64-2026-10-26.md) covers a reproducible MIT helper build and its isolated real-Frame protocol 2.0 info response. The official x86 archive remains blocked; full plugin integration and file transfers are unverified.
[LocalSend ARM64 build/runtime evidence](evidence/framey-localsend-arm64-2026-10-26.md) covers an isolated real-Frame protocol 2.0 info response from an earlier helper build. [Exact official source matching](evidence/framey-localsend-source-match-2026-10-27.md) now identifies the official helper source and a reproducible ARM64 build from that revision. The exact official x86 backend passed one local private-namespace transfer, and the rebuilt ARM64 helper passed one locked real-Frame private marker transfer. Published Decky 3.3.0 AMD64/ARM64 runtime checks discovered the adapted plugin and read its stopped status and configuration. Only the exact managed 0.4.5 archive can replace its x86 helper on Frame. The actual managed Decky service on Frame, user-facing transfer, local discovery and worn VR remain unverified ([managed adaptation evidence](evidence/framey-localsend-managed-2026-10-27.md)).
| Plugin | Native payloads | ARM64 result / remaining blocker |
| --- | --- | --- |
@@ -308,7 +308,7 @@ A [DeckyFileServer ARM64 follow-up](evidence/framey-fileserver-arm64-2026-10-27.
| HueSync | bin/hello, py_modules/portio.so | portio and handheld LED backends depend on x86 I/O/unsupported hardware; ARM64 compilation alone is insufficient. |
| Decky Clipboard | bin/xclip | xclip needs ARM64 X11 libraries/server; root and gamescope assumptions also block this archive. |
| Reroll | bin/memscan.so | memscan.so needs ARM64 process-memory support and ABI testing; root requirement remains. Frontend initializes but no memory access was tested. |
| Decky Localsend | bin/localsend-core | MIT LocalSend-Go 66edc509a28b9f877e7374504892dcbc653a4da4 reproducibly cross-built with Go 1.25.7/CGO_ENABLED=0 for ARM64; SHA-256 e79238f6051ce34f172f765ae918d3d902b819e27377989a55e5b1eba10e46f9. Help/official backend CLI flags and protocol 2.0 info endpoint passed on real Frame in an isolated network namespace. No file transfer or full plugin/CEF integration tested; exact source match to shipped helper unproven. Official ZIP stays x86_64 and blocked. |
| Decky Localsend | bin/localsend-core | Exact official x86 helper is reproducible from pinned MIT source; the managed installer replaces only the exact 0.4.5 digest with SHA-256 e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae on ARM64. The Frame rechecks original Python/helper bytes, replacement digest and ELF machine; x86 keeps the original helper. Local official x86 backend/proxy and real Frame rebuilt ARM64 helper each passed one private marker transfer. Published Decky 3.3.0 AMD64/ARM64 discovery and stopped status/configuration RPCs passed. The actual managed Decky service on Frame, network discovery, user-facing transfer, Steam routes and worn VR remain unverified (docs/evidence/framey-localsend-managed-2026-10-27.md). |
| Launch Options | bin/hello | Remove the template x86 bin/hello or publish an ARM64 rebuild; missing Steam module mapping also prevents frontend initialization. |
| Decky-Clipper | bin/gstreamer-1.0/libgstproxy.so, bin/gstreamer-1.0/libgstmidi.so, bin/gstreamer-1.0/libgstmpegtsmux.so, bin/gstreamer-1.0/gst-plugin-scanner, bin/gstreamer-1.0/libgstivfparse.so, … 115 total; full list in JSON | Rebuild all bundled GStreamer libraries/plugins for ARM64 and verify capture/codec ABI against Frame SteamVR; gamescope capture differs. |
+10 -7
View File
@@ -203,12 +203,14 @@ the managed service is stopped; it does not restart the service.
Install, Update, and Remove show the current job stage in the headset panel.
The store verifies each downloaded archive and the headset applies it with a
rollback if Decky cannot start. The panel remembers the current job ID for this
browser session, so **Check result** can resume polling after a navigation or
temporary connection error. After a polling error, Install, Update, and Remove
rollback if Decky cannot start. The panel saves a random request ID before
submitting a change. The server uses that ID as the job ID and returns the
same job for a repeated, identical request while its result is retained;
reusing the ID for a different change is rejected. **Check result** can resume
polling even if the POST response is lost. After a connection error, Install, Update, and Remove
stay disabled while the saved job's result is unknown; use **Check result**
before starting another change.
If Chromium denies session storage, the open panel keeps the accepted job ID in
If Chromium denies session storage, the open panel keeps the request ID in
memory and **Check result** still works after a connection error. A page reload
loses that memory fallback; wait for the old job to finish and refresh installed
status before starting another change.
@@ -216,9 +218,10 @@ Refresh the installed list before starting another
operation after an error. The headset panel and desktop app send the hash of
the version you saw; if the official store changes that version before the job
starts, Frame Control stops before downloading or changing the headset. Refresh
the store and review the new version before retrying. If Frame Control restarts, it cannot recover the old
job result. The panel clears that job ID and asks you to refresh the installed
list before making another change.
the store and review the new version before retrying. Job results live in server
memory for one hour after completion. If Frame Control restarts, or the request
never reached it, the panel cannot recover the result. It clears that ID and
asks you to refresh the installed list before making another change.
An open plugin panel closes before its own Update or Remove starts, so its old
frontend cannot call the changing backend. Open panel is unavailable while a
+1 -1
View File
@@ -51,7 +51,7 @@ count. So a build reaches people only when you publish it, after testing it.
The script checks that the tag is on GitHub, that exact-commit main CI
(including e2e), native amd64/ARM64 Decky runtime checks, and the tagged
release workflow are green with no failed,
cancelled, skipped or running jobs, and that all ten installers
cancelled, skipped or running jobs in any run attempt, and that all ten installers
are attached, each with the SHA-256 digest GitHub records. It also checks
that the draft does not begin with a `WITHDRAWN` notice, so the preserved
v0.4.48 draft cannot accidentally be offered again. It checks
+7 -1
View File
@@ -23,6 +23,11 @@ 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.
**Reset example setting** opens a local `ConfirmModal`. **Keep setting** closes
it without a backend call; **Reset** saves `false` through the same guarded
write path as the toggle. This is a safe example of putting a settings change
behind an explicit confirmation in Framey. It has not been tried with a
physical controller in a worn headset.
The **About this setting** button opens a local detail page at
`/framey-sample/details/saved-setting`. It demonstrates `routerHook.addRoute`,
@@ -54,7 +59,8 @@ plugin directory does not enable its VR panel.
Run `python3 -m unittest discover -s tests -p test_framey_sample.py` from the
repository root. It checks backend persistence and evaluates the built bundle
against Framey's actual host script, including the quick action's scoped
backend call and local route registration/navigation. The fake Frame store flow is exercised by
backend call, modal cancel/confirm behavior, and local route
registration/navigation. The fake Frame store flow is exercised by
the repository's e2e suite. No worn-headset visibility or controller-input test
has passed for this sample yet.
+9 -1
View File
@@ -31,6 +31,7 @@ function Content() {
const [busy, setBusy] = SP_REACT.useState(true);
const [message, setMessage] = SP_REACT.useState('Loading setting…');
const pending = SP_REACT.useRef(false);
const resetModalOpen = SP_REACT.useRef(false);
async function refresh() {
if (pending.current)
return;
@@ -72,7 +73,14 @@ function Content() {
setBusy(false);
}
}
return SP_JSX.jsxs(DFL.PanelSection, { title: "Framey settings sample", children: [SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ToggleField, { label: "Example setting", description: "Stored by the Decky Python backend", checked: enabled, disabled: busy, onChange: (value) => { void change(value); } }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { disabled: busy, onClick: () => { void refresh(); }, children: "Reload saved setting" }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { onClick: () => DFL.Navigation.Navigate('/framey-sample/details/saved-setting'), children: "About this setting" }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx("p", { role: "status", children: message }) })] });
function confirmReset() {
if (pending.current || resetModalOpen.current)
return;
const dialog = DFL.showModal(SP_JSX.jsx(DFL.ConfirmModal, { strTitle: "Reset example setting?", strDescription: "This changes only the sample plugin's saved setting.", strOKButtonText: "Reset", strCancelButtonText: "Keep setting", onOK: () => { void change(false); } }));
resetModalOpen.current = true;
void dialog.ClosedPromise.then(() => { resetModalOpen.current = false; });
}
return SP_JSX.jsxs(DFL.PanelSection, { title: "Framey settings sample", children: [SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ToggleField, { label: "Example setting", description: "Stored by the Decky Python backend", checked: enabled, disabled: busy, onChange: (value) => { void change(value); } }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { disabled: busy, onClick: () => { void refresh(); }, children: "Reload saved setting" }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { disabled: busy, onClick: confirmReset, children: "Reset example setting" }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { onClick: () => DFL.Navigation.Navigate('/framey-sample/details/saved-setting'), children: "About this setting" }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx("p", { role: "status", children: message }) })] });
}
function Details() {
const { topic } = DFL.useParams();
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"index.js","sources":["decky://decky/plugin/Framey%20Settings%20Sample/node_modules/.pnpm/@decky+api@1.1.3/node_modules/@decky/api/dist/index.js","decky://decky/plugin/Framey%20Settings%20Sample/src/index.tsx"],"sourcesContent":["import _manifest from '@decky/manifest';\nexport * from \"./types\";\nconst manifest = _manifest;\nconst API_VERSION = 2;\nif (!manifest?.name) {\n throw new Error('[@decky/api]: Failed to find plugin manifest.');\n}\nconst internalAPIConnection = window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit;\nif (!internalAPIConnection) {\n throw new Error('[@decky/api]: Failed to connect to the loader as as the loader API was not initialized. This is likely a bug in Decky Loader.');\n}\nlet api;\ntry {\n api = internalAPIConnection.connect(API_VERSION, manifest.name);\n}\ncatch {\n api = internalAPIConnection.connect(1, manifest.name);\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version 1. Some features may not work.`);\n}\nif (api._version != API_VERSION) {\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version ${api._version}. Some features may not work.`);\n}\nexport const call = api.call;\nexport const callable = api.callable;\nexport const addEventListener = api.addEventListener;\nexport const removeEventListener = api.removeEventListener;\nexport const routerHook = api.routerHook;\nexport const toaster = api.toaster;\nexport const openFilePicker = api.openFilePicker;\nexport const executeInTab = api.executeInTab;\nexport const injectCssIntoTab = api.injectCssIntoTab;\nexport const removeCssFromTab = api.removeCssFromTab;\nexport const fetchNoCors = api.fetchNoCors;\nexport const getExternalResourceURL = api.getExternalResourceURL;\nexport const useQuickAccessVisible = api.useQuickAccessVisible;\nexport const definePlugin = (fn) => {\n return (...args) => {\n return fn(...args);\n };\n};\n","import {callable, definePlugin, routerHook, toaster} from '@decky/api';\nimport {ButtonItem, Navigation, PanelSection, PanelSectionRow, ToggleField, useParams} from '@decky/ui';\nimport {useEffect, useRef, useState} from 'react';\n\nconst getSetting = callable<[key: string, fallback: boolean], boolean>('get_setting');\nconst setSetting = callable<[key: string, value: boolean], boolean>('set_setting');\n\nfunction Content() {\n const [enabled, setEnabled] = useState(false);\n const [busy, setBusy] = useState(true);\n const [message, setMessage] = useState('Loading setting…');\n const pending = useRef(false);\n\n async function refresh() {\n if (pending.current) return;\n pending.current = true;\n setBusy(true);\n try {\n const value = await getSetting('enabled', false);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n } catch (error) {\n setMessage(`Could not read setting: ${String(error)}`);\n } finally {\n pending.current = false;\n setBusy(false);\n }\n }\n\n useEffect(() => {void refresh();}, []);\n\n async function change(value: boolean) {\n // Two controller events can arrive before React redraws the disabled toggle.\n // Keep writes ordered so a late first reply cannot replace the final choice.\n if (pending.current) return;\n pending.current = true;\n setBusy(true);\n try {\n await setSetting('enabled', value);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n toaster.toast({title: 'Framey sample',\n body: <span>Setting saved: <strong>{value ? 'on' : 'off'}</strong></span>});\n } catch (error) {\n setMessage(`Could not save setting: ${String(error)}`);\n } finally {\n pending.current = false;\n setBusy(false);\n }\n }\n\n return <PanelSection title=\"Framey settings sample\">\n <PanelSectionRow>\n <ToggleField label=\"Example setting\" description=\"Stored by the Decky Python backend\"\n checked={enabled} disabled={busy} onChange={(value: boolean) => {void change(value);}} />\n </PanelSectionRow>\n <PanelSectionRow><ButtonItem disabled={busy} onClick={() => {void refresh();}}>Reload saved setting</ButtonItem></PanelSectionRow>\n <PanelSectionRow><ButtonItem onClick={() => Navigation.Navigate('/framey-sample/details/saved-setting')}>About this setting</ButtonItem></PanelSectionRow>\n <PanelSectionRow><p role=\"status\">{message}</p></PanelSectionRow>\n </PanelSection>;\n}\n\nfunction Details() {\n const {topic} = useParams<{topic?: string}>();\n return <PanelSection title=\"Sample detail page\">\n <PanelSectionRow><p>Topic: {topic || 'overview'}</p></PanelSectionRow>\n <PanelSectionRow><p>The toggle is stored by this plugin's Decky backend in the user's settings directory.</p></PanelSectionRow>\n <PanelSectionRow><ButtonItem onClick={() => Navigation.NavigateBack()}>Back Line truncated
{"version":3,"file":"index.js","sources":["decky://decky/plugin/Framey%20Settings%20Sample/node_modules/.pnpm/@decky+api@1.1.3/node_modules/@decky/api/dist/index.js","decky://decky/plugin/Framey%20Settings%20Sample/src/index.tsx"],"sourcesContent":["import _manifest from '@decky/manifest';\nexport * from \"./types\";\nconst manifest = _manifest;\nconst API_VERSION = 2;\nif (!manifest?.name) {\n throw new Error('[@decky/api]: Failed to find plugin manifest.');\n}\nconst internalAPIConnection = window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit;\nif (!internalAPIConnection) {\n throw new Error('[@decky/api]: Failed to connect to the loader as as the loader API was not initialized. This is likely a bug in Decky Loader.');\n}\nlet api;\ntry {\n api = internalAPIConnection.connect(API_VERSION, manifest.name);\n}\ncatch {\n api = internalAPIConnection.connect(1, manifest.name);\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version 1. Some features may not work.`);\n}\nif (api._version != API_VERSION) {\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version ${api._version}. Some features may not work.`);\n}\nexport const call = api.call;\nexport const callable = api.callable;\nexport const addEventListener = api.addEventListener;\nexport const removeEventListener = api.removeEventListener;\nexport const routerHook = api.routerHook;\nexport const toaster = api.toaster;\nexport const openFilePicker = api.openFilePicker;\nexport const executeInTab = api.executeInTab;\nexport const injectCssIntoTab = api.injectCssIntoTab;\nexport const removeCssFromTab = api.removeCssFromTab;\nexport const fetchNoCors = api.fetchNoCors;\nexport const getExternalResourceURL = api.getExternalResourceURL;\nexport const useQuickAccessVisible = api.useQuickAccessVisible;\nexport const definePlugin = (fn) => {\n return (...args) => {\n return fn(...args);\n };\n};\n","import {callable, definePlugin, routerHook, toaster} from '@decky/api';\nimport {ButtonItem, ConfirmModal, Navigation, PanelSection, PanelSectionRow, ToggleField, showModal, useParams} from '@decky/ui';\nimport {useEffect, useRef, useState} from 'react';\n\nconst getSetting = callable<[key: string, fallback: boolean], boolean>('get_setting');\nconst setSetting = callable<[key: string, value: boolean], boolean>('set_setting');\n\nfunction Content() {\n const [enabled, setEnabled] = useState(false);\n const [busy, setBusy] = useState(true);\n const [message, setMessage] = useState('Loading setting…');\n const pending = useRef(false);\n const resetModalOpen = useRef(false);\n\n async function refresh() {\n if (pending.current) return;\n pending.current = true;\n setBusy(true);\n try {\n const value = await getSetting('enabled', false);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n } catch (error) {\n setMessage(`Could not read setting: ${String(error)}`);\n } finally {\n pending.current = false;\n setBusy(false);\n }\n }\n\n useEffect(() => {void refresh();}, []);\n\n async function change(value: boolean) {\n // Two controller events can arrive before React redraws the disabled toggle.\n // Keep writes ordered so a late first reply cannot replace the final choice.\n if (pending.current) return;\n pending.current = true;\n setBusy(true);\n try {\n await setSetting('enabled', value);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n toaster.toast({title: 'Framey sample',\n body: <span>Setting saved: <strong>{value ? 'on' : 'off'}</strong></span>});\n } catch (error) {\n setMessage(`Could not save setting: ${String(error)}`);\n } finally {\n pending.current = false;\n setBusy(false);\n }\n }\n\n function confirmReset() {\n if (pending.current || resetModalOpen.current) return;\n const dialog = showModal(<ConfirmModal strTitle=\"Reset example setting?\"\n strDescription=\"This changes only the sample plugin's saved setting.\"\n strOKButtonText=\"Reset\" strCancelButtonText=\"Keep setting\"\n onOK={() => {void change(false);}} />);\n resetModalOpen.current = true;\n void dialog.ClosedPromise.then(() => {resetModalOpen.current = false;});\n }\n\n return <PanelSection title=\"Framey settings sample\">\n <PanelSectionRow>\n <ToggleField label=\"Example setting\" description=\"Stored by the Decky Python backend\"\n checked={enabled} disabled={busy} onChange={(value: boolean) => {void change(value);}} />\n </PanelSectionRow>\n <PanelSectionRow><ButtonItem disabled={busy} onClick={() => {void refresh();}}>Reload saved setting</ButtonItem></PanelSectionRow>\n <PanelSectionRow><ButtonItem disabled={busy} onClick={confirmReset}>Reset example setting</ButtonItem></PanelSectionRLine truncated
+13 -1
View File
@@ -1,5 +1,5 @@
import {callable, definePlugin, routerHook, toaster} from '@decky/api';
import {ButtonItem, Navigation, PanelSection, PanelSectionRow, ToggleField, useParams} from '@decky/ui';
import {ButtonItem, ConfirmModal, Navigation, PanelSection, PanelSectionRow, ToggleField, showModal, useParams} from '@decky/ui';
import {useEffect, useRef, useState} from 'react';
const getSetting = callable<[key: string, fallback: boolean], boolean>('get_setting');
@@ -10,6 +10,7 @@ function Content() {
const [busy, setBusy] = useState(true);
const [message, setMessage] = useState('Loading setting…');
const pending = useRef(false);
const resetModalOpen = useRef(false);
async function refresh() {
if (pending.current) return;
@@ -49,12 +50,23 @@ function Content() {
}
}
function confirmReset() {
if (pending.current || resetModalOpen.current) return;
const dialog = showModal(<ConfirmModal strTitle="Reset example setting?"
strDescription="This changes only the sample plugin's saved setting."
strOKButtonText="Reset" strCancelButtonText="Keep setting"
onOK={() => {void change(false);}} />);
resetModalOpen.current = true;
void dialog.ClosedPromise.then(() => {resetModalOpen.current = false;});
}
return <PanelSection title="Framey settings sample">
<PanelSectionRow>
<ToggleField label="Example setting" description="Stored by the Decky Python backend"
checked={enabled} disabled={busy} onChange={(value: boolean) => {void change(value);}} />
</PanelSectionRow>
<PanelSectionRow><ButtonItem disabled={busy} onClick={() => {void refresh();}}>Reload saved setting</ButtonItem></PanelSectionRow>
<PanelSectionRow><ButtonItem disabled={busy} onClick={confirmReset}>Reset example setting</ButtonItem></PanelSectionRow>
<PanelSectionRow><ButtonItem onClick={() => Navigation.Navigate('/framey-sample/details/saved-setting')}>About this setting</ButtonItem></PanelSectionRow>
<PanelSectionRow><p role="status">{message}</p></PanelSectionRow>
</PanelSection>;
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Rebuild the pinned LocalSend-Go helper (Go 1.25.7 and module access required)."""
import argparse
import gzip
import hashlib
import io
import json
import os
from pathlib import Path
import subprocess
import tarfile
import tempfile
ROOT = Path(__file__).resolve().parents[1]
ASSETS = ROOT / 'ui/vendor/localsend-0.4.5'
SOURCE_SHA256 = 'f1f86292624b989d540689dbfad503bf15f3937d156978157b8dd418d54415d1'
EXPECTED = {
'amd64': 'd448d03861d578685e6c70ea3ace25eb34fca99cb9d4f98bf51ba23aa5cae843',
'arm64': 'e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae',
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--go', default='go', help='Path to Go 1.25.7')
parser.add_argument('--output', type=Path, default=ASSETS)
args = parser.parse_args()
assert subprocess.check_output([args.go, 'version'], text=True).split()[2] == 'go1.25.7', 'Use Go 1.25.7'
source_data = (ASSETS/'source.tar.gz').read_bytes()
assert hashlib.sha256(source_data).hexdigest() == SOURCE_SHA256, 'Pinned source archive changed'
args.output.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix='framey-localsend-build-') as temporary:
base = Path(temporary)
with tarfile.open(fileobj=io.BytesIO(source_data), mode='r:gz') as archive:
members = archive.getmembers()
assert all((m.isfile() or m.isdir()) and
(m.name == 'source' or m.name.startswith('source/'))
and '..' not in Path(m.name).parts for m in members), 'Unsafe source archive'
archive.extractall(base, filter='data')
env = dict(os.environ, CGO_ENABLED='0', GOOS='linux', GOTOOLCHAIN='local',
GOCACHE=str(base/'cache'), GOMODCACHE=str(base/'modules'))
for arch in ('amd64', 'arm64'):
output = base/'localsend-core'
subprocess.run([args.go, 'build', '-mod=readonly', '-trimpath', '-buildvcs=false',
'-ldflags=-s -w', '-o', str(output), '.'], cwd=base/'source',
env=dict(env, GOARCH=arch), check=True)
data = output.read_bytes()
digest = hashlib.sha256(data).hexdigest()
assert digest == EXPECTED[arch], f'{arch} helper does not match pinned bytes'
if arch == 'arm64':
packed = gzip.compress(data, mtime=0)
(args.output/'aarch64.gz').write_bytes(packed)
packed_digest = hashlib.sha256(packed).hexdigest()
else:
packed_digest = None
print(json.dumps({'arch': arch, 'sha256': digest, 'gzipSha256': packed_digest,
'bytes': len(data)}), flush=True)
if __name__ == '__main__':
main()
+15 -4
View File
@@ -97,7 +97,12 @@ def render(data):
'the real ARM64 Frame repeats those lifecycle checks under the shared lock. Local Chromium with that live backend verifies '
'Begin/Cancel, countdown and a silent completion toast. Simple Timer qualifies as works for silent in-panel session timers. '
'Update/reinstall is required; old receipts stay partial. Sound/suspend are unsupported, closed-panel alerts and worn VR unverified. '
'The private service, files, test listeners and processes were removed; independent cleanup audit passed and battery stayed 99 → 99.', '',
'The private service, files, test listeners and processes were removed; independent cleanup audit passed and battery stayed 99 → 99. '
'A [shared Toggle follow-up](evidence/framey-toggle-2026-10-27.md) separates standalone Toggle.value from ToggleField.checked. '
'Nine official archives consume this API; their remaining blockers and status labels are preserved. '
'Local opaque Chromium controls and the exact Pause Games frontend callback pass. Actual published Decky 3.3.0 discovery and '
'private settings/exclusion-list persistence pass on amd64 and the ARM64 Frame through the normal CEF bridge. '
'Process control and worn VR remain unverified; independent cleanup audit passed.', '',
'## 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.', '',
@@ -118,15 +123,21 @@ def render(data):
p['backend']+('; '+', '.join(native) if native else ''),p['hardware']]))+' |')
lines += ['', '## Native payload portability', '',
'All published Linux native payloads in this snapshot are x86_64; no published aarch64 payload was found. '
'The three Windows DLLs belong to Decky-Framegen. Except for the exact managed DeckyFileServer adaptation below, the installer still rejects these archives. '
'The three Windows DLLs belong to Decky-Framegen. Except for the exact managed DeckyFileServer and Decky Localsend adaptations below, the installer still rejects these archives. '
'Apart from LocalSend and DeckyFileServer, ARM64 builds were not attempted: the constraints below remain unresolved.', '',
'A [DeckyFileServer ARM64 follow-up](evidence/framey-fileserver-arm64-2026-10-27.md) ships an offline, digest-checked GPL helper build, corresponding vendored source and a build recipe. '
'Only its exact managed archive can replace the x86 helper and remove its unnecessary root flag. Actual Decky 3.3.0 discovery/RPC and real Frame HTTPS private-fixture listing/download pass; uploads stay disabled. '
'Hostname-DNS failures and empty optional arguments are repaired. Decky 3.3.0 still exposes `decky.HOME`; that call is unchanged. '
'The plugin moves from broken to partial: its frontend advertises HTTP/steamdeck URLs and needs a Steam folder picker. Use HTTPS and a preconfigured directory. Uploads, generated thumbnails and worn VR remain unverified.', '',
'[LocalSend ARM64 build/runtime evidence](evidence/framey-localsend-arm64-2026-10-26.md) '
'covers a reproducible MIT helper build and its isolated real-Frame protocol 2.0 info response. '
'The official x86 archive remains blocked; full plugin integration and file transfers are unverified.', '',
'covers an isolated real-Frame protocol 2.0 info response from an earlier helper build. '
'[Exact official source matching](evidence/framey-localsend-source-match-2026-10-27.md) '
'now identifies the official helper source and a reproducible ARM64 build from that revision. '
'The exact official x86 backend passed one local private-namespace transfer, and the rebuilt ARM64 helper passed one locked real-Frame private marker transfer. '
'Published Decky 3.3.0 AMD64/ARM64 runtime checks discovered the adapted plugin and read its stopped status and configuration. '
'Only the exact managed 0.4.5 archive can replace its x86 helper on Frame. '
'The actual managed Decky service on Frame, user-facing transfer, local discovery and worn VR remain unverified '
'([managed adaptation evidence](evidence/framey-localsend-managed-2026-10-27.md)).', '',
'| Plugin | Native payloads | ARM64 result / remaining blocker |', '| --- | --- | --- |']
for p in data['plugins']:
if p['native']:
+2 -1
View File
@@ -40,7 +40,8 @@ def check_coverage():
raise ValueError('Managed shims must declare a literal name for runtime test coverage')
shims.add(node.args[0].value)
cases = json.loads((ROOT/'tests/decky-runtime/cases.json').read_text())
if shims != {case['shim'] for case in cases}:
# Frontend-only compatibility probes need no archive/backend adaptation.
if shims != {case['shim'] for case in cases if case['shim'] is not None}:
raise ValueError('Every managed shim needs an actual Decky runtime case')
return cases
+18 -12
View File
@@ -77,19 +77,25 @@ def successful_jobs(run, required):
attempt = run.get("run_attempt")
if not isinstance(attempt, int) or attempt < 1:
raise GateError(f"run {run['id']} has no valid attempt number")
data = api(f"actions/runs/{run['id']}/attempts/{attempt}/jobs?per_page=100")
jobs = data.get("jobs") or []
if data.get("total_count") != len(jobs):
raise GateError(f"run {run['id']} has more jobs than the gate inspected")
names = [job.get("name") for job in jobs]
if len(names) != len(set(names)) or not required.issubset(names):
latest_jobs = None
for number in range(1, attempt + 1):
data = api(f"actions/runs/{run['id']}/attempts/{number}/jobs?per_page=100")
jobs = data.get("jobs") or []
if data.get("total_count") != len(jobs):
raise GateError(f"run {run['id']} attempt {number} has more jobs than the gate inspected")
names = [job.get("name") for job in jobs]
if not jobs or len(names) != len(set(names)):
raise GateError(f"run {run['id']} attempt {number} has missing or duplicate jobs")
bad = [f"{job.get('name')}: {job.get('status')}/{job.get('conclusion')}"
for job in jobs if job.get("status") != "completed" or job.get("conclusion") != "success"]
if bad:
raise GateError(f"run {run['id']} attempt {number} has unfinished, failed, cancelled, or skipped jobs: {bad}")
latest_jobs = jobs
names = [job.get("name") for job in latest_jobs]
if not required.issubset(names):
missing = sorted(required.difference(names))
raise GateError(f"run {run['id']} has missing or duplicate jobs: {missing}")
bad = [f"{job.get('name')}: {job.get('status')}/{job.get('conclusion')}"
for job in jobs if job.get("status") != "completed" or job.get("conclusion") != "success"]
if bad:
raise GateError(f"run {run['id']} has unfinished, failed, cancelled, or skipped jobs: {bad}")
print(f"gate passed: {run.get('html_url')} ({len(jobs)} jobs)")
raise GateError(f"run {run['id']} has missing jobs in its latest attempt: {missing}")
print(f"gate passed: {run.get('html_url')} ({len(latest_jobs)} jobs, {attempt} attempts)")
def main(tag):
+10
View File
@@ -1,4 +1,9 @@
[
{
"id": 13,
"shim": null,
"probe": "toggle-settings"
},
{
"id": 12,
"shim": "music-control-system-dbus-env",
@@ -28,5 +33,10 @@
"id": 60,
"shim": "deckyfileserver-user-native",
"probe": "fileserver"
},
{
"id": 125,
"shim": "localsend-digest-native",
"probe": "localsend"
}
]
+35 -2
View File
@@ -90,7 +90,7 @@ def main():
plugin = base/'plugins'/record['folder']
receipt = json.loads((plugin/'.frame-control-store.json').read_text())
if not args.reproduce_timer_regression:
assert receipt['compatShims'] == [case['shim']], receipt
assert receipt.get('compatShims', []) == ([case['shim']] if case['shim'] else []), receipt
else:
assert record['id'] == 96
with zipfile.ZipFile(io.BytesIO(data)) as original:
@@ -152,7 +152,22 @@ def main():
report['calls'].append({'method': method, 'result': response})
return response
if case['probe'] == 'fileserver':
if case['probe'] == 'localsend':
helper = plugin/'bin/localsend-core'
expected = ('e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae'
if __import__('platform').machine() == 'aarch64'
else 'd448d03861d578685e6c70ea3ace25eb34fca99cb9d4f98bf51ba23aa5cae843')
assert hashlib.sha256(helper.read_bytes()).hexdigest() == expected
status = current('get_backend_status')
assert status == {'running': False, 'url': 'https://127.0.0.1:53317'}, status
settings = current('get_backend_config')
assert settings['use_https'] and not settings['auto_save'], settings
assert settings['network_interface'] == '*', settings
assert settings['download_folder'].startswith(str(base)), settings
stopped = current('proxy_get', '/api/localsend/v2/info')
assert stopped['status'] == 503 and stopped['data']['error'] == 'Backend not running', stopped
report['result'] = 'published Decky discovery and public read-only settings/status RPC passed; network helper remained stopped'
elif case['probe'] == 'fileserver':
assert until_ready(lambda: current('get_port') == 8000, 'default port')
assert current('get_directory') == str(home)
assert current('get_server_running') is False
@@ -187,6 +202,24 @@ def main():
report['result'] = ('original empty argument bug reproduced: thumbnail policy ignored'
if args.reproduce_fileserver_empty_flag_regression else
'discovery, API1 settings, native HTTPS listing/download, thumbnail policy and stop passed; uploads disabled')
elif case['probe'] == 'toggle-settings':
def call(method, *values):
result = rpc('loader/call_plugin_method', record['name'], method, *values)
report['calls'].append({'method': method, 'result': result})
return result
assert call('load_settings') == {}
# Settings only. Never call pause/resume/kill or signal a process.
for value in (True, False, True):
call('save_setting', 'autoPause', value)
assert call('load_settings')['autoPause'] is value
call('add_no_auto_pause_set', 424242)
assert call('load_settings')['noAutoPauseSet'] == [424242]
call('remove_no_auto_pause_set', 424242)
assert call('load_settings')['noAutoPauseSet'] == []
settings = json.loads((base/'settings'/record['folder']/'settings.json').read_text())
assert settings['autoPause'] is True and settings['noAutoPauseSet'] == []
report['result'] = 'discovery and Toggle-backed private settings persistence passed; no process actions'
elif case['probe'] == 'timer-lifecycle':
from timer_lifecycle import probe
+16 -2
View File
@@ -107,9 +107,16 @@ u=h/'.config/systemd/user/frame-control-decky.service'; u.parent.mkdir(parents=T
self.assertEqual((catalogue[0]['version'], catalogue[0]['sha256']), ('1.0.0', HASHES[0]))
status, _, _ = api('POST', '/api/decky/plugin', {'action':'install','id':999})
self.assertEqual(status, 400) # Third-party code needs explicit acknowledgement.
job = finished(ok('POST', '/api/decky/plugin',
{'action':'install','id':999,'acknowledged':True}))
request = {'action':'install','id':999,'acknowledged':True,
'requestId':'0123456789abcdef' * 2}
started = ok('POST', '/api/decky/plugin', request)
self.assertEqual(ok('POST', '/api/decky/plugin', request)['job'], started['job'])
status, conflict, _ = api('POST', '/api/decky/plugin', {**request, 'id':998})
self.assertEqual(status, 409)
self.assertIn('already used', conflict['error'])
job = finished(started)
self.assertFalse(job.get('error'), job)
self.assertEqual(ok('POST', '/api/decky/plugin', request)['job'], started['job'])
self.assertEqual(ok('GET', '/api/decky')['plugins'][0]['version'], '1.0.0')
self.assertIn('Sample 1.0.0', ssh('cat ~/.local/share/frame-control/decky/plugins/sample-panel/dist/index.js'))
receipt = HASHES[0]
@@ -203,6 +210,13 @@ u=h/'.config/systemd/user/frame-control-decky.service'; u.parent.mkdir(parents=T
arch = ssh('uname -m').strip()
self.assertEqual(ssh(f'sha256sum {installed}/bin/backend').split()[0],
frame_decky_store.FILESERVER_BINARIES[arch])
if plugin['id'] == 125:
receipt = json.loads(ssh(f'cat {installed}/.frame-control-store.json'))
self.assertEqual(receipt['compatShims'], ['localsend-digest-native'])
arch = ssh('uname -m').strip()
self.assertEqual(ssh(f'sha256sum {installed}/bin/localsend-core').split()[0],
frame_decky_store.LOCALSEND_AARCH64 if arch == 'aarch64'
else frame_decky_store.LOCALSEND_ORIGINAL)
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'))
+8 -3
View File
@@ -91,17 +91,22 @@ elif method == "GET" and endpoint.startswith(repo + "actions/workflows/"):
is_main = workflow != "release.yml"
run_id = {"checks.yml": 501, "release.yml": 502, "decky-runtime.yml": 503}[workflow]
run = {"id": run_id, "head_sha": gate.get("main_sha" if is_main else "release_sha", commit),
"head_branch": branch, "event": "push", "run_attempt": 1,
"head_branch": branch, "event": "push", "run_attempt": gate.get("release_attempts", 1) if run_id == 502 else 1,
"status": gate.get("decky_status" if run_id == 503 else ("main_status" if is_main else "release_status"), "completed"),
"conclusion": gate.get("decky_conclusion" if run_id == 503 else ("main_conclusion" if is_main else "release_conclusion"), "success"),
"html_url": "https://github.com/saphid/frame-control/actions/runs/%s" % run_id}
print(json.dumps({"workflow_runs": [run]}))
elif method == "GET" and endpoint.startswith(repo + "actions/runs/"):
run_id = endpoint[len(repo + "actions/runs/"):].split("/")[0]
parts = endpoint[len(repo + "actions/runs/"):].split("/")
run_id = parts[0]
attempt = int(parts[2])
names = {"501": main_jobs, "502": release_jobs, "503": decky_jobs}[run_id]
names = [name for name in names if name != gate.get("omit_job")]
jobs = [{"name": name, "status": "completed",
"conclusion": gate.get("bad_job_conclusion", "success") if name == gate.get("bad_job") else "success"}
"conclusion": (gate.get("prior_bad_job_conclusion", "failure")
if run_id == "502" and attempt == 1 and name == gate.get("prior_bad_job")
else gate.get("bad_job_conclusion", "success") if name == gate.get("bad_job")
else "success")}
for name in names]
print(json.dumps({"total_count": len(jobs), "jobs": jobs}))
elif method == "GET" and endpoint.startswith(repo + "releases?"):
Binary file not shown.
Binary file not shown.
+14 -2
View File
@@ -1,14 +1,17 @@
# Pinned Decky store test archives
These are unmodified ZIPs downloaded from the official Decky store CDN on
2026-10-25, except `96.zip` and `111.zip` downloaded on 2026-10-26 and `60.zip` on 2026-10-27. Each ZIP includes its
upstream `LICENSE`. They are test data, not plugins shipped or installed by
2026-10-25, except `96.zip` and `111.zip` downloaded on 2026-10-26 and `60.zip` on
2026-10-27; `13.zip` and `125.zip` were added from the digest-verified audit cache on 2026-10-27.
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 |
| --- | --- | --- | --- |
| `13.zip` | Pause Games | 1.0.3 | `4aefdfb0ef25dea9c6d9fd037731c8b99ee8a1be966c2f7d5b0602112ef29f62` |
| `60.zip` | DeckyFileServer | 1.2.0 | `7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0` |
| `125.zip` | Decky Localsend | 0.4.5 | `e271ccb55eb23f918b99a69f79aec905df4c9b2ef7b23bbf4ec1c2d1a6f0c6b7` |
| `96.zip` | Simple Timer | 1.0.0 | `973d927e9d9a1feec04679c564af7e389a729b6735d2a988ffc2cb46ce4eb312` |
| `7.zip` | CSS Loader | 2.1.3 | `99beaf1374af82408e6d056e0b0147e5fca544e1035fa7ab88419231697bc231` |
| `12.zip` | MusicControl | 1.1.6 | `246b89fd653c60a735b1cc401d1b0937d5cb969eea9024561661ff741f081d62` |
@@ -50,8 +53,17 @@ active/canceled/expired backend restarts are regression checks; the unmodified
archive must reproduce its historical lifecycle errors. Only private silent
timers/settings are used. The upstream BSD license stays unchanged.
Pause Games runs through actual published Decky 3.3.0 discovery and instance RPC
for private settings and a synthetic exclusion list. No process-control method
is called. The official frontend's standalone Toggle component separately
checks saved true/false state and matching settings callbacks; effects are inert.
DeckyFileServer also runs through actual Decky 3.3.0 discovery/API1 RPC with a
native helper replacement. HTTPS listing/download use only public markers in a
private directory; uploads remain disabled. The exact source build, GPL licenses
and reproducible recipe are in [the native assets](../../../ui/vendor/deckyfileserver-1.2.0/README.md).
The original empty optional-argument bug is reproduced by a negative runtime case.
Decky Localsend's exact original x86 backend/proxy passed an isolated private
marker transfer outside Decky. The native Decky gate checks discovery and
read-only backend status/configuration with its network helper stopped. The
managed ARM64 helper and transfer remain separately gated; the fixture does
not prove worn VR or a transfer on Frame.
+4
View File
@@ -4,8 +4,12 @@ from pathlib import Path
ROOT = Path(__file__).parent / 'fixtures/framey-official'
PLUGINS = (
{'id': 13, 'name': 'Pause Games', 'version': '1.0.3', 'api_version': 1,
'folder': 'SDH-PauseGames', 'sha256': '4aefdfb0ef25dea9c6d9fd037731c8b99ee8a1be966c2f7d5b0602112ef29f62'},
{'id': 60, 'name': 'DeckyFileServer', 'version': '1.2.0', 'api_version': 1,
'folder': 'DeckyFileServer', 'sha256': '7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0'},
{'id': 125, 'name': 'Decky Localsend', 'version': '0.4.5', 'api_version': 1,
'folder': 'decky-localsend', 'sha256': 'e271ccb55eb23f918b99a69f79aec905df4c9b2ef7b23bbf4ec1c2d1a6f0c6b7'},
{'id': 89, 'name': 'DeckySpy', 'version': '0.6.7', 'api_version': 'legacy',
'folder': 'decky-spy', 'sha256': '2d431a89826ed5cf3fd37258373eb3b46c841c39795aa8b25f83e59f4eee87c7'},
{'id': 96, 'name': 'Simple Timer', 'version': '1.0.0', 'api_version': 1,
+60
View File
@@ -46,6 +46,22 @@ const nodes=(tree,type)=>Array.isArray(tree)?tree.flatMap(child=>nodes(child,typ
[...(tree?.type===type?[tree]:[]),...(tree?.children||[]).flatMap(child=>nodes(child,type))];
// Disabled controls stay inert even if a caller invokes their handlers directly.
let changed=0;
// DFL.Toggle is a standalone value-controlled input. ToggleField uses checked.
const toggleChanges=[];
for(const value of [true,false]) {
const input=nodes(window.DFL.Toggle({value,onChange:v=>toggleChanges.push(v)}),'input')[0];
assert.equal(input.props.checked,value,'Toggle must render its value prop');
input.props.onChange({target:{checked:!value}});
}
assert.deepEqual(toggleChanges,[false,true]);
const disabledToggle=nodes(window.DFL.Toggle({value:true,disabled:true,
onChange:()=>changed++}),'input')[0];
assert.equal(disabledToggle.props.checked,true);
assert.equal(disabledToggle.props.disabled,true);
disabledToggle.props.onChange({target:{checked:false}});
assert.equal(changed,0);
const fieldToggle=nodes(window.DFL.ToggleField({checked:true,value:false}),'input')[0];
assert.equal(fieldToggle.props.checked,true,'ToggleField must retain its checked contract');
const toggle=nodes(window.DFL.ToggleField({label:'Enabled',checked:true,disabled:true,
onChange:()=>changed++}),'input')[0];
const options=[{label:'Modes',options:[{label:'Slow',data:'slow'},{label:'Fast',data:'fast'}]}];
@@ -253,6 +269,14 @@ nodes(tabs,'button')[2].props.onClick();
assert.deepEqual(tabChanges,['one']);
assert.equal(window.DFL.Spinner({}).props.role,'status');
assert.equal(window.DFL.ProgressBar({nProgress:120}).props.value,100);
assert.equal(window.DFL.ProgressBar({nProgress:40,indeterminate:true}).props.value,undefined,
'an indeterminate progress element must omit its value');
assert.equal(window.DFL.ProgressBarItem({nProgress:35}).props.value,35);
const progressInfo=window.DFL.ProgressBarWithInfo({indeterminate:true,
sOperationText:'Downloading plugin'});
assert.equal(nodes(progressInfo,'progress')[0].props.value,undefined);
assert.equal(nodes(progressInfo,'small')[0].props.role,'status');
assert.equal(nodes(progressInfo,'small')[0].children[0],'Downloading plugin');
assert.equal(window.DFL.joinClassNames('one',null,'two'),'one two');
let eventCount=0;
const unsubscribe=window.SteamClient.GameSessions.RegisterForAppLifetimeNotifications(()=>eventCount++);
@@ -464,11 +488,47 @@ assert.equal(window.DFL.useParams().view,'art');
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123/art/extra'),false);
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123?admin=1'),false);
assert.equal(window.DFL.Navigation.Navigate('/plugin/game//art'),false);
assert.equal(window.DFL.Navigation.NavigateBack(),true);
assert.equal(window.DFL.useParams().appid,'123');
assert.equal(window.DFL.useParams().view,undefined,'Back returns to the previous local page');
assert.equal(window.DFL.Navigation.NavigateBack(),true);
assert.equal(window.DFL.useParams().appid,undefined,'A second Back returns to the plugin root');
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123'),true);
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123'),true);
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123/art'),true);
assert.equal(window.DFL.Navigation.NavigateBack(),true);
assert.equal(window.DFL.useParams().view,undefined,'Navigating to the same page did not add a history entry');
assert.equal(window.DFL.Navigation.NavigateBack(),true);
assert.equal(window.DFL.useParams().appid,undefined);
assert.throws(()=>api.routerHook.addRoute('/plugin/:id/:id',GameRoute));
assert.throws(()=>api.routerHook.addRoute('/plugin/:id?/art',GameRoute));
api.routerHook.addRoute('/plugin/other',LocalRoute);
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123'),true);
assert.equal(window.DFL.Navigation.Navigate('/plugin/other'),true);
removeGame();
assert.equal(window.DFL.Navigation.NavigateBack(),true);
assert.equal(nodes(window.__surface({plugin}),LocalRoute).length,0,
'Back skips a route that the plugin unregistered');
assert.equal(window.DFL.useParams().appid,undefined);
assert.equal(nodes(window.__surface({plugin}),GameRoute).length,0);
api.routerHook.removeRoute('/plugin/other');
api.routerHook.addRoute('/plugin/first',LocalRoute);
api.routerHook.addRoute('/plugin/second',LocalRoute);
for(let index=0;index<40;index++)
assert.equal(window.DFL.Navigation.Navigate(index%2 ? '/plugin/first' : '/plugin/second'),true);
for(let index=0;index<32;index++)window.DFL.Navigation.NavigateBack();
assert.equal(nodes(window.__surface({plugin}),LocalRoute).length,1);
window.DFL.Navigation.NavigateBack();
assert.equal(nodes(window.__surface({plugin}),LocalRoute).length,0,
'local route history is bounded and falls back to the plugin root');
window.DFL.Navigation.Navigate('/plugin/first');
window.DFL.Navigation.Navigate('/plugin/second');
window.DFL.Navigation.CloseSideMenus();
window.DFL.Navigation.NavigateBack();
assert.equal(nodes(window.__surface({plugin}),LocalRoute).length,0,
'CloseSideMenus clears the local route history');
api.routerHook.removeRoute('/plugin/first');
api.routerHook.removeRoute('/plugin/second');
// Event subscription is acknowledged before a method that immediately emits.
const onMessage=listeners.find(x=>x.type==='message').handler;
+23
View File
@@ -8,6 +8,7 @@ const bundle=readFileSync(new URL('../../examples/framey-settings/dist/index.js'
assert.match(bundle,/SP_JSX\.jsx/);
const React={Fragment:Symbol('fragment'),Component:class {},
createElement:(type,props,...children)=>({type,props:props||{},children}),
cloneElement:(element,props)=>({...element,props:{...element.props,...props}}),
useState:value=>[value,()=>{}],useRef:value=>({current:value}),
useEffect(){},isValidElement:value=>!!value?.type};
class Element {
@@ -79,6 +80,28 @@ toggle.props.onChange(false);
await new Promise(resolve=>setImmediate(resolve));
assert.equal(calls.filter(call=>call.method==='set_setting').length,2,
'a completed save must release the sample setting lock');
// The sample reset is a real Framey modal: Cancel cannot write, OK writes the
// sample setting only after explicit confirmation.
const reset=content.children[0][2].children[0];
const setCalls=()=>calls.filter(call=>call.method==='set_setting');
const dialog=()=>roots.get(elements.get('framey-modal')).value[0].children[1];
reset.props.onClick();
reset.props.onClick();
assert.equal(roots.get(elements.get('framey-modal')).value.length,1,
'a rapid second tap must not stack reset dialogs');
assert.equal(dialog().props.strTitle,'Reset example setting?');
let rendered=dialog().type(dialog().props);
rendered.children.find(child=>child?.children?.[0]==='Keep setting').props.onClick();
await new Promise(resolve=>setImmediate(resolve));
assert.equal(setCalls().length,2,'cancel leaves the saved value untouched');
assert.equal(roots.get(elements.get('framey-modal')).value,null);
reset.props.onClick();
rendered=dialog().type(dialog().props);
rendered.children.find(child=>child?.children?.[0]==='Reset').props.onClick();
await new Promise(resolve=>setImmediate(resolve));
assert.equal(setCalls().length,3);
assert.deepEqual(Array.from(setCalls().at(-1).args),['enabled',false]);
assert.equal(roots.get(elements.get('framey-modal')).value,null);
assert.equal(window.DFL.Navigation.Navigate('/framey-sample/details/saved-setting'),true);
assert.equal(window.DFL.useParams().topic,'saved-setting');
assert.equal(window.DFL.Navigation.Navigate('/library/home'),false);
+42 -1
View File
@@ -1,4 +1,5 @@
import assert from 'node:assert/strict';
import {webcrypto} from 'node:crypto';
import {readFileSync} from 'node:fs';
import vm from 'node:vm';
@@ -17,6 +18,7 @@ const document = {getElementById(id) {if(!elements.has(id)) elements.set(id,new
createElement() {return new Element();}, hidden:false};
const session = new Map();
const ctx = vm.createContext({document, location:{hash:'',pathname:'/'},
crypto:webcrypto,
sessionStorage:{getItem:key=>session.get(key)||'',setItem:(key,value)=>session.set(key,value),removeItem:key=>session.delete(key)},
history:{replaceState(){}}, confirm:()=>true,
window:{addEventListener(){}}, setInterval(){}, setTimeout(){},
@@ -61,6 +63,45 @@ assert.equal(el('deckyJobProgress').value,100);
assert.equal(el('deckyJobCheck').hidden,true);
assert.equal(session.has('frameyPluginJob'),false);
// If the server accepts the POST but its response is lost, the panel already
// has the ID needed to find the job. It must not offer a second install.
let lostJobId, lostPosts=0;
ctx.mockApi=async (path,body) => {
if(path==='/api/decky/plugin'){
lostPosts++; lostJobId=body.requestId;
assert.match(lostJobId,/^[0-9a-f]{32}$/);
throw Error('POST response lost');
}
if(path.startsWith('/api/job')){
assert.equal(new URL(path,'http://localhost').searchParams.get('id'),lostJobId);
return {done:true,message:'Recovered install'};
}
if(path==='/api/decky')return {managed:true,running:true,plugins:[]};
throw Error(`unexpected ${path}`);
};
vm.runInContext('api=mockApi',ctx);
await vm.runInContext('deckyChange("install",{id:2,name:"Plugin 2",sha256:"a".repeat(64)})',ctx);
assert.equal(JSON.parse(session.get('frameyPluginJob')).job,lostJobId);
assert.equal(el('deckyJobCheck').hidden,false);
await vm.runInContext('deckyChange("install",{id:2,name:"Plugin 2",sha256:"a".repeat(64)})',ctx);
assert.equal(lostPosts,1);
await vm.runInContext('checkDeckyJob()',ctx);
assert.equal(el('deckyJobStatus').textContent,'Recovered install');
assert.equal(session.has('frameyPluginJob'),false);
// A definite validation rejection never reserved a job on the server.
ctx.mockApi=async path => {
if(path==='/api/decky/plugin'){
const error=Error('Invalid reviewed store version');error.status=400;throw error;
}
if(path==='/api/decky')return {managed:true,running:true,plugins:[]};
throw Error(`unexpected ${path}`);
};
vm.runInContext('api=mockApi',ctx);
await vm.runInContext('deckyChange("install",{id:2,name:"Plugin 2",sha256:"bad"})',ctx);
assert.equal(session.has('frameyPluginJob'),false);
assert.equal(el('deckyJobCheck').hidden,true);
// A stopped Decky service must still offer recovery by removing its owned plugin.
ctx.mockApi=async path => path==='/api/decky' ?
{managed:true,running:false,plugins:[{storeId:2,name:'Plugin 2',version:'1.0'}]} :
@@ -149,7 +190,7 @@ ctx.mockApi=async path => {
};
vm.runInContext('api=mockApi',ctx);
await vm.runInContext('checkDeckyJob()',ctx);
assert.match(el('deckyJobStatus').textContent,/app restarted and lost this job result/);
assert.match(el('deckyJobStatus').textContent,/server has no record of this request/);
assert.equal(el('deckyJobCheck').hidden,true);
assert.equal(session.has('frameyPluginJob'),false);
assert.equal(el('deckyStoreList').children[0].children[1].disabled,false);
+46
View File
@@ -0,0 +1,46 @@
// Exact official Pause Games component and settings callbacks; effects/process
// actions never execute here. Actual Decky discovery/RPC has a separate gate.
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import vm from 'node:vm';
const bundle=readFileSync(process.argv[2],'utf8');
const official=bundle.replace(/export \{ index as default \};/,
'window.__toggleTest = {AppItem, Settings};');
assert.notEqual(official,bundle);
const source=readFileSync(new URL('../../ui/framey-host.js',import.meta.url),'utf8');
const host=source.replace(/\n start\(\);\n\}\)\(\);\s*$/, '\n})();');
assert.notEqual(host,source);
const React={Fragment:'fragment',Component:class {},
createElement:(type,props,...children)=>({type,props:props||{},children}),
createContext:()=>({}),useState:value=>[value,()=>{}],useEffect(){},
useRef:value=>({current:value}),useMemo:fn=>fn(),useCallback:fn=>fn};
const messages=[],listeners=[];
const parent={postMessage:message=>messages.push(message)};
const window={React,ReactDOM:{createRoot:()=>({render(){}})},location:{hash:''}};
const context=vm.createContext({window,parent,navigator:{language:'en'},
document:{getElementById:()=>({})},console,URL,Blob,
addEventListener:(type,handler)=>listeners.push({type,handler}),setTimeout:()=>1,clearTimeout(){}});
vm.runInContext(host,context);
Object.assign(context,{DFL:window.DFL,SP_REACT:React});
vm.runInContext(official,context,{timeout:2000});
const {AppItem,Settings}=window.__toggleTest;
const find=(node,type)=>node?.type===type?node:(node?.children||[]).flat(Infinity)
.map(child=>find(child,type)).find(Boolean);
for(const excluded of [false,true]) {
vm.runInContext(`window.__toggleTest.Settings.data.noAutoPauseSet=new Set(${excluded?'[424242]':'[]'});`,context);
const row=AppItem({app:{appid:'424242',display_name:'Private fixture'},autoPause:true});
const toggle=find(row,window.DFL.Toggle);
assert.ok(toggle);
const input=window.DFL.Toggle(toggle.props);
assert.equal(input.props.checked,!excluded,'official persisted exclusion must display correctly');
input.props.onChange({target:{checked:excluded}});
const call=messages.at(-1);
assert.equal(call.type,'framey:call');
assert.equal(call.method,excluded?'remove_no_auto_pause_set':'add_no_auto_pause_set');
assert.deepEqual(Array.from(call.args),[424242]);
assert.equal(Settings.data.noAutoPauseSet.has(424242),!excluded);
listeners.find(x=>x.type==='message').handler({source:parent,
data:{type:'framey:reply',id:call.id,result:null}});
}
assert.equal(messages.filter(x=>x.type==='framey:call').length,2);
+8 -2
View File
@@ -6,8 +6,9 @@ BUILD_ID in tests/smoke/results/<time>-<BUILD_ID>.json (git-ignored):
Before any status read or title change, it atomically owns /tmp/frame-test.lock
on the Frame and checks SteamVR standby, proximity and battery read-only. A
busy lock, awake headset or battery below 20% stops the run. Only this run's
lock is released, including after test failures.
busy lock, awake headset, missing or active-range proximity reading, or battery
below 20% stops the run. Only this run's lock is released, including after
test failures.
- properties.json from Valve's devkit service names the login user;
- status (ui/frame_status.py) reads the build, battery and storage;
@@ -107,6 +108,11 @@ def preflight():
state = json.loads(result.stdout)
if state.get('activity') != 3:
raise RuntimeError('Frame is awake or SteamVR activity is unknown; active session untouched')
proximity = state.get('proximity')
if (not isinstance(proximity, list) or not proximity or
any(not isinstance(value, (int, float)) or isinstance(value, bool) or
value < 0 or value > 10 for value in proximity)):
raise RuntimeError('Frame proximity is active or unknown; active session untouched')
battery = state.get('battery')
if not battery or min(battery) < 20:
raise RuntimeError('Frame battery is unavailable or below the smoke threshold')
+56
View File
@@ -212,6 +212,62 @@ class StoreTransaction(unittest.TestCase):
self.assertEqual(helper.stat().st_mode & 0o777, 0o700)
compile((plugin/'main.py').read_text(), str(plugin/'main.py'), 'exec')
self.assertIn('if arg]', (plugin/'main.py').read_text())
@unittest.skipUnless(__import__('platform').machine() in ('x86_64', 'aarch64'), 'native Linux helper architecture')
def test_exact_localsend_gets_verified_native_helper_and_preserves_source(self):
from framey_official import PLUGINS, archive as official_archive
import platform
plugin = next(p for p in PLUGINS if p['id'] == 125)
data = official_archive(plugin)
self.assertEqual(frame_decky_store.archive_info(data, plugin['sha256'], plugin['name']), plugin['folder'])
result = self.apply('install', data, plugin_id=125, folder=plugin['folder'], version=plugin['version'])
self.assertEqual(result.returncode, 0, result.stderr)
folder = self.folder.parent/plugin['folder']
receipt = json.loads((folder/'.frame-control-store.json').read_text())
self.assertEqual(receipt['compatShims'], ['localsend-digest-native'])
self.assertEqual(json.loads((folder/'plugin.json').read_text())['flags'], [])
expected = (frame_decky_store.LOCALSEND_AARCH64 if platform.machine() == 'aarch64'
else frame_decky_store.LOCALSEND_ORIGINAL)
self.assertEqual(hashlib.sha256((folder/'bin/localsend-core').read_bytes()).hexdigest(), expected)
self.assertEqual((folder/'bin/localsend-core').stat().st_mode & 0o777, 0o700)
with zipfile.ZipFile(io.BytesIO(data)) as source:
for name in ('main.py', 'LICENSE'):
self.assertEqual((folder/name).read_bytes(), source.read(plugin['folder']+'/'+name))
result = self.apply('remove', plugin_id=125, folder=plugin['folder'])
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse(folder.exists())
def test_localsend_exception_does_not_accept_another_archive_or_corrupt_helper(self):
from framey_official import PLUGINS, archive as official_archive
plugin = next(p for p in PLUGINS if p['id'] == 125)
with zipfile.ZipFile(io.BytesIO(official_archive(plugin))) as source:
stream = io.BytesIO()
with zipfile.ZipFile(stream, 'w', zipfile.ZIP_DEFLATED) as target:
for entry in source.infolist():
target.writestr(entry, source.read(entry))
target.writestr(plugin['folder']+'/changed.txt', b'new version')
changed = stream.getvalue()
with self.assertRaisesRegex(ValueError, 'non-ARM64'):
frame_decky_store.archive_info(changed, hashlib.sha256(changed).hexdigest(), plugin['name'])
self.assertEqual(frame_decky_store.apply_script(125, '0'*64), frame_decky_store.APPLY)
with patch.object(frame_decky_store, 'LOCALSEND_AARCH64', '0'*64):
with self.assertRaisesRegex(ValueError, 'SHA-256 mismatch'):
frame_decky_store.apply_script(125, frame_decky_store.LOCALSEND_DIGEST)
def test_localsend_arm64_transaction_checks_written_helper(self):
from framey_official import PLUGINS, archive as official_archive
plugin = next(p for p in PLUGINS if p['id'] == 125)
# The local x86 host only writes the ELF; native ARM execution is gated
# separately by the published Decky runtime runner.
with patch.object(frame_decky_store, 'APPLY',
"import platform\nplatform.machine=lambda:'aarch64'\n"+frame_decky_store.APPLY):
result = self.apply('install', official_archive(plugin), plugin_id=125,
folder=plugin['folder'], version=plugin['version'])
self.assertEqual(result.returncode, 0, result.stderr)
helper = self.folder.parent/plugin['folder']/'bin/localsend-core'
self.assertEqual(hashlib.sha256(helper.read_bytes()).hexdigest(),
frame_decky_store.LOCALSEND_AARCH64)
self.assertEqual(int.from_bytes(helper.read_bytes()[18:20], 'little'), 183)
@unittest.skipUnless(__import__('platform').machine() in ('x86_64', 'aarch64'), 'native Linux wheel architecture')
def test_exact_deckyspy_gets_native_dependency_and_license_only_in_its_folder(self):
from framey_official import PLUGINS, archive as official_archive
+2
View File
@@ -28,6 +28,8 @@ class HeadsetBundle(unittest.TestCase):
self.assertIn('ui/server.py', names)
for filename in ('aarch64.gz', 'x86_64.gz', 'source.tar.gz', 'LICENSE.txt', 'GO-LICENSE.txt', 'README.md'):
self.assertIn('ui/vendor/deckyfileserver-1.2.0/'+filename, names)
for filename in ('aarch64.gz', 'source.tar.gz', 'LICENSE.txt', 'GO-LICENSE.txt', 'README.md'):
self.assertIn('ui/vendor/localsend-0.4.5/'+filename, names)
self.assertIn('scripts/build-framey-fileserver.py', names)
for name in ('aarch64.whl', 'x86_64.whl', 'LICENSE.txt'):
self.assertIn('ui/vendor/psutil-7.2.2/'+name, names)
+2 -1
View File
@@ -19,7 +19,8 @@ class RuntimeCoverage(unittest.TestCase):
self.assertEqual(len(cases), len({case['id'] for case in cases}))
for case in cases:
self.assertIn(case['id'], official)
self.assertIn(case['probe'], ('mpris', 'screenshots', 'system-monitor', 'timer-lifecycle', 'fileserver'))
self.assertIn(case['probe'], ('mpris', 'screenshots', 'system-monitor', 'timer-lifecycle',
'toggle-settings', 'fileserver', 'localsend'))
def test_new_shim_without_runtime_case_fails_coverage(self):
import frame_decky_store
+10
View File
@@ -21,6 +21,16 @@ from framey_official import LEGACY_PLUGINS, PLUGINS, archive
class OfficialBundles(unittest.TestCase):
@unittest.skipUnless(shutil.which('node'), 'Node is needed for the official frontend check')
def test_pause_games_toggle_renders_saved_value_and_dispatches_settings_only(self):
plugin = next(p for p in PLUGINS if p['id'] == 13)
with zipfile.ZipFile(io.BytesIO(archive(plugin))) as bundle, tempfile.TemporaryDirectory() as root:
frontend = Path(root)/'index.js'
frontend.write_bytes(bundle.read(plugin['folder']+'/dist/index.js'))
result = subprocess.run(['node', str(Path(__file__).parent/'page/framey_toggle.mjs'), str(frontend)],
capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
def test_legacy_css_loader_store_archive_has_unversioned_frontend(self):
plugin = LEGACY_PLUGINS[0]
data = archive(plugin)
+65
View File
@@ -0,0 +1,65 @@
"""Store requests can reconnect when the headset panel loses the POST response."""
import sandbox # noqa: F401
import sys
import threading
import time
import unittest
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'ui'))
import server
class StoreJobs(unittest.TestCase):
def test_same_request_id_runs_once_and_conflicting_reuse_is_rejected(self):
request_id = 'a' * 32
started = threading.Event()
finish = threading.Event()
calls = []
def change(_ssh, action, plugin_id, report, reviewed_hash=None):
calls.append((action, plugin_id, reviewed_hash))
started.set()
self.assertTrue(finish.wait(5))
report('Installed', 100)
return {'message': 'Installed once'}
body = {'action': 'install', 'id': 2, 'sha256': 'b' * 64,
'acknowledged': True, 'requestId': request_id}
try:
with patch.object(server.frame_decky_store, 'change', side_effect=change):
first = server.decky_plugin(body)
self.assertTrue(started.wait(5))
self.assertEqual(server.decky_plugin(body), first)
for changed in ({'action': 'update'}, {'id': 3}, {'sha256': 'c' * 64}):
with self.subTest(changed=changed), self.assertRaises(server.Failure) as error:
server.decky_plugin({**body, **changed})
self.assertEqual(error.exception.status, 409)
self.assertEqual(calls, [('install', 2, 'b' * 64)])
self.assertNotIn('_request', server.job_status('id=' + request_id))
finish.set()
for _ in range(100):
if server.job_status('id=' + request_id)['done']:
break
time.sleep(0.01)
self.assertEqual(server.job_status('id=' + request_id)['message'], 'Installed once')
self.assertEqual(server.decky_plugin(body), first)
self.assertEqual(len(calls), 1)
finally:
finish.set()
with server._jobs_lock:
server._jobs.pop(request_id, None)
def test_invalid_request_id_does_not_start_work(self):
with patch.object(server.frame_decky_store, 'change') as change:
for request_id in (1, '', 'A' * 32, 'f' * 31, 'f' * 33, '../' * 11):
with self.subTest(request_id=request_id), self.assertRaises(server.Failure) as error:
server.decky_plugin({'action': 'install', 'id': 2,
'acknowledged': True, 'requestId': request_id})
self.assertEqual(error.exception.status, 400)
change.assert_not_called()
if __name__ == '__main__':
unittest.main()
+6
View File
@@ -309,6 +309,12 @@ class Renderer(unittest.TestCase):
resources = {r['from']: r.get('filter', ['**/*']) for r in config['build']['extraResources']}
self.assertIn('*.js', resources['../ui'])
self.assertIn('*.js', resources['../frame/android'])
self.assertIn('*.gz', resources['../ui/vendor/localsend-0.4.5'])
self.assertIn('*.txt', resources['../ui/vendor/localsend-0.4.5'])
self.assertIn('build-framey-localsend.py', resources['../scripts'])
checker = (ROOT / 'app/build/check-resources.js').read_text()
for name in ('aarch64.gz', 'source.tar.gz', 'LICENSE.txt', 'GO-LICENSE.txt', 'README.md'):
self.assertIn(f'ui/vendor/localsend-0.4.5/{name}', checker)
class SteamGridDB(unittest.TestCase):
+11
View File
@@ -134,6 +134,17 @@ class PublishRelease(unittest.TestCase):
self.assertIsNone(upload)
self.assertFalse(any("-X" in call for call in log))
def test_refuses_a_prior_cancelled_installer_after_successful_rerun(self):
gates = {"release_attempts": 2,
"prior_bad_job": "build (macos-latest, dist, app/dist/*.dmg app/dist/*.zip)",
"prior_bad_job_conclusion": "cancelled"}
p, log, upload = self.run_script([draft()], "v9.8.7", gates=gates)
self.assertNotEqual(p.returncode, 0)
self.assertIn("attempt 1", p.stderr)
self.assertIn("cancelled", p.stderr)
self.assertIsNone(upload)
self.assertFalse(any("-X" in call for call in log))
def test_refuses_missing_or_nonexact_gate_results(self):
for gates in ({"omit_job": "e2e"}, {"main_sha": "c" * 40},
{"decky_conclusion": "failure"}, {"release_conclusion": "failure"},
+13
View File
@@ -95,6 +95,19 @@ class RealProcesses(unittest.TestCase):
class Safety(unittest.TestCase):
def test_locked_preflight_rejects_active_or_unknown_proximity_before_titles(self):
for proximity in ([], [88.0625], [4.6875, 88.0625], [None]):
with self.subTest(proximity=proximity), mock.patch.object(
frame_smoke, 'remote', return_value=mock.Mock(
returncode=0, stdout=json.dumps({
'activity': 3, 'proximity': proximity, 'battery': [72]}))):
with self.assertRaisesRegex(RuntimeError, 'proximity is active or unknown'):
frame_smoke.preflight()
with mock.patch.object(frame_smoke, 'remote', return_value=mock.Mock(
returncode=0, stdout=json.dumps({
'activity': 3, 'proximity': [4.6875], 'battery': [72]}))):
self.assertEqual(frame_smoke.preflight()['proximity'], [4.6875])
def test_missing_x86_runtime_skips_launch_but_still_removes_title(self):
smoke = frame_smoke.Smoke()
gid = 'fc_smoke_x86_64'
+45 -4
View File
@@ -33,6 +33,10 @@ FILESERVER_BINARIES = {
'aarch64': '86b80021ebd62ed619eba065757c1122defa5a74b2c045aa44a0c6b4403a508a',
}
FILESERVER_ORIGINAL = 'd9eab8559c87c01736188bc676f3b4b44dee47cf952b6e571c770d1ea61733e9'
LOCALSEND_DIGEST = 'e271ccb55eb23f918b99a69f79aec905df4c9b2ef7b23bbf4ec1c2d1a6f0c6b7'
LOCALSEND_ORIGINAL = 'd448d03861d578685e6c70ea3ace25eb34fca99cb9d4f98bf51ba23aa5cae843'
LOCALSEND_MAIN = 'db03f9201fe50b75dea99a7a05b54ec3f352d1bdfe174115b0e569154198d8ad'
LOCALSEND_AARCH64 = 'e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae'
VERSION = re.compile(r'[A-Za-z0-9][A-Za-z0-9._+\-]{0,39}\Z')
@@ -101,6 +105,8 @@ def archive_info(data, expected_hash, expected_name):
raise ValueError('Invalid plugin folder')
fileserver = (expected_hash == FILESERVER_DIGEST and expected_name == 'DeckyFileServer'
and folder == 'DeckyFileServer')
localsend = (expected_hash == LOCALSEND_DIGEST and expected_name == 'Decky Localsend'
and folder == 'decky-localsend')
seen = {}
for entry in files:
name = entry.filename
@@ -130,8 +136,10 @@ def archive_info(data, expected_hash, expected_name):
with archive.open(entry) as source:
header = source.read(20)
if header[:4] == b'\x7fELF' and int.from_bytes(header[18:20], 'little') != 183:
if not (fileserver and name == 'DeckyFileServer/bin/backend'
and hashlib.sha256(archive.read(entry)).hexdigest() == FILESERVER_ORIGINAL):
if not ((fileserver and name == 'DeckyFileServer/bin/backend'
and hashlib.sha256(archive.read(entry)).hexdigest() == FILESERVER_ORIGINAL)
or (localsend and name == 'decky-localsend/bin/localsend-core'
and hashlib.sha256(archive.read(entry)).hexdigest() == LOCALSEND_ORIGINAL)):
raise ValueError('Plugin contains a non-ARM64 Linux binary: ' + name)
if header[:2] == b'MZ':
raise ValueError('Plugin contains a Windows binary: ' + name)
@@ -145,6 +153,8 @@ def archive_info(data, expected_hash, expected_name):
raise ValueError('Invalid plugin flags')
if ('root' in flags or '_root' in flags) and not fileserver:
raise ValueError('This plugin needs root and is not supported by the Frame user service')
if localsend and hashlib.sha256(archive.read(folder+'/main.py')).hexdigest() != LOCALSEND_MAIN:
raise ValueError('LocalSend Python source differs from the pinned archive')
return folder
@@ -157,6 +167,12 @@ PSUTIL_DIGESTS = {
def apply_script(plugin_id, digest):
"""Deliver verified offline dependencies only to exact managed archives."""
if plugin_id == 125 and digest == LOCALSEND_DIGEST:
packed = (Path(__file__).parent/'vendor/localsend-0.4.5/aarch64.gz').read_bytes()
data = gzip.decompress(packed)
if hashlib.sha256(data).hexdigest() != LOCALSEND_AARCH64:
raise ValueError('Bundled LocalSend ARM64 helper SHA-256 mismatch')
return ('FRAMEY_LOCALSEND_AARCH64 = '+repr(base64.b64encode(packed).decode('ascii'))+'\n'+APPLY)
if plugin_id == 60 and digest == FILESERVER_DIGEST:
assets = Path(__file__).parent/'vendor/deckyfileserver-1.2.0'
payloads = {}
@@ -227,6 +243,7 @@ try:
if not files or len(files)>3000 or sum(f.file_size for f in files)>150*1024*1024:
raise RuntimeError('Plugin archive too large')
fileserver=(int(plugin_id)==60 and digest=='7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0' and folder=='DeckyFileServer')
localsend=(int(plugin_id)==125 and digest=='e271ccb55eb23f918b99a69f79aec905df4c9b2ef7b23bbf4ec1c2d1a6f0c6b7' and folder=='decky-localsend')
manifest=json.loads(z.read(folder+'/plugin.json'))
if not isinstance(manifest,dict): raise RuntimeError('Invalid plugin manifest')
flags=manifest.get('flags',[])
@@ -257,8 +274,10 @@ try:
if not f.is_dir():
with z.open(f) as source: header=source.read(20)
if header[:4]==b'\x7fELF' and int.from_bytes(header[18:20],'little')!=183:
if not (fileserver and f.filename==folder+'/bin/backend' and
hashlib.sha256(z.read(f)).hexdigest()=='d9eab8559c87c01736188bc676f3b4b44dee47cf952b6e571c770d1ea61733e9'):
if not ((fileserver and f.filename==folder+'/bin/backend' and
hashlib.sha256(z.read(f)).hexdigest()=='d9eab8559c87c01736188bc676f3b4b44dee47cf952b6e571c770d1ea61733e9')
or (localsend and f.filename==folder+'/bin/localsend-core' and
hashlib.sha256(z.read(f)).hexdigest()=='d448d03861d578685e6c70ea3ace25eb34fca99cb9d4f98bf51ba23aa5cae843')):
raise RuntimeError('Plugin contains a non-ARM64 Linux binary')
if header[:2]==b'MZ': raise RuntimeError('Plugin contains a Windows binary')
stage.mkdir(mode=0o700)
@@ -276,6 +295,28 @@ try:
unpacked=stage/folder
if not unpacked.is_dir(): raise RuntimeError('Missing plugin folder')
compat_shims=[]
if localsend:
helper=unpacked/'bin/localsend-core'
source=unpacked/'main.py'
if hashlib.sha256(helper.read_bytes()).hexdigest()!='d448d03861d578685e6c70ea3ace25eb34fca99cb9d4f98bf51ba23aa5cae843':
raise RuntimeError('LocalSend original helper mismatch')
if hashlib.sha256(source.read_bytes()).hexdigest()!='db03f9201fe50b75dea99a7a05b54ec3f352d1bdfe174115b0e569154198d8ad':
raise RuntimeError('LocalSend Python source mismatch')
if manifest.get('name')!='Decky Localsend' or manifest.get('api_version')!=1 or manifest.get('flags')!=[]:
raise RuntimeError('LocalSend manifest mismatch')
arch=platform.machine()
if arch=='aarch64':
packed=globals().get('FRAMEY_LOCALSEND_AARCH64')
if not isinstance(packed,str): raise RuntimeError('Missing LocalSend ARM64 helper')
native=gzip.decompress(base64.b64decode(packed,validate=True))
if (hashlib.sha256(native).hexdigest()!='e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae'
or native[:4]!=b'\x7fELF' or int.from_bytes(native[18:20],'little')!=183):
raise RuntimeError('LocalSend ARM64 helper mismatch')
helper.write_bytes(native)
elif arch!='x86_64':
raise RuntimeError('Unsupported LocalSend helper architecture')
helper.chmod(0o700)
compat_shims.append('localsend-digest-native')
# GPL source and reproducible build recipe ship with Frame Control.
# The exact official helper is x86_64; this plugin needs no root.
if int(plugin_id)==60 and digest=='7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0' and folder=='DeckyFileServer':
+10 -1
View File
@@ -137,7 +137,16 @@ class Handler(BaseHTTPRequestHandler):
return self.reply(403, {'error': 'Forbidden'})
try:
size = int(self.headers.get('Content-Length', '0'))
if not 0 < size <= 1024:
if size > 1024:
# Windows can reset a connection closed with an unread POST
# body before the client receives our 400 response. Drain only
# a bounded rejected body; never buffer an unbounded payload.
if size <= 64 * 1024:
self.rfile.read(size)
else:
self.close_connection = True
raise ValueError('Invalid request size')
if size <= 0:
raise ValueError('Invalid request size')
body = json.loads(self.rfile.read(size))
if not isinstance(body, dict):
+76 -17
View File
@@ -183,7 +183,7 @@
"https://github.com/suchmememanyskill/SDH-CssLoader.git"
],
"status": "partial",
"reason": "Panel initialization succeeds; CSS themes patch Steam UI and cannot theme the isolated VR shell.",
"reason": "Panel initialization succeeds; CSS themes patch Steam UI and cannot theme the isolated VR shell. Shared Toggle value rendering is repaired; other listed blockers remain.",
"blockInstall": false,
"verification": [
"static archive",
@@ -238,6 +238,11 @@
"sha256": "99beaf1374af82408e6d056e0b0147e5fca544e1035fa7ab88419231697bc231",
"archiveValidation": "accepted",
"result": "installed-removed"
},
"toggleContract": {
"verified": "local browser control fixture; official Pause Games callback regression",
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
"backendVerified": false
}
},
{
@@ -584,12 +589,13 @@
"https://github.com/wynn1212/SDH-PauseGames.git"
],
"status": "partial",
"reason": "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.",
"reason": "Settings and a synthetic running-game list render locally. Actual Decky 3.3.0 discovery, private settings writes/readback and exclusion-list persistence pass on amd64 and ARM64 Frame; standalone Toggle now honors its value prop. Live empty-list/lifetime checks passed earlier. Real process pause/resume, automatic pausing, suspend integration and worn VR remain unverified/unsupported.",
"blockInstall": false,
"verification": [
"static archive",
"local browser",
"fake-Frame"
"fake-Frame",
"real Frame SSH"
],
"browser": {
"id": 13,
@@ -622,6 +628,11 @@
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
},
"toggleContract": {
"verified": "local browser control fixture; official Pause Games callback regression",
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
"backendVerified": true
}
},
{
@@ -1962,7 +1973,7 @@
"https://github.com/jurassicplayer/decky-autoflatpaks.git"
],
"status": "broken",
"reason": "This plugin needs root and is not supported by the Frame user service.",
"reason": "This plugin needs root and is not supported by the Frame user service. Shared Toggle value rendering is repaired; other listed blockers remain.",
"blockInstall": true,
"verification": [
"static archive",
@@ -2004,6 +2015,11 @@
"archiveValidation": "This plugin needs root and is not supported by the Frame user service",
"result": "blocked",
"reason": "ValueError: Root plugins are not supported by the Frame user service"
},
"toggleContract": {
"verified": "local browser control fixture; official Pause Games callback regression",
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
"backendVerified": false
}
},
{
@@ -4333,7 +4349,7 @@
"https://github.com/grimkor/DeckyFileServer"
],
"status": "partial",
"reason": "Managed install supplies a native ARM64 helper and removes the exact archive's unnecessary root request. Real Decky 3.3.0 on Frame passes private read-only HTTPS listing/download and upload denial. Frontend address/picker and thumbnail-toggle display limitations remain; use HTTPS and a preconfigured directory. Uploads, generated thumbnails and worn VR are unverified.",
"reason": "Managed install supplies a native ARM64 helper and removes the exact archive's unnecessary root request. Real Decky 3.3.0 on Frame passes private read-only HTTPS listing/download and upload denial. Frontend address/picker limitations remain; use HTTPS and a preconfigured directory. Uploads, generated thumbnails and worn VR are unverified. Shared Toggle value rendering is repaired; other listed blockers remain.",
"blockInstall": false,
"verification": [
"static archive",
@@ -4376,6 +4392,11 @@
"helperSha256": "86b80021ebd62ed619eba065757c1122defa5a74b2c045aa44a0c6b4403a508a",
"sourceCommit": "dcf3f3ce5104d05811fa8c156c4f58632b84b313",
"verification": "Real Decky discovery/public RPC and private read-only HTTPS fixture; docs/evidence/framey-fileserver-arm64-2026-10-27.md"
},
"toggleContract": {
"verified": "local browser control fixture; official Pause Games callback regression",
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
"backendVerified": false
}
},
{
@@ -4802,7 +4823,7 @@
"https://github.com/theCapypara/steamdeck-decky-syncthing.git"
],
"status": "broken",
"reason": "Plugin contains a non-ARM64 Linux binary: decky-syncthing/bin/decky-syncthing-watchdog.",
"reason": "Plugin contains a non-ARM64 Linux binary: decky-syncthing/bin/decky-syncthing-watchdog. Shared Toggle value rendering is repaired; other listed blockers remain.",
"blockInstall": true,
"verification": [
"static archive",
@@ -4843,6 +4864,11 @@
"archiveValidation": "Plugin contains a non-ARM64 Linux binary: decky-syncthing/bin/decky-syncthing-watchdog",
"result": "blocked",
"reason": "ValueError: Plugin contains a non-ARM64 Linux binary: decky-syncthing/bin/decky-syncthing-watchdog."
},
"toggleContract": {
"verified": "local browser control fixture; official Pause Games callback regression",
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
"backendVerified": false
}
},
{
@@ -9841,7 +9867,7 @@
"https://github.com/xXJSONDeruloXx/lsfg-vk"
],
"status": "partial",
"reason": "Initializer succeeds; runtime LSFG/Windows DLL downloads and Vulkan integration are not ARM64-validated.",
"reason": "Initializer succeeds; runtime LSFG/Windows DLL downloads and Vulkan integration are not ARM64-validated. Shared Toggle value rendering is repaired; other listed blockers remain.",
"blockInstall": false,
"verification": [
"static archive",
@@ -9890,6 +9916,11 @@
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
},
"toggleContract": {
"verified": "local browser control fixture; official Pause Games callback regression",
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
"backendVerified": false
}
},
{
@@ -10211,7 +10242,7 @@
"https://github.com/wtlnetwork/muon.git"
],
"status": "broken",
"reason": "This plugin needs root and is not supported by the Frame user service.",
"reason": "This plugin needs root and is not supported by the Frame user service. Shared Toggle value rendering is repaired; other listed blockers remain.",
"blockInstall": true,
"verification": [
"static archive",
@@ -10255,6 +10286,11 @@
"archiveValidation": "This plugin needs root and is not supported by the Frame user service",
"result": "blocked",
"reason": "ValueError: Root plugins are not supported by the Frame user service"
},
"toggleContract": {
"verified": "local browser control fixture; official Pause Games callback regression",
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
"backendVerified": false
}
},
{
@@ -11106,13 +11142,17 @@
"https://github.com/moyoez/decky-localsend",
"https://github.com/moyoez/decky-localsend.git"
],
"status": "broken",
"reason": "Plugin contains a non-ARM64 Linux binary: decky-localsend/bin/localsend-core.",
"blockInstall": true,
"status": "partial",
"reason": "Exact managed 0.4.5 archive replaces its x86 helper with a pinned ARM64 build. Published Decky 3.3.0 ARM64 stopped status/configuration RPCs and a private real-Frame ARM64 helper marker transfer passed. The managed service, user-facing discovery/transfer, Steam routes and worn VR remain unverified.",
"blockInstall": false,
"verification": [
"static archive",
"local browser",
"fake-Frame"
"exact archive and source hashes",
"local Chromium initial panel",
"host managed transaction",
"fake Frame managed install/remove",
"private x86 backend transfer",
"published Decky 3.3.0 AMD64/ARM64 stopped status/configuration RPCs",
"private real Frame ARM64 helper marker transfer"
],
"browser": {
"id": 125,
@@ -11138,7 +11178,7 @@
},
"backend": "Python + native payload",
"hardware": "No specific hardware requirement detected statically; runtime requirements unverified.",
"nativePort": "MIT LocalSend-Go 66edc509a28b9f877e7374504892dcbc653a4da4 reproducibly cross-built with Go 1.25.7/CGO_ENABLED=0 for ARM64; SHA-256 e79238f6051ce34f172f765ae918d3d902b819e27377989a55e5b1eba10e46f9. Help/official backend CLI flags and protocol 2.0 info endpoint passed on real Frame in an isolated network namespace. No file transfer or full plugin/CEF integration tested; exact source match to shipped helper unproven. Official ZIP stays x86_64 and blocked.",
"nativePort": "Exact official x86 helper is reproducible from pinned MIT source; the managed installer replaces only the exact 0.4.5 digest with SHA-256 e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae on ARM64. The Frame rechecks original Python/helper bytes, replacement digest and ELF machine; x86 keeps the original helper. Local official x86 backend/proxy and real Frame rebuilt ARM64 helper each passed one private marker transfer. Published Decky 3.3.0 AMD64/ARM64 discovery and stopped status/configuration RPCs passed. The actual managed Decky service on Frame, network discovery, user-facing transfer, Steam routes and worn VR remain unverified (docs/evidence/framey-localsend-managed-2026-10-27.md).",
"uiSurfaces": [
"local/Steam routes",
"toasts",
@@ -11150,6 +11190,15 @@
"archiveValidation": "Plugin contains a non-ARM64 Linux binary: decky-localsend/bin/localsend-core",
"result": "blocked",
"reason": "ValueError: Plugin contains a non-ARM64 Linux binary: decky-localsend/bin/localsend-core."
},
"requiredShims": [
"localsend-digest-native"
],
"managedNative": {
"shim": "localsend-digest-native",
"helperSha256": "e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae",
"sourceCommit": "ad28b8e9440e17e44b4735a6c4aa0512009cf333",
"verification": "Host transaction and fake Frame managed install/remove; local original x86 backend private marker transfer. Published Decky ARM64 runtime and real Frame transfer pending (docs/evidence/framey-localsend-managed-2026-10-27.md)."
}
},
{
@@ -11238,7 +11287,7 @@
"https://github.com/casey/just"
],
"status": "broken",
"reason": "Plugin contains a non-ARM64 Linux binary: decky-launch-options/bin/hello. Frontend audit failed: Cannot convert undefined or null to object.",
"reason": "Plugin contains a non-ARM64 Linux binary: decky-launch-options/bin/hello. Frontend audit failed: Cannot convert undefined or null to object. Shared Toggle value rendering is repaired; other listed blockers remain.",
"blockInstall": true,
"verification": [
"static archive",
@@ -11272,6 +11321,11 @@
"archiveValidation": "Plugin contains a non-ARM64 Linux binary: decky-launch-options/bin/hello",
"result": "blocked",
"reason": "ValueError: Plugin contains a non-ARM64 Linux binary: decky-launch-options/bin/hello. Frontend audit failed: Cannot convert undefined or null to object."
},
"toggleContract": {
"verified": "local browser control fixture; official Pause Games callback regression",
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
"backendVerified": false
}
},
{
@@ -11556,7 +11610,7 @@
"https://github.com/tikh-iv/ShadowDeck.git"
],
"status": "broken",
"reason": "This plugin needs root and is not supported by the Frame user service.",
"reason": "This plugin needs root and is not supported by the Frame user service. Shared Toggle value rendering is repaired; other listed blockers remain.",
"blockInstall": true,
"verification": [
"static archive",
@@ -11594,6 +11648,11 @@
"archiveValidation": "This plugin needs root and is not supported by the Frame user service",
"result": "blocked",
"reason": "ValueError: Root plugins are not supported by the Frame user service"
},
"toggleContract": {
"verified": "local browser control fixture; official Pause Games callback regression",
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
"backendVerified": false
}
},
{
+33 -9
View File
@@ -56,6 +56,7 @@
const pending = new Map();
const routes = new Map();
const routeListeners = new Set();
const routeHistory = [];
let currentRoute = null;
let currentParams = {};
let nextId = 0;
@@ -219,6 +220,11 @@
};
const Field = ({label, description, children}) => R.createElement('label', null,
R.createElement('strong', null, label), description && R.createElement('small', null, description), children);
// DFL/@decky/ui Toggle uses value; ToggleField uses checked. Keep the
// standalone control separate so persisted true settings render correctly.
const Toggle = ({value, disabled, onChange}) => R.createElement('input', {
type:'checkbox', checked:!!value, disabled:!!disabled,
onChange:e=>{if(!disabled)onChange?.(e.target.checked);}});
const ToggleField = ({label, description, checked, disabled, onChange}) => R.createElement(Field, {label, description},
R.createElement('input', {type:'checkbox', checked:!!checked, disabled:!!disabled,
onChange:e=>{if(!disabled)onChange?.(e.target.checked);}}));
@@ -278,8 +284,10 @@
title && R.createElement('h2', null, title), R.createElement(Tabs, {
tabs:pages.filter(page=>page.visible!==false).map((page,index)=>({
id:page.route || String(index), title:page.title, content:page.content}))}));
const ProgressBar = ({nProgress=0, value, max=100}) => R.createElement('progress', {
value:Math.max(0,Math.min(max,Number(value ?? nProgress)||0)), max});
const ProgressBar = ({nProgress=0, value, max=100, indeterminate=false}) => R.createElement('progress', {
value:indeterminate ? undefined : Math.max(0,Math.min(max,Number(value ?? nProgress)||0)), max});
const ProgressBarWithInfo = ({sOperationText, ...props}) => R.createElement('div', null,
ProgressBar(props), sOperationText == null ? null : R.createElement('small', {role:'status'}, sOperationText));
const MenuItem = ({children, disabled, bDisabled, onClick, onSelected}) =>
R.createElement('button', {type:'button',disabled:!!(disabled || bDisabled),
onClick:event=>{if(!disabled && !bDisabled)(onClick || onSelected)?.(event);}}, children);
@@ -368,15 +376,31 @@
}
return null;
}
function navigate(path) {
if (path !== null && !localPath(path)) return false;
const match = path === null ? null : activeRoute(path);
if (path !== null && !match) return false;
function showRoute(path, match) {
currentRoute = path;
currentParams = match?.params || {};
for (const listener of routeListeners) listener(path);
return true;
}
function navigate(path) {
if (path !== null && !localPath(path)) return false;
const match = path === null ? null : activeRoute(path);
if (path !== null && !match) return false;
if (path === null) routeHistory.length = 0;
else if (path !== currentRoute) {
routeHistory.push(currentRoute);
if (routeHistory.length > 32) routeHistory.shift();
}
return showRoute(path, match);
}
function navigateBack() {
while (routeHistory.length) {
const previous = routeHistory.pop();
const match = previous === null ? null : activeRoute(previous);
if (previous === null || match) return showRoute(previous, match);
}
return navigate(null);
}
const routerHook = {
addRoute(path, component) {
if (!routePattern(path) ||
@@ -476,7 +500,7 @@
Focusable, ScrollPanel: passthrough('div'), ScrollPanelGroup: passthrough('div'),
DialogButton: passthrough('button'), DialogButtonPrimary: passthrough('button'), ConfirmModal,
DialogButtonSecondary: passthrough('button'), Button: ButtonItem, GamepadButton: passthrough('button'),
Toggle: ToggleField, Spinner, SteamSpinner:Spinner, Tabs, SidebarNavigation,
Toggle, Spinner, SteamSpinner:Spinner, Tabs, SidebarNavigation,
SuspensefulImage:({alt='', ...props})=>R.createElement('img', {...props, alt}),
Marquee:passthrough('span'), DialogBody:passthrough('div'), DialogBodyText:passthrough('p'),
DialogHeader:passthrough('h2'), DialogSubHeader:passthrough('h3'), DialogFooter:passthrough('footer'),
@@ -484,14 +508,14 @@
DialogControlsSectionHeader:passthrough('h3'), ModalRoot:passthrough('section'),
ModalPosition:passthrough('div'),
Menu:passthrough('div'), MenuGroup:passthrough('section'), MenuItem,
ProgressBar, ProgressBarWithInfo:ProgressBar,
ProgressBar, ProgressBarItem:ProgressBar, ProgressBarWithInfo,
gamepadDialogClasses:{}, quickAccessMenuClasses:{}, gamepadTabbedPageClasses:{},
gamepadSliderClasses:{}, gamepadContextMenuClasses:{}, scrollPanelClasses:{},
quickAccessControlsClasses:{}, mainMenuAppRunningClasses:{}, libraryAssetImageClasses:{},
joinClassNames:(...names)=>names.filter(Boolean).join(' '),
sleep:ms=>new Promise(resolve=>setTimeout(resolve,Math.max(0,Number(ms)||0))),
staticClasses: {Title:'framey-title'},
Navigation: {Navigate:navigate, NavigateBack:()=>navigate(null), CloseSideMenus:()=>navigate(null),
Navigation: {Navigate:navigate, NavigateBack:navigateBack, CloseSideMenus:()=>navigate(null),
NavigateToExternalWeb: url => { if (/^https:\/\//.test(url)) window.open(url, '_blank', 'noopener'); }},
showModal, showContextMenu:showModal,
// Steam app, library, and store patches have no target inside this panel.
+12 -7
View File
@@ -72,7 +72,8 @@ const byId=id=>document.getElementById(id);
let latestStatus=null, activeExtension=null, recordBusy=false;
async function api(path,body){const response=await fetch(path,{method:body?'POST':'GET',
headers:{'X-Frame-UI':key,'Content-Type':'application/json'},body:body?JSON.stringify(body):undefined});
const data=await response.json();if(!response.ok)throw Error(data.error||'Frame request failed');return data;}
const data=await response.json();if(!response.ok){const error=Error(data.error||'Frame request failed');
error.status=response.status;throw error;}return data;}
async function refresh(){try{const status=latestStatus=await api('/api/status');
byId('battery').textContent=status.battery?.percent==null?'Unavailable':`${status.battery.percent}%`;
const free=status.disk?.home?.free;
@@ -107,9 +108,8 @@ async function refreshGames(){const list=byId('games');try{const data=await api(
row.append(name,quit);list.append(row);}if(!data.running.length)list.textContent='No games are running.';
}catch(error){list.textContent=error.message;}}
function compatibilityText(plugin){const c=plugin.compatibility;return c ? `${c.status === 'na' ? 'N/A' : c.status}: ${c.reason}${c.verification?.length ? ' Verified: '+c.verification.join(', ')+'.' : ''}` : 'Unverified on Frame';}
// Some Chromium profiles deny sessionStorage. Keep the accepted job ID in
// memory so this open panel can still reconnect instead of offering a second
// install while the first one may be running.
// Some Chromium profiles deny sessionStorage. Keep the reserved request ID in
// memory so this open panel can still reconnect if the POST response is lost.
let deckyPendingJob=null;
function savedDeckyJob(){
if(deckyPendingJob)return deckyPendingJob;
@@ -124,6 +124,8 @@ function clearDeckyJob(){
try{sessionStorage.removeItem('frameyPluginJob');}catch(_){}
}
function deckyJobPending(){return !!savedDeckyJob();}
function newDeckyRequestId(){return Array.from(crypto.getRandomValues(new Uint8Array(16)),
byte=>byte.toString(16).padStart(2,'0')).join('');}
let deckyState=null, deckyStore=[], deckyStoreIndex=0, deckyStoreRequest=0,
deckyStatusRequest=0,
activeFrameyId=null, activeFrameyReceipt=null, frameySession=null, deckyBusy=false;
@@ -200,7 +202,7 @@ async function followDeckyJob(job,action,name){
catch(error){
if(/no such job/i.test(error.message)){
clearDeckyJob();
throw Error('The app restarted and lost this job result. Refresh installed plugins to see what changed.');
throw Error('The server has no record of this request. It may not have arrived, or the app may have restarted. Refresh installed plugins before trying again.');
}
throw error;
}
@@ -224,14 +226,17 @@ async function deckyChange(action,plugin){if(deckyBusy)return;
if(panelClosed)closeFrameyPanel();
deckyJobView(`${action} ${plugin.name}`,
panelClosed?'Starting… Old plugin panel closed until the change finishes.':'Starting…',0);
const requestId=newDeckyRequestId();
rememberDeckyJob({job:requestId,action,name:plugin.name});
try{const started=await api('/api/decky/plugin',{action,id:plugin.id,
sha256:action==='remove'?undefined:plugin.sha256,acknowledged:true});
sha256:action==='remove'?undefined:plugin.sha256,acknowledged:true,requestId});
if(!started.job)throw Error('The server did not return a job ID');
rememberDeckyJob({job:started.job,action,name:plugin.name});
await followDeckyJob(started.job,action,plugin.name);
clearDeckyJob();
if(panelNote)byId('deckyJobStatus').textContent+=panelNote;
}catch(error){deckyJobView(`${action} ${plugin.name}`,`Error: ${error.message}${panelNote}`,null,true);}
}catch(error){if(error.status>=400&&error.status<500)clearDeckyJob();
deckyJobView(`${action} ${plugin.name}`,`Error: ${error.message}${panelNote}`,null,true);}
finally{deckyBusy=false;await refreshDecky();}
}
async function checkDeckyJob(){const saved=savedDeckyJob();
+25 -6
View File
@@ -345,11 +345,16 @@ def backfill_art(apps=(), titles=(), meant=None):
return True
def _new_job_locked(label, **fields):
def _prune_jobs_locked():
now = time.time()
for j in [j for j, v in _jobs.items() if v["done"] and now - v["time"] > JOB_TTL]:
del _jobs[j]
job = secrets.token_hex(8)
def _new_job_locked(label, job_id=None, **fields):
_prune_jobs_locked()
now = time.time()
job = job_id or secrets.token_hex(8)
_jobs[job] = {"label": label, "done": False, "error": None, "message": None, "result": None, "time": now, **fields}
return job
@@ -376,10 +381,18 @@ def _run_job(job, label, work, progress):
_jobs[job].update(fields, done=True, time=time.time())
def start_job(label, work, progress=False):
def start_job(label, work, progress=False, *, request_id=None, request=None):
"""Run work() in the background. It returns a dict with a "message"."""
with _jobs_lock:
job = _new_job_locked(label)
if request_id is not None:
_prune_jobs_locked()
existing = _jobs.get(request_id)
if existing:
if existing.get('_request') != request:
raise Failure('This plugin request ID was already used for a different change', 409)
return {"message": f"{label}…", "job": request_id}
job = _new_job_locked(label, job_id=request_id,
**({'_request': request} if request_id is not None else {}))
busy_thread(_run_job, job, label, work, progress).start()
return {"message": f"{label}…", "job": job}
@@ -462,7 +475,7 @@ def queue_action(body):
def job_status(query):
with _jobs_lock:
job = _jobs.get((parse_qs(query).get("id") or [""])[0])
snapshot = job and {k: v for k, v in job.items() if k != "time"}
snapshot = job and {k: v for k, v in job.items() if k not in ("time", "_request")}
if not snapshot:
raise Failure("no such job (the app may have restarted)", 404)
return snapshot
@@ -572,9 +585,15 @@ def decky_plugin(body):
if body.get('acknowledged') is not True:
raise Failure('Confirm that store plugins run code as your Frame user', 400)
reviewed_hash = body.get('sha256')
request_id = body.get('requestId')
if request_id is not None and (not isinstance(request_id, str) or
not re.fullmatch(r'[0-9a-f]{32}', request_id)):
raise Failure('Invalid plugin request ID', 400)
device = LINK.active_device()['id'] if LINK else FRAME
return start_job('Decky plugin ' + action,
lambda report: frame_decky_store.change(ssh, action, plugin_id, report,
reviewed_hash=reviewed_hash), progress=True)
reviewed_hash=reviewed_hash), progress=True,
request_id=request_id, request=(device, action, plugin_id, reviewed_hash))
def framey_method_action(body):
+27
View File
@@ -0,0 +1,27 @@
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 MoeMagicMango
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+43
View File
@@ -0,0 +1,43 @@
# Decky Localsend 0.4.5 native helper
The official Decky store ZIP SHA-256 is
`e271ccb55eb23f918b99a69f79aec905df4c9b2ef7b23bbf4ec1c2d1a6f0c6b7`.
Its Python backend matches upstream plugin tag `v0.45` byte for byte. That
tag pins LocalSend-Go commit `ad28b8e9440e17e44b4735a6c4aa0512009cf333`.
Building that source with Go 1.25.7, CGO disabled, `-trimpath`,
`-buildvcs=false` and `-ldflags='-s -w'` reproduces the published x86_64
helper **byte for byte**. The same source produced the ARM64 helper twice
with identical hashes.
| Artifact | SHA-256 |
| --- | --- |
| Official x86_64 helper | `d448d03861d578685e6c70ea3ace25eb34fca99cb9d4f98bf51ba23aa5cae843` |
| Rebuilt ARM64 helper | `e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae` |
| `aarch64.gz` | `2c4e24dd00d67c6d4f2125b42f1eedc0fe8bcedcbf804be0c7ae91e7b391d5f9` |
| `source.tar.gz` | `f1f86292624b989d540689dbfad503bf15f3937d156978157b8dd418d54415d1` |
`source.tar.gz` is a Git archive of the exact LocalSend-Go commit, including
`go.mod` and `go.sum`. Go modules are **not vendored**: reproducing the
build requires access to checksum-verified dependencies or a populated module
cache. Run the following to check both expected helper hashes:
```sh
python3 scripts/build-framey-localsend.py \
--go /path/to/go1.25.7/bin/go \
--output /tmp/framey-localsend-build
```
The helper shipped to Frame needs no compiler, modules, root service or
runtime download. `LICENSE.txt` is the upstream MIT license; `GO-LICENSE.txt`
is the Go runtime BSD license. The official plugin ZIP retains its own
BSD-3-Clause license unchanged.
Only the exact official ZIP can use this managed adaptation. The Frame
transaction checks its original x86 helper and Python backend hashes,
checks the replacement ARM64 SHA-256 and ELF machine locally and again on the
headset, then records a managed receipt. Other native archives remain blocked.
The official x86 backend and the rebuilt ARM64 helper each passed a single
private no-route marker transfer. Published Decky 3.3.0 AMD64/ARM64 runtime
checks discovered the plugin and read its stopped status and configuration.
The actual managed Decky service on Frame, user-facing transfer, headset UI
and worn use remain unverified. See
`docs/evidence/framey-localsend-managed-2026-10-27.md` and its linked result.
Binary file not shown.
Binary file not shown.