Compare commits

...
Author SHA1 Message Date
SAPHID 792e9c016e Merge remote-tracking branch 'origin/main' into release/v0.4.39 2026-10-26 01:14:31 +00:00
Alex Southwell aeac07cf54 Merge pull request #276 from saphid/docs/framey-rich-toast-sample
Framey: show rich toasts in the author sample
2026-10-26 01:14:15 +00:00
SAPHID d90d111218 Prepare Frame Control 0.4.39 2026-10-26 01:07:02 +00:00
SAPHID 8fc1117209 Demonstrate rich Framey toasts in author sample 2026-10-26 01:05:55 +00:00
Alex Southwell 442b5ecfd8 Merge pull request #264 from saphid/docs/real-mac-air-test
Document the physical Intel Mac test and honest opening steps
2026-10-26 01:05:41 +00:00
Alex Southwell 5afffbef89 Merge pull request #262 from saphid/fleet/es4ntcpvra
Build native Intel Mac installers and check bundled architectures
2026-10-26 01:05:16 +00:00
Alex Southwell 76f619280a Merge pull request #275 from saphid/docs/framey-localsend-arm64
Framey: verify a reproducible ARM64 LocalSend core in isolation
2026-10-26 00:58:53 +00:00
Alex Southwell 2b92599d94 Merge pull request #274 from saphid/feat/framey-steam-reads
Framey: relay three read-only SteamClient APIs from the Frame
2026-10-26 00:58:12 +00:00
Alex Southwell d87b863aa3 Merge pull request #273 from saphid/release/v0.4.38
Prepare Frame Control v0.4.38
2026-10-26 00:51:38 +00:00
SAPHID 6aa74619d5 Document reproducible ARM64 LocalSend core and isolated Frame health probe 2026-10-26 00:46:39 +00:00
Alex Southwell 0a463dea23 Merge remote-tracking branch 'origin/main' into docs/real-mac-air-test
# Conflicts:
#	docs/current-support-matrix.md
2026-10-26 11:44:18 +11:00
SAPHID e44ea65080 Record refreshed store and full frontend regression results 2026-10-26 00:43:11 +00:00
SAPHID ebb1091a37 Merge remote-tracking branch 'origin/main' into release/v0.4.38 2026-10-26 00:41:48 +00:00
Alex Southwell e6430c39ed Merge pull request #272 from saphid/docs/v0437-smoke-evidence
Record v0.4.37 real Frame smoke evidence
2026-10-26 00:41:35 +00:00
SAPHID b518f8c741 Note managed plugin name collision guard in release matrix 2026-10-26 00:41:18 +00:00
SAPHID 1bb5ed6216 Merge remote-tracking branch 'origin/main' into feat/framey-steam-reads 2026-10-26 00:40:46 +00:00
SAPHID 93a4ae95fa Prepare Frame Control 0.4.38 2026-10-26 00:40:46 +00:00
SAPHID e3bb608dde Read allowlisted SteamClient data from the Frame CEF context 2026-10-26 00:40:46 +00:00
Alex Southwell f2e73d98f4 Merge pull request #271 from saphid/feat/framey-ui-fallbacks
Framey: restore detached settings and private Steam JSON cache
2026-10-26 00:39:30 +00:00
Alex Southwell 78d284b154 Merge pull request #270 from saphid/feat/framey-next-compat
Framey: relay real Decky backend events to open panels
2026-10-26 00:33:44 +00:00
SAPHID 2c4581f122 Record v0.4.37 real Frame release smoke 2026-10-26 00:31:56 +00:00
Alex Southwell 4372c913a6 Merge pull request #269 from saphid/fix/framey-update-name-collision
Framey: reject plugin name collisions on update
2026-10-26 00:26:08 +00:00
Alex Southwell 5239dae1a8 Merge pull request #268 from saphid/release/v0.4.37
Prepare Frame Control v0.4.37
2026-10-26 00:25:20 +00:00
SAPHID 9bc5294f36 Reject Decky plugin name collisions during updates 2026-10-26 00:15:48 +00:00
SAPHID 7ed08c5bbd Document Framey toast limits for 0.4.37 2026-10-26 00:14:17 +00:00
SAPHID 449146bbdb Merge remote-tracking branch 'origin/main' into release/v0.4.37 2026-10-26 00:14:04 +00:00
SAPHID 30fbadbb23 Prepare Frame Control 0.4.37 2026-10-26 00:12:23 +00:00
Alex Southwell 19e8b826c6 Merge remote-tracking branch 'origin/main' into docs/real-mac-air-test 2026-10-26 10:35:28 +11:00
Alex Southwell aea65bb282 Merge remote-tracking branch 'origin/main' into fleet/es4ntcpvra 2026-10-26 10:34:56 +11:00
Alex Southwell 2c7b93f756 Fix lipo architecture verification argument order 2026-10-26 10:34:56 +11:00
Alex Southwell 879e84468b Record physical Intel Mac testing and clarify unsigned app opening 2026-10-26 10:30:30 +11:00
Alex Southwell 6ea9eb2618 Build native Intel Mac installers and verify bundled architectures 2026-10-26 10:20:58 +11:00
40 changed files with 737 additions and 65 deletions

No files matched your search

+14
View File
@@ -70,6 +70,9 @@ jobs:
- os: macos-latest
script: dist
files: app/dist/*.dmg app/dist/*.zip
- os: macos-15-intel
script: dist:mac:x64
files: app/dist/*.dmg app/dist/*.zip
- os: windows-latest
script: dist:win
files: app/dist/*.exe app/dist/*.zip
@@ -110,6 +113,17 @@ jobs:
run: npm ci && npm run ${{ matrix.script }}
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
- name: Check the Mac bundle matches its runner
if: runner.os == 'macOS'
shell: bash
run: |
bundle=$(find app/dist -maxdepth 2 -name 'Frame Control.app' -type d -print -quit)
test -n "$bundle"
resources="$bundle/Contents/Resources"
for binary in "$bundle/Contents/MacOS/Frame Control" "$resources/python/bin/python3" "$resources/tools/adb" "$resources/mac/bin/frame-mac-view"; do
lipo "$binary" -verify_arch "$(uname -m)"
done
"$resources/python/bin/python3" -I -B -c 'import http.server, ssl, sqlite3; print(ssl.OPENSSL_VERSION)'
- name: Launch the built Linux AppImage
if: matrix.os == 'ubuntu-latest'
run: python3 scripts/check-appimage-launch.py app/dist/Frame-Control-linux-x86_64.AppImage
+28 -5
View File
@@ -173,16 +173,39 @@ distribution's. If you already have `adb`, the app uses yours.
<details>
<summary><b>macOS: the app isn't notarized</b></summary>
There's no paid Apple developer account behind it, so macOS says the app is
damaged or can't be checked. Drag it to Applications, then clear the download
quarantine once, only for a download you trust from the releases linked above:
The app has an **ad hoc signature**, no Apple Developer ID signature, and no
notarization. The DMG is unsigned. First verify the download against the
`SHA256SUMS` from the same release, then open the DMG and drag **Frame Control**
to **Applications**.
Check your processor before opening: v0.4.36 and earlier publish only
`mac-arm64` for Apple Silicon. On an Intel Mac, this build cannot run;
removing quarantine or installing Rosetta does not make an ARM app run on Intel.
On a real Intel MacBook Air with macOS 15.7.7, opening v0.4.35 and v0.4.36
reported: **“The application cannot be opened because it has an incorrect
executable format.”** This is an architecture failure, not an observed
Gatekeeper warning. See the [real-Mac test record](docs/evidence/mac-desktop-2026-10-26.md).
On a compatible Mac, if macOS blocks an unverified developer, try opening the
app once, then go to **System Settings → Privacy & Security → Open Anyway**
and confirm **Open**. Only do this for a download you verified and trust.
[Apple's opening instructions](https://support.apple.com/102445) describe the
current security controls. The exact warning and this override flow have not
yet been verified here on Apple Silicon.
If a trusted, verified copy still reports “damaged” and offers no Open Anyway
option, the Terminal fallback removes only this app's download quarantine
(it does not sign or notarize the app):
```sh
xattr -dr com.apple.quarantine "/Applications/Frame Control.app"
```
The first time, macOS also asks to allow local network access (for SSH) and
control of Terminal (for the password prompts).
Then open it from Applications. Do not disable Gatekeeper globally. Depending
on the features you use, macOS may also request Local Network access, permission
to control Terminal, or Screen Recording; approve only the feature you intend
to use. Those permission prompts were not verified in the release test above.
</details>
<details>
+2 -1
View File
@@ -4,7 +4,7 @@
// copies into the app's resources. Also KDE Connect for the Frame (the same arm64
// packages for every build), into ../frame/kdeconnect/packages as listed in
// ../frame/kdeconnect/packages.json. Everything is pinned by version and SHA-256.
// node build/fetch-deps.js mac arm64 | win x64 | linux x64 arm64
// node build/fetch-deps.js mac arm64 x64 | win x64 | linux x64 arm64
const crypto = require("crypto");
const fs = require("fs");
const https = require("https");
@@ -15,6 +15,7 @@ const PY = "3.12.14+20260924";
const PY_URL = (triple) => "https://github.com/astral-sh/python-build-standalone/releases/download/"
+ `${PY.split("+")[1]}/cpython-${PY}-${triple}-install_only_stripped.tar.gz`;
const PYTHON = {
"mac-x64": ["x86_64-apple-darwin", "7ea9761b9069c10b9a20531d568645849d604c59e9c7f11f6659f1e1790c968e"],
"mac-arm64": ["aarch64-apple-darwin", "c2edb321cd32ec2b170df208db0446dccc4398db602ca27cf2079098fb1f7d9d"],
"win-x64": ["x86_64-pc-windows-msvc", "c5bf8edfe858c1df9891be498b5bbc8761d383df5b9790658b088fea4870433a"],
"linux-x64": ["x86_64-unknown-linux-gnu", "269b2c99e4db15b242bf01832f4fea1e8f1a664f273cff519393f296e9820b41"],
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "frame-control",
"version": "0.4.36",
"version": "0.4.39",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frame-control",
"version": "0.4.36",
"version": "0.4.39",
"license": "MIT",
"devDependencies": {
"electron": "^44.4.5",
+5 -4
View File
@@ -1,7 +1,7 @@
{
"name": "frame-control",
"productName": "Frame Control",
"version": "0.4.36",
"version": "0.4.39",
"description": "Desktop app for managing a Valve Steam Frame over SSH",
"private": true,
"main": "main.js",
@@ -9,10 +9,11 @@
"scripts": {
"start": "env -u ELECTRON_RUN_AS_NODE electron .",
"icon": "env -u ELECTRON_RUN_AS_NODE electron build/make-icon.js",
"dist": "sh ../mac/frame-mac-view/build.sh && node build/fetch-deps.js mac arm64 && electron-builder --mac --arm64 --publish never",
"dist:dir": "sh ../mac/frame-mac-view/build.sh && node build/fetch-deps.js mac arm64 && electron-builder --mac --arm64 --dir",
"dist": "FRAME_MAC_ARCH=arm64 sh ../mac/frame-mac-view/build.sh && node build/fetch-deps.js mac arm64 && electron-builder --mac --arm64 --publish never",
"dist:dir": "FRAME_MAC_ARCH=arm64 sh ../mac/frame-mac-view/build.sh && node build/fetch-deps.js mac arm64 && electron-builder --mac --arm64 --dir",
"dist:linux": "node build/fetch-deps.js linux x64 arm64 && electron-builder --linux --x64 --arm64 --publish never",
"dist:win": "node build/fetch-deps.js win x64 && electron-builder --win --x64 --publish never"
"dist:win": "node build/fetch-deps.js win x64 && electron-builder --win --x64 --publish never",
"dist:mac:x64": "FRAME_MAC_ARCH=x86_64 sh ../mac/frame-mac-view/build.sh && node build/fetch-deps.js mac x64 && electron-builder --mac --x64 --publish never"
},
"devDependencies": {
"electron": "^44.4.5",
+6 -5
View File
@@ -1,10 +1,11 @@
# What we have tested
Current for Frame Control **v0.4.36**, 25 October 2026. “Real Frame + physical PC” means an actual headset and a non-virtual computer. “VM only” means a Windows virtual machine, sometimes connected to the real Frame. A CI test or a simulated device is **not** a physical hardware test. The date/version column is the last relevant check, not a promise that every new release was retested on that platform. See each link for the exact procedure and limits.
Current for Frame Control **v0.4.39**, 26 October 2026. “Real Frame + physical PC” means an actual headset and a non-virtual computer. “VM only” means a Windows virtual machine, sometimes connected to the real Frame. A CI test or a simulated device is **not** a physical hardware test. The date/version column is the last relevant check, not a promise that every new release was retested on that platform. See each link for the exact procedure and limits.
| Feature | Real Frame + physical PC tested | VM only | Untested or limited | Last check and proof |
|---|---|---|---|---|
| Core title smoke test | Linux lxso2 installed, launched and removed reserved native/Proton titles on the real Frame; all 13 steps passed | No | Unworn headset; visible rendering and controller input not checked. Other feature paths retain their separate limits below | 25 Oct, v0.4.36 tag `1701e62`, Frame OS 0.5.6 build `20261022.6153909`: [release smoke evidence](evidence/v0436-release-smoke-2026-10-25.md); [earlier detailed results](evidence/round2-smoke-2026-10-25.json) |
| Mac desktop release — tested on a real Mac | Physical MacBook Air (MacBookAir9,1), Intel Core i3, macOS 15.7.7 (24G720): v0.4.35/v0.4.36 DMGs verified and installed; both fail to launch because they are ARM64-only. Separate Intel candidate launched: UI, native preference read/write (restored), bundled Python logs, latest-version check and offline OBS Start/Stop checked; released UI browsed 116 Decky store entries | No | Candidate results are not released-build acceptance. Native Open at Login state, Gatekeeper override, real OBS/Frame stream, native settings across restart, headset transfer and wearer interaction remain unverified | 26 Oct, v0.4.35/v0.4.36 and Intel candidate `6ea9eb2`: [real-Mac evidence](evidence/mac-desktop-2026-10-26.md) |
| Core title smoke test | Linux lxso2 installed and removed reserved native/Proton titles on the real Frame; all 13 steps passed. ARM64 and Windows titles launched | No | The x86_64 title could not launch because Steam Linux Runtime 4.0 was not installed. The headset was unworn; visible rendering and controller input were not checked. Other feature paths retain their separate limits below | 26 Oct, v0.4.37 tag `5239dae`, Frame OS 0.5.6 build `20261022.6153909`: [release smoke evidence](evidence/v0437-release-smoke-2026-10-26.md); [earlier detailed results](evidence/round2-smoke-2026-10-25.json) |
| Windows desktop in the headset | No physical Windows PC test | Windows 11 VM sent a whole desktop to a real Frame; this used an intermediate fix, not the final native DLL | Physical GPU, capture and controller input; individual windows unavailable in bundled DXGI build | 24 Oct, v0.4.10 installer + candidate fix: [VM log](evidence/windows-vm-2026-10-24.md), [PC guide](pc-in-headset.md) |
| Linux desktop in the headset | No | No | Native Linux PC capture is disabled; lxso2 lacked a desktop portal and PipeWire session. The separate Steam `-pipewire` helper is not Frame Control's PC capture | 24 Oct, v0.4.17: [host audit](evidence/linux-pc-host-lxso2-2026-10-24.md), [Steam helper PR #163](https://github.com/saphid/frame-control/pull/163) |
| Mac desktop in the headset | A physical Mac sent its screen/window to a real Frame | No | Current v0.4.26 Mac installer and worn-headset controller input have not been repeated | 28 Sep, pre-v0.4 / Frame OS 0.4.1: [Mac guide and measurement](streaming.md#b-show-the-macs-desktop-inside-the-frame) |
@@ -22,8 +23,8 @@ Current for Frame Control **v0.4.36**, 25 October 2026. “Real Frame + physical
| Passthrough in recordings | Real room pixels in OBS and a production MP4 after safe SteamVR wake | No | **Yes** for the checked 0.5.6 mirror. Raw cameras are not opened. Other games/modes and Steam clips may differ | 25 Oct: [validation](evidence/obs-validation-2026-10-25.md), [recording guide](recording.md#does-record-include-passthrough) |
| Open at login | No Frame needed; Linux autostart file toggled on a physical Linux host with an isolated config directory | No | A full login session and physical Windows/macOS host login have not been checked; OS API calls were unit tested | 25 Oct, v0.4.26: [PR #189](https://github.com/saphid/frame-control/pull/189) |
| Local Frame Control extensions | Example panel displayed live Frame status in a browser preview; the real Frame served a read-only example in the experimental headset panel | No | No wearer-visible extension view; third-party extensions and physical Windows/macOS extension folders untested | 25 Oct, v0.4.29: [PR #194](https://github.com/saphid/frame-control/pull/194), [PR #201](https://github.com/saphid/frame-control/pull/201), [extension guide](extensions.md) |
| Experimental Frame Control SteamVR panel | Real Frame OS 0.5.6 installed its user service, registered an overlay, served authenticated live status and a read-only extension, saved a screenshot and short MP4, and removed owned resources | No | Headset was unworn in standby: overlay reported `not_visible`, stereo capture was dark; wearer-visible pixels and controller input are unverified. The new large read-only status screen has browser layout and packaging checks only | 25 Oct, v0.4.29 device check and v0.4.31 status screen: [PR #198](https://github.com/saphid/frame-control/pull/198), [PR #201](https://github.com/saphid/frame-control/pull/201), [PR #220](https://github.com/saphid/frame-control/pull/220), [panel guide](headset-app.md) |
| Experimental Decky Loader manager | Real Frame OS 0.5.6 ARM64 user-service install, status, update and remove completed; seven official-store plugin backends loaded, and ProtonDB Badges completed managed store install/update/remove | No | v0.4.33 shared transaction lock and stopped-loader plugin removal have Linux and CI tests, but no live recovery test on the Frame. v0.4.35 treats `_root` store tags as root-required and hides duplicate IDs; this has test coverage, not a live store check. v0.4.36 stops a store install or update when the catalog hash changed after review; this was tested with a fake Frame, not with a live store change on the headset. Plugin pages, settings, routes, toasts and controller input in VR remain unverified; some plugins need root, x86_64 binaries or Deck-specific hardware | 25 Oct, v0.4.31 hardware and v0.4.36 tests: [PR #208](https://github.com/saphid/frame-control/pull/208), [PR #230](https://github.com/saphid/frame-control/pull/230), [PR #234](https://github.com/saphid/frame-control/pull/234), [PR #246](https://github.com/saphid/frame-control/pull/246), [PR #253](https://github.com/saphid/frame-control/pull/253), [compatibility table](decky-frame.md) |
| Experimental Framey VR plugin panels and store | No live real-Frame panel test | No | ProtonDB Badges settings rendered and sent a setting call in local Chromium. Official Steam Status rendered in local Chromium; a session-only cache avoided its earlier storage errors. A later local browser check restored a bounded per-plugin cache after panel reopen; live Decky and offline behavior were not tested. Fake Frame tests cover setting read/write through update and reinstall and install/remove two pinned official API v2 archives plus the modern API v1 Storage Cleaner archive. Page tests cover the official JSX runtime, disabled controls, text fields, sliders, grouped dropdowns and an author sample bundle with a settings tab and read-only backend quick action. The sample is not published in the store. The 11-archive manifest audit found only ProtonDB Badges API v2 in that set. Storage Cleaner 1.4.0 showed an empty-data panel and four disabled cleanup buttons in local Chromium with a stub backend; no cleanup action was tested. HLTB for Deck 2.0.12 built a shallow settings view and Bluetooth 3.1.1 built its initial panel in local frontend tests without effects or backend calls. Their badges, search, pairing and Bluetooth changes remain unverified. A hash match, local panel and fake Frame install do not prove VR use. Wearer-visible pixels, controller input and live settings persistence remain unverified | 25 Oct, v0.4.32–0.4.36: [#223](https://github.com/saphid/frame-control/pull/223), [#224](https://github.com/saphid/frame-control/pull/224), [#225](https://github.com/saphid/frame-control/pull/225), [#226](https://github.com/saphid/frame-control/pull/226), [#227](https://github.com/saphid/frame-control/pull/227), [#229](https://github.com/saphid/frame-control/pull/229), [#231](https://github.com/saphid/frame-control/pull/231), [#232](https://github.com/saphid/frame-control/pull/232), [#233](https://github.com/saphid/frame-control/pull/233), [#236](https://github.com/saphid/frame-control/pull/236), [#237](https://github.com/saphid/frame-control/pull/237), [#238](https://github.com/saphid/frame-control/pull/238), [#239](https://github.com/saphid/frame-control/pull/239), [#240](https://github.com/saphid/frame-control/pull/240), [#241](https://github.com/saphid/frame-control/pull/241), [#243](https://github.com/saphid/frame-control/pull/243), [#244](https://github.com/saphid/frame-control/pull/244), [#245](https://github.com/saphid/frame-control/pull/245), [#247](https://github.com/saphid/frame-control/pull/247), [#248](https://github.com/saphid/frame-control/pull/248), [#251](https://github.com/saphid/frame-control/pull/251), [#252](https://github.com/saphid/frame-control/pull/252), [#253](https://github.com/saphid/frame-control/pull/253), [browser/fake Frame evidence](evidence/framey-browser-2026-10-25.md), [guide](framey-loader.md) |
| Experimental Frame Control SteamVR panel | Frame OS 0.5.6 registered and served the opt-in overlay. A later unattended check safely woke SteamVR and found the panel in stereo compositor and mirror captures; it removed its temporary service and restored power settings | No | No person wore the headset for that check. Physical controller input, scrolling, VR keyboard, comfort and legibility remain unverified. The large status view has browser and packaging checks, not a wearer review | 25 Oct, v0.4.29–0.4.37: [panel PR #201](https://github.com/saphid/frame-control/pull/201), [unattended evidence](evidence/framey-vr-2026-10-25/README.md), [worn checklist](framey-vr-check.md) |
| Experimental Decky Loader manager | Frame OS 0.5.6 ARM64 user-service install/status/update/remove and managed ProtonDB install/update/remove passed earlier. A private Decky 3.3.0 service returned read-only ProtonDB and Steam Status setting RPCs; a later locked check verified the official Notebook default drawing/save/reopen flow and Simple Timer backend events in local Chromium | No | The loader remains opt-in. Installs/updates reject an archive whose Decky name collides with another installed plugin before download and under the headset transaction lock; this safety path has host/fake Frame tests, not a new real-headset collision test. The exact-version official catalog covers 116 plugins: 1 works (Notebook default flow only), 45 partial, 59 broken and 11 not applicable. A read-only Steam bridge returned the Frame country, install folders and system information; IsThereAnyDeal selected that country in local Chromium, but its deployed HTTP endpoint and deal lookup remain unverified. An isolated ARM64 LocalSend core answered its info endpoint, but the official x86-only plugin archive and file transfers remain blocked. Most official backends and VR controls remain unverified. Physical controller use and wearer-visible official panels remain unverified | 26 Oct, v0.4.31–0.4.39: [managed store PR #208](https://github.com/saphid/frame-control/pull/208), [name-collision PR #269](https://github.com/saphid/frame-control/pull/269), [catalog PR #266](https://github.com/saphid/frame-control/pull/266), [backend event evidence](evidence/framey-events-backend-2026-10-26.json), [Steam read PR #274](https://github.com/saphid/frame-control/pull/274), [LocalSend evidence PR #275](https://github.com/saphid/frame-control/pull/275), [compatibility table](framey-compatibility.md) |
| Experimental Framey VR plugin panels and store | An unattended Frame OS 0.5.6 run showed the Framey sample panel in stereo compositor and mirror captures. Synthetic OpenVR clicks changed a live sample setting, read its Decky backend file and confirmed it after a fresh iframe reopen | No | This was the repository sample, not an official store plugin. Physical controllers, native scrolling, VR keyboard, wearer comfort and legibility remain unverified. CSS Loader and SteamGridDB exact archives passed fake Frame install/remove and panel packing; this does not establish theme or artwork controls in VR. Panel-local backend events and React toasts have local/real-backend checks, but no wearer review. Pause Games and Tailscale Control settings render in local Chromium; their live actions are unverified. The 116-plugin catalog had 60 fake installs/removals and 56 expected blocks; 98 local Chromium initializers were ready and 18 failed. Notebook is marked works only for its default drawing/save/reopen flow | 26 Oct, v0.4.38–0.4.39: [unattended sample evidence](evidence/framey-vr-2026-10-25/README.md), [backend event PR #270](https://github.com/saphid/frame-control/pull/270), [settings/cache PR #271](https://github.com/saphid/frame-control/pull/271), [worn checklist](framey-vr-check.md) |
Release notes must preserve these limits until a newer recorded check changes them. When a feature is retested, add its date, exact app/Frame version, host type and proof here. The older [v0.3.1 matrix](support-matrix.html) is a historical snapshot and does not describe current coverage.
+5 -2
View File
@@ -88,8 +88,11 @@ entries. Install, update, and remove need an explicit confirmation. They only
change Frame Control owned plugin folders under
`~/.local/share/frame-control/decky/plugins`. The installer checks the store
hash and archive paths, blocks root plugins and non-ARM64 binaries, and rolls
back an update if the service restart fails. The current VR shell does not
provide Decky's Steam Deck Quick Access Menu. Frame Control's headset panel
back an update if the service restart fails. An update is also refused if its
new plugin name is already used by another installed plugin, because Decky
routes backend calls by name. This collision check has local tests only.
The current VR shell does not provide Decky's Steam Deck Quick Access Menu.
Frame Control's headset panel
shows installed plugins and store actions. Its separate Framey iframe hosts
API v2 pages and API v1 bundles that use Decky's modern frontend initializer,
with settings, local routes, and panel toasts where supported.
@@ -0,0 +1,97 @@
{
"checkedAtUtc": "2026-10-26",
"plugin": {
"id": 125,
"name": "Decky Localsend",
"officialArchiveStatus": "x86_64; blocked; no substitutions or safeguard changes"
},
"source": {
"url": "https://github.com/MoYoez/LocalSend-Go",
"revision": "66edc509a28b9f877e7374504892dcbc653a4da4",
"license": "MIT",
"licenseUrl": "https://github.com/MoYoez/LocalSend-Go/blob/66edc509a28b9f877e7374504892dcbc653a4da4/LICENSE",
"pluginLicense": "BSD-3-Clause; distinct from the MIT helper",
"officialElfBuildInfo": "Go 1.25.7, CGO_ENABLED=0, GOARCH=amd64; no embedded VCS revision. Exact source correspondence unproven."
},
"build": {
"toolchain": "go1.25.7",
"environment": {
"GOTOOLCHAIN": "go1.25.7",
"CGO_ENABLED": "0",
"GOOS": "linux",
"GOARCH": "arm64"
},
"command": "go build -trimpath -o localsend-core .",
"elfMachine": 183,
"architecture": "aarch64",
"staticallyLinked": true,
"sha256": "e79238f6051ce34f172f765ae918d3d902b819e27377989a55e5b1eba10e46f9",
"rebuildSha256Matches": true
},
"realFrame": {
"os": "Frame OS 0.5.6 ARM64",
"sharedLockOwner": "kp0z857q58-live-steam-reads-20261026",
"helpExitCode": 0,
"officialBackendFlagsAllRecognized": true,
"helpFlags": [
"-doNotMakeSessionFolder",
"-log",
"-scanTimeout",
"-skipNotify",
"-useAlias",
"-useAutoSave",
"-useAutoSaveFromFavorites",
"-useConfigPath",
"-useDefaultUploadFolder",
"-useDownload",
"-useHttp",
"-useMultcastAddress",
"-useMultcastPort",
"-usePin",
"-useReferNetworkInterface",
"-useWebOutPath"
],
"runtimeExitCode": 0,
"isolation": "unshare -Urn; loopback and private veth pair only; no external interface/default route",
"receiving": "-useAutoSave=false and -useAutoSaveFromFavorites=false; no upload/transfer requests",
"notifications": "-skipNotify",
"readOnlyRequest": "GET http://127.0.0.1:53317/api/localsend/v2/info",
"result": {
"ok": true,
"protocolVersion": "2.0",
"deviceType": "headless",
"responseKeys": [
"alias",
"deviceModel",
"deviceType",
"download",
"fingerprint",
"version"
],
"aliasMatches": true
},
"cleanup": {
"helperExited": true,
"filesCreated": [
"config.yaml",
"helper.log"
]
},
"privateDirectoryAbsent": true,
"servicesInstalledOrStarted": false,
"powerChanges": false,
"sharedLockReleasedAfterTests": true,
"independentReacquireAudit": "Deferred: another job had acquired the shared lock; it was left untouched. The helper process exited and TemporaryDirectory removal completed within the original locked probe."
},
"failedProbe": {
"interface": "loopback only",
"result": "Upstream rejects lo as a discovery interface and exits. Repeated with a veth pair wholly inside the private namespace; no source changes."
},
"unverified": [
"Exact source correspondence to shipped official ELF",
"Unmodified official Python backend invoking this ARM64 artifact",
"HTTPS, discovery on real network, pairing and file transfers",
"Framey frontend/proxy protocol integration",
"Worn VR/controller/keyboard"
]
}
@@ -0,0 +1,57 @@
# LocalSend ARM64 build and isolated Frame probe
The official Decky Localsend archive remains **broken/blocked**: its helper is
x86_64. A source rebuild is now proven to execute on the ARM64 Frame and answer
the LocalSend 2.0 info endpoint. This is a core runtime result, not a working
official-plugin or file-transfer claim. [Machine-readable evidence](framey-localsend-arm64-2026-10-26.json)
records the exact scope and cleanup.
## Reproduce the build
The helper is MIT-licensed; the separate official Python plugin is BSD-3-Clause.
Use the pinned helper source and retain its license for any redistribution.
No binary is redistributed in Frame Control, and no installer digest or
architecture safeguard was changed.
```sh
framey_build_dir=$(mktemp -d)
git clone https://github.com/MoYoez/LocalSend-Go "$framey_build_dir/source"
git -C "$framey_build_dir/source" checkout --detach 66edc509a28b9f877e7374504892dcbc653a4da4
cd "$framey_build_dir/source"
env GOTOOLCHAIN=go1.25.7 CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
go build -trimpath -o "$framey_build_dir/localsend-core" .
sha256sum "$framey_build_dir/localsend-core"
```
Two builds produced the same static aarch64 ELF SHA-256:
`e79238f6051ce34f172f765ae918d3d902b819e27377989a55e5b1eba10e46f9`.
The official ELF uses Go 1.25.7 too, but embeds no VCS revision, so the exact
source correspondence is still unproven. The rebuilt helper recognizes every
CLI flag supplied by the official Python backend.
## Real Frame scope
Tests ran under the normal atomic `/tmp/frame-test.lock` with a private temporary
directory. `-h` exited before configuration or networking. For the health probe,
`unshare -Urn` created a private user/network namespace with loopback and a veth
pair entirely inside it: no external interface or default route. The veth used
the documentation-only `192.0.2.1/24` address. Upstream rejects a loopback-only
discovery interface, which is why the second isolated attempt used a veth.
The helper used a private config/upload path, `-skipNotify`,
`-useAutoSave=false`, `-useAutoSaveFromFavorites=false`, `-useHttp=true`,
`-useReferNetworkInterface probe0` and `-scanTimeout 1`. The test client sent only
`GET /api/localsend/v2/info` over loopback; it returned version `2.0`, device type
`headless` and the private test alias. No upload, transfer, pairing or external
discovery request ran. The helper exited; its private config/log and executable
directory were removed; no service or power setting changed. The lock was
released. An independent cleanup reacquisition found another job's lock and
left it alone; cleanup success comes from the original probe's process wait and
temporary-directory exit, not that deferred audit.
Before enabling an ARM64 official package, verify the unmodified Python backend
and frontend/proxy protocol with this source revision, then HTTPS, discovery and
an explicitly safe private transfer. The official store must publish a matching
ARM64 archive or Frame Control must gain a separately reviewed, digest-scoped
build/package path. Compiling the core does not justify accepting the current
x86 archive or marking the full plugin works.
@@ -0,0 +1,99 @@
{
"checkedAtUtc": "2026-10-26T00:40:00Z",
"scope": "Actual SteamClient parameterless reads through Steam CEF on ARM64 Frame. Official frontends ran in local Chromium with plugin backend/network calls denied. Private loopback relay used the production read bridge with fixture installed-plugin status and the real frame_steam module supplied in memory because the optional headset service was not deployed. This does not verify a deployed real-Frame HTTP endpoint.",
"allowlist": [
"User.GetIPCountry",
"InstallFolder.GetInstallFolders",
"System.GetSystemInfo"
],
"probeResults": {
"GetIPCountry": {
"ok": true,
"type": "string",
"length": 2,
"value": "redacted"
},
"GetInstallFolders": {
"ok": true,
"folders": 1,
"apps": 29,
"pathsAndTitles": "redacted"
},
"GetSystemInfo": {
"ok": true,
"fields": 27,
"identifiers": "redacted"
}
},
"officialPanels": {
"IsThereAnyDeal for Deck": {
"observation": {
"id": 81,
"name": "IsThereAnyDeal for Deck",
"ready": true,
"error": null,
"logs": [],
"methods": [
"settings_load",
"settings_save"
]
},
"countryDropdownMatchesLiveFrameValue": true,
"backendSettingsSave": "attempted by official frontend; denied; no setting written",
"dealLookups": "not run"
},
"Deck Settings": {
"observation": {
"id": 93,
"name": "Deck Settings",
"ready": true,
"error": null,
"logs": [
{
"kind": "error",
"message": "[decky-game-settings:AuthorFooter] Error fetching avatar image: Error: Decky proxy fetch is unavailable in Framey"
},
{
"kind": "error",
"message": "[decky-game-settings:GameSelectView] Error fetching installed games: TypeError: Cannot read properties of undefined (reading 'allGamesCollection')"
}
],
"methods": []
},
"remainingBlocker": "collectionStore.allGamesCollection"
}
},
"boundariesTested": [
"Only three explicit zero-argument APIs",
"Installed store plugin required",
"Headset-service gate",
"Iframe source and current session required",
"Late replies dropped after reopen",
"256 KiB UTF-8 response cap and socket cleanup"
],
"deviceState": {
"sharedLock": "owned normally; released after testing",
"servicesInstalledOrStarted": false,
"powerChanges": false,
"deviceFilesForSteamProbe": false
},
"unverified": [
"Deployed headset HTTP endpoint",
"Plugin backend settings/deal lookups",
"Steam page patches and live library context",
"Worn VR/controller/keyboard"
],
"catalogRefresh": {
"source": "https://plugins.deckbrew.xyz/plugins",
"checkedAtUtc": "2026-10-26T00:44:00Z",
"visiblePlugins": 116,
"idVersionDigestChanges": 0
},
"localRegressionAudit": {
"exactArchivedFrontends": 116,
"ready": 98,
"failed": 18,
"initializationRegressions": 0,
"pluginBackendAndNetworkActions": "denied, including live Steam read APIs"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

+112
View File
@@ -0,0 +1,112 @@
# Physical Mac desktop check, 26 October 2026
Host: **MacBook Air (MacBookAir9,1), Intel Core i3 1.1 GHz, macOS 15.7.7
(24G720), x86_64**. This is physical hardware. The console is an unattended
server with display/automation restrictions. No Apple Silicon runtime result
is inferred from this test.
## Released installers: verified, installed, cannot run on Intel
GitHub's latest release was v0.4.35 at the start; v0.4.36 became latest during
the check. Each DMG and its SHA256SUMS were downloaded from that exact release
with `gh release download`. Both passed `shasum -a 256 -c SHA256SUMS
--ignore-missing` (only the downloaded asset was checked):
| Release | Asset | SHA-256 |
|---|---|---|
| v0.4.35 | Frame-Control-mac-arm64.dmg | `eda670748679ca02fcf242870cd53954b22cf8b93af47a193c9903bd08a8334f` |
| v0.4.36 | Frame-Control-mac-arm64.dmg | `4d13068f0cfd3616823d63c82333afa7e368c1d57d375e9564f5f7ed4f0ebbd7` |
Each read-only DMG was mounted with `hdiutil attach -readonly -nobrowse`.
There was no existing `/Applications/Frame Control.app`; the app was copied
there with `ditto`, the command-line equivalent of the DMG's drag to
Applications. `open "/Applications/Frame Control.app"` returned exactly:
> The application cannot be opened because it has an incorrect executable format.
`file` identified the executable as `Mach-O 64-bit executable arm64`.
`codesign -dv` identified an ad hoc signature with no TeamIdentifier. The
v0.4.35 `spctl -a -vv` assessment returned `rejected`. The v0.4.36 assessment
stalled and is not counted as a completed assessment.
**No Gatekeeper warning dialog was observed.** LaunchServices rejected the
architecture first. The CLI download had no `com.apple.quarantine` attribute,
so this was not a browser-quarantine test. Neither right-click Open, Open
Anyway nor removing quarantine was tested. On this Intel Mac there is no
honest opening workaround for either released ARM64 app. Rosetta does not
run ARM64 apps on Intel. The README now separates that fact from Apple's
recommended Open Anyway flow for compatible Macs and labels the latter
unverified here. No global Gatekeeper setting was changed.
## Separate Intel candidate: partial native runtime pass
[PR #262](https://github.com/saphid/frame-control/pull/262), commit `6ea9eb2`,
adds native x64 DMG/ZIP builds, pinned x64 Python, an x86_64 Swift helper, and
CI architecture checks. Swift 6.2.4 rejected an ambiguous
`Data.withUnsafeMutableBytes` overload in the helper; an explicit
`UnsafeMutableRawBufferPointer` annotation compiled successfully on this Air.
`npm run dist:mac:x64` completed locally, producing an ad hoc signed app,
ZIP and DMG. The unpacked candidate was copied to
`/Applications/Frame Control Intel Test.app` and launched with telemetry off,
a task-local server data directory and a temporary loopback debug port.
This is **not** the published v0.4.36 binary.
The first launch stalled for several minutes before app code ran. A process
sample showed only `_dyld_start`, a 12 KiB footprint and no loaded image
list, while `syspolicyd` used about 89% CPU. It later proceeded without any
system-service restart; the app window and server loaded. The initial stall
is a host observation, not proof of a Frame Control bug. Restarting that
security daemon was rejected by automatic approval review, and was not done.
| Check | Observed result and limit |
|---|---|
| Native window and IPC bridge | The Electron renderer displayed Apps and the offline identity warning; `window.frameApp` was present. Native renderer inspected through its temporary loopback CDP port; this does not prove every macOS permission dialog or physical click |
| Settings | Native preference read returned `{}`. Writing `mvSeparate="0"` read back correctly. The original preference file was restored byte-for-byte (or removed when originally absent) in a finally block. Persistence across an app restart is not claimed |
| Open at Login | Source is opt-in and focused login-item tests passed. Native menu read via System Events timed out; an OS login session and actual checkbox state remain unverified. No login item was enabled |
| Logs | `~/Library/Logs/Frame Control/server.log` exists. Its startup line proves the candidate used `Contents/Resources/python/bin/python3` and its bundled `ui/server.py`; HTTP reads and offline failures were logged. Show Server Log menu interaction was not verified |
| Update check | Actual `frameApp.update.check()` completed with `status="none"`, `current="0.4.36"`, latest `0.4.36`, and `error=null`. No update was installed; native dialog dismissal was not verified |
| OBS setup and lifecycle | Native status reported supported/idle, Start on port 15123 reported starting, Stop reported stopping, then idle. The Frame's SSH identity rejection prevented a headset stream. This checks local lifecycle only: no OBS app, audio/video delivery or receiver tested |
| Decky store | The exact released UI/server resources, separately served by this Mac's existing Python 3.9.6, loaded 116 official entries in local Chromium. Forty rows rendered; searching ProtonDB found ProtonDB Badges 1.3.4; all Install buttons stayed disabled while offline. No loader or plugin was installed. [Browser screenshot](mac-decky-browser-2026-10-26.png) |
| Other UI | Released Apps, Settings, Privacy & updates, Tools, Capture & Live and Decky views rendered. The privacy page correctly reported telemetry blocked by `FRAME_CONTROL_TELEMETRY=0`; sharing/contact choices were left alone. The activity log displayed the offline failure. The browser path hides native OBS buttons as designed; it is not a substitute for the native lifecycle check above |
| Framey preview | No standalone local preview control was found in the desktop UI. The Framey panel belongs to the headset runtime; no VR or plugin-backend result is claimed |
The released server refused a stale SSH host identity rather than silently
trusting it. That safety check was preserved; no host pin was replaced just
to make the test pass. Capture, media, game launches, power controls, installs,
sign-ins and notifications were not triggered without a working connection.
## Headset: read-only relay checks, no changes
`frame.local` resolved on the Air, but direct TCP/SSH did not connect. The
coordinator host lxso2 could reach it. The app's read-only `frame_status.py`
was piped through lxso2 to the Frame and returned:
- SteamOS **0.5.6**, build **20261022.6153909**, variant `vr`.
- Battery **99%, Charging**.
- Home storage: 996,984,774,656 bytes total, 769,023,275,008 bytes free.
- A read-only search counted **14** Steam screenshot JPEGs; none was copied.
The shared `/tmp/frame-test.lock` was owned by another Framey test on each
check. This job did not acquire, remove or replace it. File upload/delete,
OBS on the device and every other headset mutation were skipped. These relay
reads do not prove the native Mac app's direct SSH or file-transfer path.
The headset was never wiped or rebooted.
## Validation and follow-up
A disposable Linux checkout of exact commit `6ea9eb2` on lxso2 passed:
**1,590 server tests (16 skipped)** and **46 app tests**. On the physical Air,
**14 focused login-item/preferences/OBS tests** and **18 website tests** passed.
Full Mac server/release suites were attempted but spawned helper programs
stalled and timed out during the policy-service problem; they are not a pass.
Node 22's complete app run cancelled sign-in tests around an unreferenced
timeout; the exact same app suite passed on lxso2. CI uses Node 24.
Required next steps: pass exact-head Mac packaging/architecture CI, publish
an Intel asset through coordinator job mppkd9qczt, then repeat a normal
browser-download/quarantine installation of that release on this Air. Verify
Open Anyway and Open at Login through an authorized interactive console.
Acquire the shared headset lock before a temporary transfer or real OBS test.
Preserve all unverified labels until those checks actually pass. PR #113 was
not changed. No social post was made.
@@ -0,0 +1,24 @@
# v0.4.37 release smoke on a real Frame
On 26 October 2026 at 00:27 UTC, a physical Linux host ran the release
`scripts/frame-smoke.sh` against a real Steam Frame. The smoke scripts were
byte-identical to tag `v0.4.37` (`5239dae1a82c35635ef89f59eac9721c5014e2ed`).
The headset reported Frame OS 0.5.6, build `20261022.6153909`.
The test acquired `/tmp/frame-test.lock` with a unique owner marker before
reading the headset. SteamVR activity stayed at 3 (standby); initial proximity
raw was 6.3125 and battery was 99%. The test released only its own lock after
cleanup. Battery read 100% afterward.
All **13 of 13** smoke steps passed. ARM64 and Windows test titles installed,
launched, and were removed. The x86_64 title installed and was removed; its
launch reported that Steam Linux Runtime 4.0 was found but not installed, so
this result does **not** establish x86_64 game execution. Devkit properties and
status reads passed. No key was paired. The final cleanup removed the reserved
test titles and processes. The private result remains under
`/tmp/frame-v0437-smoke-results/`; this summary omits headset identifiers and
device status details.
Because the headset was in standby, this test did not verify wearer-visible
pixels, physical controller input, a Framey panel, or official Decky plugin
settings in VR. Those checks remain on the [worn checklist](../framey-vr-check.md).
+7 -5
View File
@@ -4,7 +4,7 @@ Snapshot: 2026-10-25T23:19:48.854504+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: 1 · Partial: 44 · Broken: 60 · N/A: 11.**
**Works: 1 · Partial: 45 · Broken: 59 · 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 a limited frontend/settings surface initialized or rendered. Backend-dependent functionality remains unverified; it is not an end-to-end success claim.
@@ -27,7 +27,7 @@ Open the browser audit URL in local Chromium, run `runAudit()` and inspect/downl
The current published digests are pinned in the JSON; if the store changed, retain/download those exact archives for reproduction. Audit downloads permit larger ZIPs for inspection; production installer limits remain 50 MiB ZIP / 150 MiB unpacked / 30 MiB per member.
After the VR job released the shared lock, this audit acquired it and used a private temporary Decky 3.3.0 ARM64 user service. Read-only ProtonDB `get_setting` and Steam Status `get_settings` calls succeeded through the live Steam CEF websocket. The service and private base were removed and the lock released; normal managed services stayed inactive, battery was 100 → 99, and no power setting was changed. [Redacted results](evidence/framey-official-backend-2026-10-25.json) record exactly what ran. A [2026-10-26 follow-up](evidence/framey-events-backend-2026-10-26.json) verified default Notebook drawing/save/reopen across a real backend restart and Simple Timer start/cancel, countdown/completion events, a visible toast and persisted recents/subtle mode. Only private test settings were written. Simple Timer remains partial because its own expired-timer startup raises a missing `timer_task` error; sound/suspend are unsupported. All other official backends remain unexecuted. The separate VR job proves Frame Control/sample compositor rendering, not any official plugin. No worn-VR official-plugin result is claimed. A [local UI follow-up](evidence/framey-ui-fallbacks-2026-10-26.json) rendered Pause Games and Tailscale Control settings with all backend/network actions denied; it does not verify process pausing or Tailscale changes. Earlier focused checks cover [ProtonDB](evidence/framey-browser-2026-10-25.md), [Steam Status/cache](evidence/framey-cache-browser-2026-10-25.md), [Storage Cleaner](evidence/framey-api-v1-browser-2026-10-25.md), and [legacy MusicControl](evidence/framey-legacy-2026-10-25.md).
After the VR job released the shared lock, this audit acquired it and used a private temporary Decky 3.3.0 ARM64 user service. Read-only ProtonDB `get_setting` and Steam Status `get_settings` calls succeeded through the live Steam CEF websocket. The service and private base were removed and the lock released; normal managed services stayed inactive, battery was 100 → 99, and no power setting was changed. [Redacted results](evidence/framey-official-backend-2026-10-25.json) record exactly what ran. A [2026-10-26 follow-up](evidence/framey-events-backend-2026-10-26.json) verified default Notebook drawing/save/reopen across a real backend restart and Simple Timer start/cancel, countdown/completion events, a visible toast and persisted recents/subtle mode. Only private test settings were written. Simple Timer remains partial because its own expired-timer startup raises a missing `timer_task` error; sound/suspend are unsupported. All other official backends remain unexecuted. The separate VR job proves Frame Control/sample compositor rendering, not any official plugin. No worn-VR official-plugin result is claimed. A [local UI follow-up](evidence/framey-ui-fallbacks-2026-10-26.json) rendered Pause Games and Tailscale Control settings with all backend/network actions denied; it does not verify process pausing or Tailscale changes. A [read-only Steam bridge follow-up](evidence/framey-steam-reads-2026-10-26.json) verified actual Frame country, install folders and system information, plus IsThereAnyDeal's country dropdown in local Chromium. The installed-plugin status was fixture-shaped in that relay; plugin backends stayed denied and the deployed headset HTTP endpoint remains unverified. Earlier focused checks cover [ProtonDB](evidence/framey-browser-2026-10-25.md), [Steam Status/cache](evidence/framey-cache-browser-2026-10-25.md), [Storage Cleaner](evidence/framey-api-v1-browser-2026-10-25.md), and [legacy MusicControl](evidence/framey-legacy-2026-10-25.md).
## Every current plugin
@@ -89,7 +89,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 78: Brightness Bar 1.0.4 | legacy / legacy | N/A | Not supported on Frame: Targets Steam/QAM brightness shortcuts and a global Steam overlay; neither is attached to the Framey panel. | static archive, local browser, fake-Frame |
| 79: Decky Notifications 1.0.1 | legacy / legacy | partial | Initializer succeeds; device/account setup and notification delivery unverified. | static archive, local browser, fake-Frame |
| 80: MagicBlack 2.0.2 | 1 / 2 | N/A | Not supported on Frame: Targets a global Steam Deck screen-black overlay; the isolated panel cannot black out the Frame compositor. | static archive, local browser, fake-Frame |
| 81: IsThereAnyDeal for Deck 1.1.1 | legacy / legacy | broken | Country lookup calls missing SteamClient.User.GetIPCountry; deal loading fails after initialization. | static archive, local browser, fake-Frame |
| 81: IsThereAnyDeal for Deck 1.1.1 | legacy / legacy | partial | Country settings render and select the real Frame Steam country through the read-only bridge. Settings writes, deal lookups and Steam page integration remain unverified; plugin backend calls were denied. | static archive, local browser, fake-Frame, real Frame SSH |
| 82: MagicPods 2.0.21 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: MagicPodsDecky/bin/magicpodscore. Frontend audit failed: Audit blocks WebSocket. | static archive, local browser, fake-Frame |
| 84: Junk-Store 1.1.13 | legacy / legacy | broken | Route registration succeeds; rendering fails on a missing Steam ContainerBackground component. | static archive, local browser, fake-Frame |
| 85: Decky-Undervolt 1.0.8 | 1 / 2 | N/A | Not supported on Frame: RyzenAdj Curve Optimizer requires an AMD CPU; Frame is ARM64. | static archive, local browser, fake-Frame |
@@ -99,7 +99,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 90: Picture in Picture 1.0.0 | 1 / 2 | partial | Initializer succeeds; global picture-in-picture injection cannot escape the isolated panel. | static archive, local browser, fake-Frame |
| 91: PlayCount 1.7 | 1 / 2 | partial | Initializer succeeds; Steam game-page play-count patch has no supported VR target. | static archive, local browser, fake-Frame |
| 92: Crosshair 1.1.0 | 1 / 2 | partial | Initializer succeeds; global crosshair injection has no supported compositor target. | static archive, local browser, fake-Frame |
| 93: Deck Settings 1.2.6 | 1 / 2 | broken | Initializer succeeds; game selection fails because SteamClient.InstallFolder.GetInstallFolders is unavailable. | static archive, local browser, fake-Frame |
| 93: Deck Settings 1.2.6 | 1 / 2 | broken | Real Steam install folders are now available; game selection still fails on missing collectionStore.allGamesCollection. Steam library context, proxy fetch and backend actions remain unsupported/unverified. | static archive, local browser, fake-Frame |
| 94: Decky DoT DNS 0.0.2 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
| 95: Decky-SpoofDPI 0.12.0 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-spoof-dpi/bin/spoofdpi. | static archive, local browser, fake-Frame |
| 96: Simple Timer 1.0.0 | 1 / 2 | partial | Live start/cancel, countdown events, completion toast, persisted recents/subtle mode verified in Chromium with real ARM64 Decky. Upstream expired-timer restart raises missing timer_task. Audio/suspend unsupported; alerts require an open panel; worn VR unverified. | static archive, local browser, fake-Frame, real Frame SSH |
@@ -279,6 +279,8 @@ DFL includes exports imported through decky-frontend-lib / @decky/ui. Steam stor
All published Linux native payloads in this snapshot are x86_64; no published aarch64 payload was found. The three Windows DLLs belong to Decky-Framegen. The installer still rejects these archives. Apart from the explicitly described LocalSend experiment, ARM64 builds were not attempted: the constraints below remain unresolved.
[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.
| Plugin | Native payloads | ARM64 result / remaining blocker |
| --- | --- | --- |
| Fantastic | bin/backend | Fan backend/driver is Deck-specific; an ARM64 rebuild would not provide compatible Frame hardware. |
@@ -302,7 +304,7 @@ All published Linux native payloads in this snapshot are x86_64; no published aa
| 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 | LocalSend-Go commit 66edc509a28b9f877e7374504892dcbc653a4da4 cross-compiled successfully with CGO_ENABLED=0 GOOS=linux GOARCH=arm64. Static aarch64 ELF SHA-256 e79238f6051ce34f172f765ae918d3d902b819e27377989a55e5b1eba10e46f9. Not executed; source tip is not proven to match the shipped helper/protocol. Official ZIP is still x86_64. |
| 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. |
| 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. |
+12 -1
View File
@@ -124,6 +124,17 @@ verifies Pause Games and Tailscale Control settings in local Chromium; their
primary backend actions remain unverified. PlayTime still requires Steam's
real app store and game observers.
Three parameterless SteamClient reads use Steam's actual CEF context:
`User.GetIPCountry`, `InstallFolder.GetInstallFolders` and `System.GetSystemInfo`.
The authenticated headset-only endpoint checks that the plugin is installed,
selects an expression from a fixed allowlist, and caps replies at 256 KiB.
The iframe cannot supply a Steam property path or executable source, and replies
are discarded if the panel session changed. Unsupported APIs remain unavailable.
[Locked Frame checks](evidence/framey-steam-reads-2026-10-26.json) verified all
three reads and IsThereAnyDeal's country dropdown in local Chromium. Its backend
settings/deal calls stayed denied; Deck Settings still needs real Steam library
context. A deployed headset HTTP endpoint and worn-VR behavior remain unverified.
The fake Frame integration test now writes a Decky setting through Framey's
scoped backend endpoint, reads it in a new CEF request, updates the plugin,
then checks it again. It also confirms removal blocks method calls and a
@@ -180,7 +191,7 @@ plugin initializer. Framey displays up to eight actions and reports a pending,
success, or failure state. These descriptor fields are Framey additions; Decky
ignores them. Use a plugin backend method through `@decky/api` for work needing
persistent state, and keep a quick action safe to repeat.
See the [API v2 compatibility table](frame-plugin-author.md#framey-api-v2-compatibility)
See the [Decky API compatibility table](frame-plugin-author.md#framey-decky-api-compatibility)
for tested behavior and remaining limits.
The [Frame Control extension API](extensions.md) is an alternative for a
+3 -2
View File
@@ -41,8 +41,9 @@ frontend host is browser-tested, but wearer-visible VR use remains unverified.
The panel also lists Decky plugins and can install, update, or remove compatible
plugins from the official store when the opt-in managed loader is running.
Each change asks for confirmation. The Frame VR shell does not show Decky's
Steam Deck Quick Access Menu, so Decky plugin pages, settings, routes, and
toasts are not available in this panel yet. The [compatibility table](decky-frame.md)
Steam Deck Quick Access Menu. Framey's separate panel can show compatible
plugin pages, settings, local routes, and local toasts, but these controls
still need a worn-headset check. The [compatibility table](decky-frame.md)
separates backend loading from VR use. Authors can use the
[Frame plugin and extension guide](frame-plugin-author.md).
+3 -3
View File
@@ -13,7 +13,7 @@ count. So a build reaches people only when you publish it, after testing it.
```
`.github/workflows/release.yml` builds macOS, Windows and Linux, and
attaches everything to a **draft** release for that tag. After all eight
attaches everything to a **draft** release for that tag. After all ten
installers upload, the workflow also attaches `SHA256SUMS` built from
GitHub's SHA-256 digests of those uploaded bytes. Nobody is
offered a draft. Editing a draft while installers are uploading can make
@@ -39,9 +39,9 @@ count. So a build reaches people only when you publish it, after testing it.
scripts/publish-release.sh v0.4.0
```
The script checks that the tag is on GitHub and that all eight installers
The script checks that the tag is on GitHub and that all ten installers
are attached, each with the SHA-256 digest GitHub records. It also checks
that the uploaded `SHA256SUMS` digest matches a fresh list of those eight
that the uploaded `SHA256SUMS` digest matches a fresh list of those ten
installer digests. It attaches
`update.json` (the version, the notes, the release page and each
installer's digest), then publishes the release and marks it latest. It
+3 -1
View File
@@ -4,7 +4,9 @@ This is a small Decky API v2 plugin for authors. Its panel uses the official
`@decky/api`, `@decky/ui`, and `@decky/rollup` packages. A toggle calls a Python
backend method, stores one boolean under `decky.DECKY_PLUGIN_SETTINGS_DIR`,
and reads it back when the panel opens. The backend rejects other keys and
values. The frontend also shows a local toast after a successful save. A
values. The frontend also shows a local toast with a React body after a
successful save. The toast stays inside the Framey panel; it is not a Steam
system notification. A
Framey settings tab exposes the same toggle, and a read-only quick action
fetches the saved value without changing it. These two fields are Framey
additions; Decky's own menu still uses the main panel content. The quick
+2 -1
View File
@@ -51,7 +51,8 @@ function Content() {
await setSetting('enabled', value);
setEnabled(value);
setMessage(`Saved setting: ${value ? 'on' : 'off'}`);
toaster.toast({ title: 'Framey sample', body: 'Setting saved' });
toaster.toast({ title: 'Framey sample',
body: SP_JSX.jsxs("span", { children: ["Setting saved: ", SP_JSX.jsx("strong", { children: value ? 'on' : 'off' })] }) });
}
catch (error) {
setMessage(`Could not save setting: ${String(error)}`);
+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, 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\n async function refresh() {\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 setBusy(false);\n }\n }\n\n useEffect(() => {void refresh();}, []);\n\n async function change(value: boolean) {\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', body: 'Setting saved'});\n } catch (error) {\n setMessage(`Could not save setting: ${String(error)}`);\n } finally {\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 to setting</ButtonItem></PanelSectionRow>\n </PanelSection>;\n}\n\nexport default definePlugin(() => {\n routerHook.addRoute('/framey-sample/details/:topic?', Details);\n return Object.assign({\n name: 'Framey Settings Sample',\n icon: <span aria-hidden=\"true\">⚙</span>,\n content: <Content />,\n onDismount: () => routerHook.removeRoute('/framey-sample/details/:topic?'),\n}, {\n // Framey additions are ignored by Decky's own menu. Keep the mainLine 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, Navigation, PanelSection, PanelSectionRow, ToggleField, useParams} from '@decky/ui';\nimport {useEffect, 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\n async function refresh() {\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 setBusy(false);\n }\n }\n\n useEffect(() => {void refresh();}, []);\n\n async function change(value: boolean) {\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 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 to setting</ButtonItem></PanelSectionRow>\n </PanelSection>;\n}\n\nexport default definePlugin(() => {\n routerHook.addRoute('/framey-sample/details/:topic?', Details);\n return Object.assign({\n name: 'Framey Settings Sample',\n icon: <span aria-hidden=\"true\">⚙</span>,\n content: <Content />,\n onDismount: () => routerHook.removeRoute('/framey-sample/details/:topic?'),\n}, {\n // FrLine truncated
+2 -1
View File
@@ -31,7 +31,8 @@ function Content() {
await setSetting('enabled', value);
setEnabled(value);
setMessage(`Saved setting: ${value ? 'on' : 'off'}`);
toaster.toast({title: 'Framey sample', body: 'Setting saved'});
toaster.toast({title: 'Framey sample',
body: <span>Setting saved: <strong>{value ? 'on' : 'off'}</strong></span>});
} catch (error) {
setMessage(`Could not save setting: ${String(error)}`);
} finally {
+1 -1
View File
@@ -249,7 +249,7 @@ final class WebSocket {
var payload = Data(buffer[(start + off)..<(start + off + len)])
if masked {
let mask = [UInt8](buffer[(start + maskOff)..<(start + maskOff + 4)])
payload.withUnsafeMutableBytes { p in
payload.withUnsafeMutableBytes { (p: UnsafeMutableRawBufferPointer) in
for i in 0..<len { p[i] ^= mask[i & 3] }
}
}
+4 -2
View File
@@ -1,11 +1,13 @@
#!/bin/sh
# Builds the Mac streaming agent into mac/bin/frame-mac-view (arm64, macOS 14+).
# Builds the Mac streaming agent into mac/bin/frame-mac-view (arm64 or x86_64, macOS 14+).
# Frame Control runs it for "Mac in the headset"; the Electron app bundles it.
set -eu
here=$(cd "$(dirname "$0")" && pwd)
out=${1:-$here/../bin/frame-mac-view}
arch=${FRAME_MAC_ARCH:-$(uname -m)}
case "$arch" in arm64|x86_64) ;; *) echo "unsupported Mac architecture: $arch" >&2; exit 1 ;; esac
mkdir -p "$(dirname "$out")"
xcrun swiftc -O -swift-version 5 -target arm64-apple-macos14.0 \
xcrun swiftc -O -swift-version 5 -target "$arch-apple-macos14.0" \
-import-objc-header "$here/Sources/CGVirtualDisplay.h" \
-o "$out" "$here"/Sources/*.swift
echo "built $out"
+1
View File
@@ -45,6 +45,7 @@ addEventListener('message',event=>{
if(data.type==='framey:error')active.error=String(data.message).slice(0,500);
if(data.type==='framey:ready')active.ready=true;
if(data.type==='framey:events')event.source.postMessage({type:'framey:reply',id:data.id,result:{events:[],cursor:0}},'*');
if(data.type==='framey:steam')event.source.postMessage({type:'framey:reply',id:data.id,error:'Read-only compatibility audit: live Steam API blocked'},'*');
if(data.type==='framey:call'){
if(!active.methods.includes(data.method))active.methods.push(data.method);
event.source.postMessage({type:'framey:reply',id:data.id,error:'Read-only compatibility audit: backend method blocked'},'*');}
+6
View File
@@ -56,6 +56,9 @@ def render(data):
'No worn-VR official-plugin result is claimed. '
'A [local UI follow-up](evidence/framey-ui-fallbacks-2026-10-26.json) rendered Pause Games and Tailscale Control settings '
'with all backend/network actions denied; it does not verify process pausing or Tailscale changes. '
'A [read-only Steam bridge follow-up](evidence/framey-steam-reads-2026-10-26.json) verified actual Frame country, '
'install folders and system information, plus IsThereAnyDeal\'s country dropdown in local Chromium. '
'The installed-plugin status was fixture-shaped in that relay; plugin backends stayed denied and the deployed headset HTTP endpoint remains unverified. '
'Earlier focused checks cover [ProtonDB](evidence/framey-browser-2026-10-25.md), '
'[Steam Status/cache](evidence/framey-cache-browser-2026-10-25.md), '
'[Storage Cleaner](evidence/framey-api-v1-browser-2026-10-25.md), and '
@@ -82,6 +85,9 @@ def render(data):
'All published Linux native payloads in this snapshot are x86_64; no published aarch64 payload was found. '
'The three Windows DLLs belong to Decky-Framegen. The installer still rejects these archives. '
'Apart from the explicitly described LocalSend experiment, ARM64 builds were not attempted: the constraints below remain unresolved.', '',
'[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.', '',
'| Plugin | Native payloads | ARM64 result / remaining blocker |', '| --- | --- | --- |']
for p in data['plugins']:
if p['native']:
+3 -3
View File
@@ -15,7 +15,7 @@ dry=""
[ "${1:-}" = "--dry-run" ] && { dry=1; shift; }
tag="${1:?usage: $0 [--dry-run] vX.Y.Z}"
repo=saphid/frame-control
expected="Frame-Control-mac-arm64.dmg Frame-Control-mac-arm64.zip Frame-Control-Setup-x64.exe
expected="Frame-Control-mac-arm64.dmg Frame-Control-mac-arm64.zip Frame-Control-mac-x64.dmg Frame-Control-mac-x64.zip Frame-Control-Setup-x64.exe
Frame-Control-win-x64.zip Frame-Control-linux-x86_64.AppImage Frame-Control-linux-arm64.AppImage
Frame-Control-linux-amd64.deb Frame-Control-linux-arm64.deb"
page="https://github.com/$repo/releases/tag/$tag"
@@ -70,7 +70,7 @@ print(next((a.get("digest") or "" for a in d["assets"] if a["name"]==n), "absent
done
[ -z "$missing" ] || { echo "not publishing: fix the assets above" >&2; exit 1; }
# The workflow uploads this only after all eight platform builds complete.
# The workflow uploads this only after all ten platform builds complete.
# Regenerate it from GitHub's installer digests and compare its own GitHub
# digest, so a stale or partial checksum file cannot pass the release gate.
python3 "$(dirname "$0")/release-checksums.py" < "$tmp/release.json" > "$tmp/SHA256SUMS"
@@ -79,7 +79,7 @@ checksum_digest=$(python3 -c 'import json,sys
d=json.load(open(sys.argv[1])); matches=[a.get("digest") for a in d["assets"] if a["name"]=="SHA256SUMS"]
print(matches[0] if len(matches)==1 else "absent")' "$tmp/release.json")
[ "$checksum_digest" = "sha256:$expected_sum" ] || {
echo "not publishing: SHA256SUMS is absent or does not match the eight installer digests" >&2; exit 1;
echo "not publishing: SHA256SUMS is absent or does not match the ten installer digests" >&2; exit 1;
}
echo "ok SHA256SUMS"
+2 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Print SHA256SUMS for the eight release installers from GitHub asset metadata.
"""Print SHA256SUMS for the ten release installers from GitHub asset metadata.
GitHub computes each asset digest from its uploaded bytes. The workflow runs
this only after all platform builds finish; publish-release.sh checks that the
@@ -13,6 +13,7 @@ import sys
INSTALLERS = (
"Frame-Control-mac-arm64.dmg",
"Frame-Control-mac-arm64.zip",
"Frame-Control-mac-x64.dmg", "Frame-Control-mac-x64.zip",
"Frame-Control-Setup-x64.exe",
"Frame-Control-win-x64.zip",
"Frame-Control-linux-x86_64.AppImage",
+11
View File
@@ -99,6 +99,17 @@ listeners.find(x=>x.type==='message').handler({source:parent,data:{type:'framey:
assert.equal(await reply,'dark');
await assert.rejects(api.call('x;alert(1)'),/Invalid backend method/);
// These APIs use a separate, parameterless allowlisted read bridge.
for(const [namespace,method,result] of [['User','GetIPCountry','AU'],
['InstallFolder','GetInstallFolders',[{vecApps:[]}]],['System','GetSystemInfo',{sOSName:'Frame OS'}]]){
const pendingRead=window.SteamClient[namespace][method]();
const request=messages.at(-1);assert.equal(request.type,'framey:steam');assert.equal(request.method,method);
assert.equal(request.args.length,0);
listeners.find(x=>x.type==='message').handler({source:parent,data:{type:'framey:reply',id:request.id,result}});
assert.equal(await pendingRead,result);
}
assert.equal(window.SteamClient.System.ShutdownPC,undefined);
const toast=api.toaster.toast({title:'Saved',body:'Mode updated',duration:4000});
const toastNode=elements.get('framey-toasts').children[0];
assert.equal(toastNode.attributes.role,'status');
+19
View File
@@ -47,3 +47,22 @@ assert.equal(replies.length,1);assert.equal(replies[0].result.events[0].args[0],
el('frameyClose').onclick();
assert.equal(requests[3].body.session,session);assert.equal(requests[3].body.events.length,0);
for(const request of requests.slice(1))request.resolve({ok:true,json:async()=>({events:[],cursor:0})});
// Only explicit zero-argument Steam reads reach the trusted parent endpoint.
vm.runInContext('openFrameyPanel(96,"Timer")',context);
session=vm.runInContext('frameySession',context);
const steamRead={type:'framey:steam',id:10,session,method:'GetIPCountry',args:[]};
const initialCount=requests.length;
for(const change of [{method:'ShutdownPC'},{method:'User.GetIPCountry'},{args:['injected']},{session:'stale'}])
await send({...steamRead,...change});
await send(steamRead,{});
assert.equal(requests.length,initialCount);
const read=send(steamRead);
assert.equal(requests.at(-1).path,'/api/framey/steam');assert.equal(requests.at(-1).body.id,96);
requests.at(-1).resolve({ok:true,json:async()=>({result:'AU'})});
await read;assert.equal(replies.at(-1).result,'AU');
const staleRead=send({...steamRead,id:11});
vm.runInContext('openFrameyPanel(96,"Timer")',context);
const priorReplies=replies.length;
requests.at(-1).resolve({ok:true,json:async()=>({result:'AU'})});
await staleRead;assert.equal(replies.length,priorReplies);
+28 -2
View File
@@ -9,7 +9,20 @@ assert.match(bundle,/SP_JSX\.jsx/);
const React={Fragment:Symbol('fragment'),Component:class {},
createElement:(type,props,...children)=>({type,props:props||{},children}),
useState:value=>[value,()=>{}],useEffect(){},isValidElement:value=>!!value?.type};
const window={React,ReactDOM:{createRoot:()=>({render(){}})},addEventListener(){}};
class Element {
constructor(){this.children=[];this.textContent='';this.attributes={};}
append(...nodes){this.children.push(...nodes);}
setAttribute(name,value){this.attributes[name]=value;}
remove(){this.removed=true;}
}
const elements=new Map(), roots=new Map();
const document={getElementById:id=>{
if(!elements.has(id))elements.set(id,new Element());return elements.get(id);
},createElement:()=>new Element()};
const window={React,ReactDOM:{createRoot:node=>{
const root={render(value){this.value=value;},unmount(){this.unmounted=true;}};
roots.set(node,root);return root;
}},addEventListener(){}};
const listeners=[];
const calls=[];
const parent={postMessage(message){
@@ -18,7 +31,7 @@ const parent={postMessage(message){
listeners.find(x=>x.type==='message').handler({source:parent,
data:{type:'framey:reply',id:message.id,result:true}});
}};
const context=vm.createContext({window,document:{getElementById:()=>({})},
const context=vm.createContext({window,document,
navigator:{language:'en'},parent,addEventListener:(type,handler)=>listeners.push({type,handler}),
setTimeout:()=>1,clearTimeout(){}});
vm.runInContext(host,context);
@@ -36,6 +49,19 @@ assert.equal(plugin.frameyQuickActions[0].label,'Read saved setting');
assert.equal(await plugin.frameyQuickActions[0].onClick(),'Saved setting is on');
assert.equal(calls[0].method,'get_setting');
assert.deepEqual(Array.from(calls[0].args),['enabled',false]);
// Exercise the built sample's save path through the real Framey toast host.
// Its JSX body must reach a React root, rather than stringify to an object.
content.children[0][0].children[0].props.onChange(true);
await new Promise(resolve=>setImmediate(resolve));
const toast=elements.get('framey-toasts').children[0];
assert.equal(toast.attributes.role,'status');
assert.equal(toast.children[0].textContent,'Framey sample');
const toastBody=roots.get(toast.children[1]).value;
assert.equal(toastBody.type,'span');
assert.match(JSON.stringify(toastBody),/Setting saved/);
assert.match(JSON.stringify(toastBody),/on/);
toast.children[2].onclick();
assert.equal(roots.get(toast.children[1]).unmounted,true);
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);
+51
View File
@@ -181,6 +181,27 @@ class StoreTransaction(unittest.TestCase):
self.assertNotEqual(result.returncode, 0)
self.assertFalse(self.folder.exists())
def test_remote_apply_rechecks_plugin_name_under_transaction_lock(self):
data, _ = archive()
other = self.folder.parent / 'Other'
other.mkdir()
(other / 'plugin.json').write_text('{"name":"Example"}')
result = self.apply('install', data)
self.assertNotEqual(result.returncode, 0)
self.assertIn('different plugin with this name', result.stderr)
self.assertFalse(self.folder.exists())
self.assertEqual((other / 'plugin.json').read_text(), '{"name":"Example"}')
self.assertEqual(list(self.folder.parent.glob('.frame-control-stage-*')), [])
(other / 'plugin.json').write_text('{"name":"Distinct"}')
self.assertEqual(self.apply('install', data).returncode, 0)
(other / 'plugin.json').write_text('{"name":"Example"}')
result = self.apply('update', data)
self.assertNotEqual(result.returncode, 0)
self.assertIn('different plugin with this name', result.stderr)
self.assertTrue(self.folder.exists())
self.assertEqual(list(self.folder.parent.glob('.frame-control-stage-*')), [])
class StoreHost(unittest.TestCase):
def test_reviewed_archive_changed_before_install_does_not_download(self):
@@ -219,6 +240,36 @@ class StoreHost(unittest.TestCase):
'install', 42)
network.assert_not_called()
def test_update_rejects_name_taken_by_another_plugin_before_download(self):
state = {'managed': True, 'running': True, 'plugins': [
{'storeId': 42, 'folder': 'Example', 'name': 'Old Name', 'version': '1.0'},
{'storeId': 43, 'folder': 'Other', 'name': 'Example', 'version': '1.0'}]}
catalog = [{'id': 42, 'name': 'Example', 'root': False,
'sha256': 'a' * 64, 'version': '2.0'}]
with patch.object(frame_decky_store.frame_decky, 'status', return_value=state), \
patch.object(frame_decky_store, 'catalog', return_value=catalog), \
patch.object(frame_decky_store.urllib.request, 'urlopen',
side_effect=AssertionError('No download expected')):
with self.assertRaisesRegex(ValueError, 'different plugin with this name'):
frame_decky_store.change(lambda *args, **kwargs: self.fail('No headset write expected'),
'update', 42)
def test_update_allows_its_own_existing_name(self):
data, digest = archive()
state = {'managed': True, 'running': True, 'plugins': [
{'storeId': 42, 'folder': 'Example', 'name': 'Example', 'version': '1.0'}]}
catalog = [{'id': 42, 'name': 'Example', 'root': False,
'sha256': digest, 'version': '2.0'}]
calls = []
with patch.object(frame_decky_store.frame_decky, 'status', return_value=state), \
patch.object(frame_decky_store, 'catalog', return_value=catalog), \
patch.object(frame_decky_store.urllib.request, 'urlopen', return_value=io.BytesIO(data)):
result = frame_decky_store.change(lambda command, **kwargs: calls.append(command) or '{}',
'update', 42)
self.assertEqual(result, {})
self.assertEqual(len(calls), 3) # Upload, apply, and temporary archive cleanup.
self.assertIn('update', calls[1])
def test_owned_plugin_can_be_removed_without_store_network(self):
state = {'managed': True, 'running': True, 'plugins':
[{'storeId': 42, 'folder': 'Example', 'name': 'Example', 'version': '1.0'}]}
+40
View File
@@ -20,6 +20,46 @@ import server
class Panel(unittest.TestCase):
def test_steam_reads_are_allowlisted_and_installed_only(self):
with patch.object(framey_panel.frame_decky, 'status', return_value={
'plugins':[{'storeId':81,'name':'IsThereAnyDeal for Deck'}]}):
for method, expression in framey_panel.STEAM_READS.items():
def fake_ssh(command, stdin, **_):
self.assertEqual(command, 'python3 -')
compile(stdin, '<Steam read>', 'exec')
self.assertIn(expression, stdin)
self.assertNotIn('__FRAMEY_STEAM_READ__', stdin)
return '{"result":"actual read"}'
self.assertEqual(framey_panel.read_steam(fake_ssh,81,method)['result'], 'actual read')
for plugin_id, method in [(True,'GetIPCountry'), (0,'GetIPCountry'),
(82,'GetIPCountry'), (81,'ShutdownPC'), (81,'User.GetIPCountry'),
(81,[]), (81,'GetIPCountry();alert(1)')]:
with self.subTest(plugin_id=plugin_id, method=method):
with self.assertRaises(framey_panel.PanelError):
framey_panel.read_steam(lambda *a,**k:self.fail('Unexpected SSH'),plugin_id,method)
def test_steam_read_response_limit_and_socket_cleanup(self):
from types import SimpleNamespace
for value, oversized in [('AU',False), ('x'*(256*1024),True)]:
closed=[]
page=SimpleNamespace(eval=lambda _:value,sock=SimpleNamespace(close=lambda:closed.append(True)))
fake_module=SimpleNamespace(Page=lambda:page)
script=framey_panel.STEAM_READ_SCRIPT.replace('__FRAMEY_STEAM_READ__',repr('safe()'))
with patch.dict(sys.modules, {'frame_steam':fake_module}), patch('builtins.print') as output:
if oversized:
with self.assertRaisesRegex(ValueError,'too large'):exec(script,{})
else:
exec(script,{})
self.assertEqual(json.loads(output.call_args.args[0]),{'result':'AU'})
self.assertEqual(closed,[True])
def test_steam_reads_have_the_headset_service_boundary(self):
with patch.object(server,'HEADSET_RUNTIME',False), patch.dict(server.os.environ,{'FRAME_E2E_CONTAINER':'0'}):
with self.assertRaises(server.Failure):server.framey_steam_action({'id':81,'method':'GetIPCountry'})
with patch.object(server,'HEADSET_RUNTIME',True), patch.object(framey_panel,'read_steam',return_value={'result':'AU'}) as read:
self.assertEqual(server.framey_steam_action({'id':81,'method':'GetIPCountry'}),{'result':'AU'})
read.assert_called_once_with(server.ssh,81,'GetIPCountry')
def test_decky_event_queue_scope_bounds_expiry_and_reconnect(self):
for name in ('framey_events.mjs', 'framey_event_parent.mjs'):
with self.subTest(script=name):
+2 -1
View File
@@ -17,7 +17,8 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SCRIPT = ROOT / "scripts" / "publish-release.sh"
FAKEGH = ROOT / "tests" / "fakegh"
INSTALLERS = ["Frame-Control-mac-arm64.dmg", "Frame-Control-mac-arm64.zip", "Frame-Control-Setup-x64.exe",
INSTALLERS = ["Frame-Control-mac-arm64.dmg", "Frame-Control-mac-arm64.zip",
"Frame-Control-mac-x64.dmg", "Frame-Control-mac-x64.zip", "Frame-Control-Setup-x64.exe",
"Frame-Control-win-x64.zip", "Frame-Control-linux-x86_64.AppImage",
"Frame-Control-linux-arm64.AppImage", "Frame-Control-linux-amd64.deb",
"Frame-Control-linux-arm64.deb"]
+1 -1
View File
@@ -17,7 +17,7 @@ class Checksums(unittest.TestCase):
for i, name in enumerate(module.INSTALLERS):
assets.append({"name": name, "digest": "sha256:" + hashlib.sha256(str(i).encode()).hexdigest()})
actual = module.checksums({"assets": assets})
self.assertEqual(len(actual.splitlines()), 8)
self.assertEqual(len(actual.splitlines()), 10)
self.assertEqual(actual.splitlines()[0], hashlib.sha256(b"0").hexdigest() + " " + module.INSTALLERS[0])
def test_missing_duplicate_and_bad_digest_fail(self):
+19 -3
View File
@@ -175,6 +175,19 @@ try:
z.extractall(stage)
unpacked=stage/folder
if not unpacked.is_dir(): raise RuntimeError('Missing plugin folder')
# Recheck under the transaction lock. Another store operation may
# have installed a colliding name while this archive downloaded.
proposed_name=json.loads((unpacked/'plugin.json').read_text()).get('name')
for peer in base.iterdir():
if peer==target or peer.is_symlink() or not peer.is_dir(): continue
manifest=peer/'plugin.json'
if manifest.is_symlink() or not manifest.is_file(): continue
try:
if manifest.stat().st_size>16384: continue
peer_data=json.loads(manifest.read_text())
if isinstance(peer_data,dict) and peer_data.get('name')==proposed_name:
raise RuntimeError('A different plugin with this name is already installed')
except (OSError,ValueError,UnicodeError): continue
(unpacked/marker).write_text(json.dumps({'storeId':int(plugin_id),'sha256':digest,'version':version})+'\n')
# Stop even a failed unit before removal to cancel any pending auto-restart.
if running or action=='remove': service('stop','frame-control-decky.service')
@@ -228,9 +241,12 @@ def change(ssh, action, plugin_id, report=lambda stage, percent=None: None, revi
raise ValueError('Root plugins are not supported by the Frame user service')
if plugin.get('compatibility', {}).get('blockInstall'):
raise ValueError(plugin['compatibility']['reason'])
existing_name = [p for p in state['plugins'] if p.get('name') == plugin['name']]
if action == 'install' and existing_name:
raise ValueError('A plugin with this name is already installed')
# Decky routes backend calls by plugin name. A store update may rename
# its plugin, so check names for updates as well as fresh installs.
existing_name = [p for p in state['plugins'] if p.get('name') == plugin['name']
and (action == 'install' or p.get('storeId') != plugin_id)]
if existing_name:
raise ValueError('A different plugin with this name is already installed')
if action == 'update' and len(installed) != 1:
raise ValueError('Only an installed Frame Control store plugin can be updated')
data = None
+11 -14
View File
@@ -6154,27 +6154,24 @@
"https://github.com/JtdeGraaf/IsThereAnyDeal-DeckyPlugin",
"https://github.com/JtdeGraaf/IsThereAnyDeal-DeckyPlugin.git"
],
"status": "broken",
"reason": "Country lookup calls missing SteamClient.User.GetIPCountry; deal loading fails after initialization.",
"status": "partial",
"reason": "Country settings render and select the real Frame Steam country through the read-only bridge. Settings writes, deal lookups and Steam page integration remain unverified; plugin backend calls were denied.",
"blockInstall": false,
"verification": [
"static archive",
"local browser",
"fake-Frame"
"fake-Frame",
"real Frame SSH"
],
"browser": {
"id": 81,
"name": "IsThereAnyDeal for Deck",
"ready": true,
"error": null,
"logs": [
{
"kind": "rejection",
"message": "TypeError: SteamClient.User.GetIPCountry is not a function"
}
],
"logs": [],
"methods": [
"settings_load"
"settings_load",
"settings_save"
]
},
"archiveValidation": {
@@ -7202,7 +7199,7 @@
"https://github.com/SteamDeckHomebrew/decky-frontend-lib"
],
"status": "broken",
"reason": "Initializer succeeds; game selection fails because SteamClient.InstallFolder.GetInstallFolders is unavailable.",
"reason": "Real Steam install folders are now available; game selection still fails on missing collectionStore.allGamesCollection. Steam library context, proxy fetch and backend actions remain unsupported/unverified.",
"blockInstall": false,
"verification": [
"static archive",
@@ -7217,11 +7214,11 @@
"logs": [
{
"kind": "error",
"message": "[decky-game-settings:GameSelectView] Error fetching installed games: TypeError: Cannot read properties of undefined (reading 'GetInstallFolders')"
"message": "[decky-game-settings:AuthorFooter] Error fetching avatar image: Error: Decky proxy fetch is unavailable in Framey"
},
{
"kind": "error",
"message": "[decky-game-settings:AuthorFooter] Error fetching avatar image: Error: Decky proxy fetch is unavailable in Framey"
"message": "[decky-game-settings:GameSelectView] Error fetching installed games: TypeError: Cannot read properties of undefined (reading 'allGamesCollection')"
}
],
"methods": []
@@ -10900,7 +10897,7 @@
},
"backend": "Python + native payload",
"hardware": "No specific hardware requirement detected statically; runtime requirements unverified.",
"nativePort": "LocalSend-Go commit 66edc509a28b9f877e7374504892dcbc653a4da4 cross-compiled successfully with CGO_ENABLED=0 GOOS=linux GOARCH=arm64. Static aarch64 ELF SHA-256 e79238f6051ce34f172f765ae918d3d902b819e27377989a55e5b1eba10e46f9. Not executed; source tip is not proven to match the shipped helper/protocol. Official ZIP is still x86_64.",
"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.",
"uiSurfaces": [
"local/Steam routes",
"toasts",
+4 -1
View File
@@ -335,10 +335,13 @@
'RegisterForGameActionStart','RegisterForGameActionEnd','RegisterForGameActionUserRequest']),
GameSessions:register(['RegisterForAppLifetimeNotifications','RegisterForScreenshotNotification']),
System:{...register(['RegisterForOnResumeFromSuspend','RegisterForOnSuspendRequest']),
GetSystemInfo:()=>request('GetSystemInfo',[],false,'framey:steam'),
UI:register(['RegisterForFocusChangeEvents','RegisterForSystemKeyEvents'])},
Input:register(['RegisterForControllerInputMessages','RegisterForControllerStateChanges']),
User:register(['RegisterForLoginStateChange','RegisterForPrepareForSystemSuspendProgress',
User:{...register(['RegisterForLoginStateChange','RegisterForPrepareForSystemSuspendProgress',
'RegisterForResumeSuspendedGamesProgress','RegisterForCurrentUserChanges']),
GetIPCountry:()=>request('GetIPCountry',[],false,'framey:steam')},
InstallFolder:{GetInstallFolders:()=>request('GetInstallFolders',[],false,'framey:steam')},
Overlay:register(['RegisterForOverlayActivated']),
UI:register(['RegisterForUIModeChanged']),
// This is private plugin cache, not Steam's account/global JSON store.
+35
View File
@@ -108,6 +108,41 @@ def call_method(ssh, plugin_id, method, args, legacy=False):
stdin=METHOD_SCRIPT, timeout=25))
# Only parameterless read APIs. Never resolve a plugin-supplied property path
# against SteamClient or evaluate a plugin-supplied expression in trusted CEF.
STEAM_READS = {
'GetIPCountry': 'SteamClient.User.GetIPCountry()',
'GetInstallFolders': 'SteamClient.InstallFolder.GetInstallFolders()',
'GetSystemInfo': 'SteamClient.System.GetSystemInfo()',
}
STEAM_READ_SCRIPT = r'''
import json, pathlib, sys
sys.path.insert(0, str(pathlib.Path.home()/'.local/share/frame-control/headset/bundle/ui'))
from frame_steam import Page
page = Page()
try:
encoded = json.dumps({'result':page.eval(__FRAMEY_STEAM_READ__)})
if len(encoded.encode('utf-8')) > 256*1024:
raise ValueError('Steam read response is too large')
print(encoded)
finally: page.sock.close()
'''
def read_steam(ssh, plugin_id, method):
"""Read one explicitly supported Steam API for an installed store panel."""
if type(plugin_id) is not int or plugin_id <= 0:
raise PanelError('Choose an installed Decky store plugin')
if not isinstance(method, str) or method not in STEAM_READS:
raise PanelError('This Steam API is unavailable in Framey')
found = [p for p in frame_decky.status(ssh)['plugins'] if p.get('storeId') == plugin_id]
if len(found) != 1:
raise PanelError('The plugin is not managed by the Frame Control Decky store')
expression = '(async()=>await ' + STEAM_READS[method] + ')()'
script = STEAM_READ_SCRIPT.replace('__FRAMEY_STEAM_READ__', repr(expression))
return json.loads(ssh('python3 -', stdin=script, timeout=25))
EVENT_SCRIPT = r'''
import json, pathlib, sys
sys.path.insert(0, str(pathlib.Path.home()/'.local/share/frame-control/headset/bundle/ui'))
+5 -2
View File
@@ -260,9 +260,12 @@ window.addEventListener('message',async event=>{
event.source.postMessage(reply,'*');
return;
}
if(req.type!=='framey:call'||req.session!==frameySession||!Number.isSafeInteger(req.id)||req.id<1||typeof req.method!=='string'||!Array.isArray(req.args))return;
if(!['framey:call','framey:steam'].includes(req.type)||req.session!==frameySession||!Number.isSafeInteger(req.id)||req.id<1||typeof req.method!=='string'||!Array.isArray(req.args))return;
const steamRead=req.type==='framey:steam';
if(steamRead&&(req.args.length!==0||!['GetIPCountry','GetInstallFolders','GetSystemInfo'].includes(req.method)))return;
const pluginId=activeFrameyId,session=frameySession,reply={type:'framey:reply',id:req.id};
try{const data=await api('/api/framey/method',{id:pluginId,method:req.method,args:req.args,legacy:req.legacy===true});reply.result=data.result;}
try{const data=await api(steamRead?'/api/framey/steam':'/api/framey/method',
{id:pluginId,method:req.method,args:req.args,legacy:req.legacy===true});reply.result=data.result;}
catch(error){reply.error=error.message;}
if(pluginId===activeFrameyId&&session===frameySession&&event.source===byId('frameyFrame').contentWindow)event.source.postMessage(reply,'*');
});
+10
View File
@@ -586,6 +586,15 @@ def framey_method_action(body):
raise Failure(str(exc), 400) from exc
def framey_steam_action(body):
if not HEADSET_RUNTIME and os.environ.get('FRAME_E2E_CONTAINER') != '1':
raise Failure('Decky plugin panels run in the headset service', 400)
try:
return framey_panel.read_steam(ssh, body.get('id'), body.get('method'))
except framey_panel.PanelError as exc:
raise Failure(str(exc), 400) from exc
def framey_events_action(body):
if not HEADSET_RUNTIME and os.environ.get('FRAME_E2E_CONTAINER') != '1':
raise Failure('Decky plugin panels run in the headset service', 400)
@@ -3462,6 +3471,7 @@ POST = {
"/api/decky": decky,
"/api/decky/plugin": decky_plugin,
"/api/framey/method": framey_method_action,
"/api/framey/steam": framey_steam_action,
"/api/framey/events": framey_events_action,
"/api/framey/check": framey_check_action,
"/api/headset": headset_action,