mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-26 06:00:26 +01:00
Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
792e9c016e | ||
|
|
aeac07cf54 | ||
|
|
d90d111218 | ||
|
|
8fc1117209 | ||
|
|
442b5ecfd8 | ||
|
|
5afffbef89 | ||
|
|
76f619280a | ||
|
|
2b92599d94 | ||
|
|
d87b863aa3 | ||
|
|
6aa74619d5 | ||
|
|
0a463dea23 | ||
|
|
e44ea65080 | ||
|
|
1bb5ed6216 | ||
|
|
e3bb608dde | ||
|
|
19e8b826c6 | ||
|
|
aea65bb282 | ||
|
|
2c7b93f756 | ||
|
|
879e84468b | ||
|
|
6ea9eb2618 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"version": "0.4.38",
|
||||
"version": "0.4.39",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frame-control",
|
||||
"version": "0.4.38",
|
||||
"version": "0.4.39",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^44.4.5",
|
||||
|
||||
+5
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"productName": "Frame Control",
|
||||
"version": "0.4.38",
|
||||
"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",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# What we have tested
|
||||
|
||||
Current for Frame Control **v0.4.38**, 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.
|
||||
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 |
|
||||
|---|---|---|---|---|
|
||||
| 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) |
|
||||
@@ -23,7 +24,7 @@ Current for Frame Control **v0.4.38**, 26 October 2026. “Real Frame + physical
|
||||
| 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 | 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), 44 partial, 60 broken and 11 not applicable. 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.38: [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), [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: [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) |
|
||||
| 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.
|
||||
@@ -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 |
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -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'},'*');}
|
||||
|
||||
@@ -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']:
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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,'*');
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user