Compare commits

...
Author SHA1 Message Date
SAPHID 70b4bb1368 Merge remote-tracking branch 'origin/main' into docs/v0444-release-evidence 2026-10-26 10:01:30 +00:00
Alex Southwell b961fc4cef Merge pull request #319 from saphid/fix/fakeframe-pacman-retry
Retry truncated fake Frame package downloads
2026-10-26 10:00:55 +00:00
SAPHID 9af06c7845 Retry transient fake Frame package downloads 2026-10-26 09:52:47 +00:00
SAPHID 9359b4ffbd Record v0.4.44 release verification 2026-10-26 09:40:30 +00:00
Alex Southwell 37e25e62ac Merge pull request #315 from saphid/release/v0.4.44
Prepare Frame Control v0.4.44
2026-10-26 09:17:25 +00:00
SAPHID d1e526cb4b Record exact v0.4.44 candidate Frame smoke 2026-10-26 09:01:33 +00:00
SAPHID 8230fdb19d Merge remote-tracking branch 'origin/main' into release/v0.4.44 2026-10-26 08:59:06 +00:00
Alex Southwell 2937a12864 Merge pull request #314 from saphid/docs/v0443-release-evidence
Document v0.4.43 tagged release verification
2026-10-26 08:58:23 +00:00
SAPHID 71fe4a3ebb Prepare Frame Control v0.4.44 candidate 2026-10-26 08:50:12 +00:00
SAPHID 42f3b2e1e7 Record post-tag Framey scroll button exercise 2026-10-26 08:47:27 +00:00
SAPHID ca4c370b33 Document v0.4.43 tagged release verification 2026-10-26 08:41:10 +00:00
Alex Southwell c50f96510f Merge pull request #313 from saphid/feat/framey-installed-app-context
fix(framey): restore Junk-Store About with installed metadata
2026-10-26 08:31:28 +00:00
Alex Southwell 43e801091a Merge pull request #312 from saphid/feat/framey-scroll-buttons
Add visible scroll controls to Framey plugin panels
2026-10-26 08:30:07 +00:00
Alex Southwell b4e632b218 Merge pull request #311 from saphid/release/v0.4.43
Prepare Frame Control v0.4.43
2026-10-26 08:25:09 +00:00
SAPHID d84491c0ec fix(framey): project installed metadata for Junk-Store About 2026-10-26 08:23:50 +00:00
SAPHID 83b8d13846 Add visible scroll controls to Framey plugin panels 2026-10-26 08:20:11 +00:00
SAPHID 340ca88359 Record test-only main merge in v0.4.43 candidate 2026-10-26 08:10:20 +00:00
SAPHID e62589417b Merge remote-tracking branch 'origin/main' into release/v0.4.43 2026-10-26 08:09:54 +00:00
Alex Southwell 03b3796d8d Merge pull request #310 from saphid/fix/windows-wireless-live-retry
Retry transient Windows wireless inventory smoke timeout
2026-10-26 08:09:31 +00:00
SAPHID 93da4e466f Prepare Frame Control v0.4.43 candidate 2026-10-26 08:05:12 +00:00
Alex Southwell 05093ea4d4 Merge pull request #309 from saphid/feat/framey-script-entrypoints
fix(framey): restore executable scripts for Junk-Store
2026-10-26 08:03:32 +00:00
SAPHID 232b253e7a Merge Web Browser compatibility into Junk-Store fix 2026-10-26 07:53:39 +00:00
Alex Southwell de8abe32c6 Merge pull request #308 from saphid/feat/framey-web-browser
Framey: handle missing Steam React root in legacy panels
2026-10-26 07:51:52 +00:00
SAPHID 42492e7879 Retry transient Windows wireless smoke timeout once 2026-10-26 07:45:31 +00:00
SAPHID f50139fe6a Merge main into Framey Web Browser compatibility 2026-10-26 07:34:55 +00:00
SAPHID 698a643eb7 Merge remote-tracking branch 'origin/main' into feat/framey-script-entrypoints
# Conflicts:
#	docs/framey-compatibility.md
2026-10-26 07:34:03 +00:00
SAPHID d0cef31656 fix(framey): restore executable plugin script entrypoints 2026-10-26 07:33:14 +00:00
Alex Southwell 4ac59f1a44 Merge pull request #307 from saphid/feat/framey-volume-boost-arm64
docs: verify Volume Boost sink controls on ARM64 Frame
2026-10-26 07:32:50 +00:00
SAPHID c91cfff797 docs: record exact Framey unworn regression gate 2026-10-26 07:28:07 +00:00
SAPHID d5e62f643d Framey: tolerate missing Steam React root in legacy panels 2026-10-26 07:26:21 +00:00
Alex Southwell f3f574666f Merge pull request #306 from saphid/docs/v0442-release-evidence
Document v0.4.42 tagged release verification
2026-10-26 07:13:01 +00:00
SAPHID 40ece4fef0 docs: verify Volume Boost sink controls on ARM64 Frame 2026-10-26 06:19:34 +00:00
Alex Southwell f52c020310 Merge pull request #305 from saphid/docs/framey-running-postmerge-vr
Document post-merge Framey running panel check
2026-10-26 06:04:36 +00:00
SAPHID efe0c508f4 docs: record v0.4.42 tagged release verification 2026-10-26 06:03:42 +00:00
SAPHID 1af2bb45d9 docs: record post-merge Framey running panel exercise 2026-10-26 05:56:16 +00:00
Alex Southwell daee952873 Merge pull request #304 from saphid/feat/framey-running-context
Framey: bridge running-game context and Steam lifetime notifications
2026-10-26 05:53:58 +00:00
Alex Southwell 98909cb543 Merge pull request #302 from saphid/release/v0.4.42
Prepare Frame Control v0.4.42
2026-10-26 05:52:18 +00:00
SAPHID 7172be17fc Document running receipt guard and its verification boundary 2026-10-26 05:39:17 +00:00
SAPHID 761e952d8f Merge remote-tracking branch 'origin/main' into feat/framey-running-context 2026-10-26 05:38:07 +00:00
SAPHID 0237e342d2 Bind Framey running polls and cleanup to the installed plugin receipt 2026-10-26 05:38:07 +00:00
SAPHID 10b196beaa Merge remote-tracking branch 'origin/main' into feat/framey-running-context 2026-10-26 05:23:14 +00:00
SAPHID 3f74ff4bb0 Add read-only running-game context and Steam lifetime relay for Framey 2026-10-26 05:23:13 +00:00
38 changed files with 4927 additions and 71 deletions

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "frame-control",
"version": "0.4.42",
"version": "0.4.44",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frame-control",
"version": "0.4.42",
"version": "0.4.44",
"license": "MIT",
"devDependencies": {
"electron": "^44.4.5",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "frame-control",
"productName": "Frame Control",
"version": "0.4.42",
"version": "0.4.44",
"description": "Desktop app for managing a Valve Steam Frame over SSH",
"private": true,
"main": "main.js",
@@ -0,0 +1,22 @@
# Fake Frame package download retry, 26 October 2026
The ARM64 fake Frame container sometimes fails before project tests start.
Valve's `holo-packages.steamos.cloud` returned package responses with **164
bytes missing** during the `pacman -Syu` layer in
`tests/fakeframe/Containerfile`:
| Run | Package and outcome |
| --- | --- |
| [v0.4.44 candidate checks](https://github.com/saphid/frame-control/actions/runs/40296431563) | `iptables` download truncated; a failed-only rerun passed on the unchanged PR head. |
| [PR #316 checks](https://github.com/saphid/frame-control/actions/runs/40298316496) | `c-ares` download truncated before e2e tests. |
| [PR #318 checks](https://github.com/saphid/frame-control/actions/runs/40301478955) | First attempt truncated `iptables`; failed-only rerun on the unchanged documentation head truncated `rsync`. The six other jobs passed. |
The fake Frame image now retries that package transaction at most five times,
with a two-second delay after each failed attempt except the last. A fifth
failure still fails the build. The end-to-end tests and their assertions are
unchanged. This retry affects only the fake Frame test image; it does not
change a real headset or the released application.
Exact-head hosted CI must pass before this change is merged. A successful
container build would show that at least one package attempt completed; it
would not establish that Valve's package service is permanently healthy.
@@ -0,0 +1,140 @@
{
"checkedAtUtc": "2026-10-26T08:14:37.777131+00:00",
"plugin": {
"id": 84,
"name": "Junk-Store",
"version": "1.1.13",
"sha256": "f7ad2a0103c85bf308929b2e96ec9aeeb0d76bcc6ae97eb1e4b5f31ffba0cbb1"
},
"projection": {
"field": "local_per_client_data.installed",
"wire": "optional ninth scalar: boolean or null",
"unknown": "null creates an empty detached metadata object; installed stays undefined",
"privateFieldsCopied": false,
"oldEightScalarRows": "accepted; running updates preserve existing library metadata",
"liveObservation": false
},
"realFrame": {
"os": "Frame OS 0.5.6",
"arch": "aarch64",
"uid": 1000,
"decky": {
"version": "v3.3.0",
"sha256": "8ef5dcf36cbbbb6e97201a6baed5d79c4cc44ec27789fc9af1c5b6ae8f0b25b6"
},
"transport": "trusted SSH through lxso2",
"sharedLock": true,
"runtime": "private bounded removable user service; not enabled",
"library": {
"apps": 1106,
"games": 895,
"installedTrue": 41,
"installedFalse": 0,
"installedUnknown": 1065
},
"protonRuntimeInstalledFields": "undefined for both EasyAntiCheat and BattlEye; no false value invented",
"legendaryInstalled": false,
"allowedMethods": [
"execute_action",
"get_websocket_port"
],
"staticActions": [
"MainMenu/GetContent",
"init/InitActions"
],
"noMutationActions": true
},
"localBrowser": {
"host": "actual headset.html/framey-host.js, vendored React, opaque iframe",
"backend": "exact official ARM64 plugin through production receipt-bound bridge",
"library": "production receipt-bound snapshot from real Frame CEF",
"outerStatusInstallCheck": "fixture responses",
"passed": [
"main menu",
"About content and tab list",
"Dependencies tab controls render; no buttons invoked",
"Back to main menu",
"close/reopen and About again"
],
"limits": [
"optional localhost websocket blocked by test relay CSP",
"RSS calls denied",
"running-context relay returns fixture/unavailable data; no lifetime claims",
"outer extension/status fixture is incomplete; unrelated outer status/extension errors excluded from plugin claim",
"deployed headset HTTP endpoint unverified",
"worn VR unverified"
]
},
"cleanup": {
"initialSshStop": "timed out while test unit deactivating",
"recovery": "SIGKILL to exact owned unit only after matching lock owner; private files removed",
"result": {
"baseAbsent": true,
"unitAbsent": true,
"archiveAbsent": true,
"state": "MainPID=0\nLoadState=not-found\nActiveState=inactive\n",
"normal": {
"frame-control-decky.service": "inactive",
"frame-control-headset.service": "inactive"
},
"battery": [
99
]
},
"freshLockedAudit": {
"services": 1,
"allArtifactsAbsent": true,
"allUnloadedInactivePidZero": true
},
"lockReleased": true,
"batteryBeforeAfter": [
[
99
],
[
99
]
],
"powerSettingChanged": false,
"localRelayAndBrowserClosed": true
},
"verification": {
"python": {
"run": 1628,
"skipped": 16,
"failures": 0
},
"panelFocused": {
"run": 22,
"failures": 0
},
"nodeContracts": [
"framey_actions.mjs",
"framey_running.mjs"
],
"siteTestFiles": {
"passed": 2,
"failures": 0
},
"fakeFrame": {
"blocked": 56,
"installed-removed": 60
},
"officialStore": {
"plugins": 116,
"errors": 0,
"identitySetUnchanged": true,
"checkedAtUtc": "2026-10-26T08:21:01.653768+00:00"
}
},
"status": "partial",
"unverified": [
"dependency/runtime installation",
"login",
"game listing/download/launch",
"RSS",
"optional websocket operations",
"Steam shortcut mutation",
"worn VR"
]
}
@@ -0,0 +1,62 @@
# Junk-Store About and read-only installed metadata
Junk-Store 1.1.13 (official ID 84) now renders its About and Dependencies tabs
without the `local_per_client_data.installed` crash recorded in the
[script-entrypoint audit](framey-script-entrypoints-2026-10-26.md). It remains
**partial**. The catalog and both store views use the same updated reason.
The library snapshot omitted the entire `local_per_client_data` object. The
official frontend dereferences it to display its dependency controls. Under the
shared headset lock, a read-only probe confirmed that the real Frame exposes
this object for the two Proton runtimes, but neither has an `installed` boolean.
Across 1,106 app overviews, 41 had `installed: true`, none had `false`, and 1,065
had no installed boolean. Those absent values must not be relabelled as false.
The snapshot adds one bounded scalar per app: a Steam-supplied boolean or null.
Framey's detached overview creates a minimal metadata object, copying `installed`
only for a boolean. Null leaves that property undefined. No client IDs, paths,
other metadata, live Steam objects or methods cross the iframe boundary. Eight
scalar rows remain accepted, and running-app updates preserve library metadata.
The snapshot still refreshes at panel open; it is not a live installation feed.
## Actual ARM64 backend and local Chromium
The unmodified, hash-verified official archive ran in published Decky 3.3.0 ARM64
on Frame OS 0.5.6 using a private, bounded, removable user service. Legendary was
absent; no dependency installer, runtime install, login, game operation or custom
backend download was invoked. The exact plugin and loader digests are in the
[redacted results](framey-installed-context-2026-10-26.json).
Local Chromium ran the actual Framey host and official frontend in an opaque
iframe. The production receipt-bound bridges read the real Steam library and
called the official ARM64 backend's static menu actions. About content and its
tab list, Dependencies controls, Back, close/reopen, and About again passed.
No dependency buttons were pressed. The real library counts matched the probe.
The optional localhost websocket was blocked by the test relay CSP; RSS was
deliberately denied. Outer status/install/check replies were fixtures; incomplete
outer extension/status and running-context replies are excluded from the plugin
claim. The screenshot still used the catalog reason from before this follow-up.
Dependency installation, login, game operations, RSS, websocket operations,
shortcut changes, deployed headset HTTP and worn VR remain unverified.
## Cleanup and regression checks
The initial cleanup SSH timed out while the private Decky unit was deactivating.
Recovery checked the matching lock owner, killed only that exact test unit, and
removed its private base, unit file and archive. A fresh locked audit independently
confirmed unloaded/inactive/PID 0 and no artifacts, then released its lock. Normal
managed Decky/panel services stayed inactive; battery was 99 → 99. No power-setting
action ran. Local relay and owned browser tab are closed.
- Full Python suite: 1,628 tests, 16 skipped, no failures; focused panel: 22 passed.
- Framey action/running Node contracts passed, including true/false/unknown,
malformed metadata, detached values, old rows and running-update preservation.
- Both site test files passed. Generated docs and diff checks passed.
- All 116 exact official archives passed the fake-Frame audit: 60 installed/removed,
56 expected blocks; disposable project removed. Backend execution is stubbed in
that harness. The fresh official store has 116 entries, no audit errors, and an
unchanged ID/version/digest set.
No new works claim is made. Catalog totals remain **4 works / 49 partial / 52 broken
/ 11 N/A**; only Junk-Store's verified scope and remaining limits change.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,101 @@
# Running-game context and Discord Status, 2026-10-26
Framey now projects running apps and the selected main app from Steam CEF into
detached `SteamUIStore` / `DFL.Router` getters. It relays actual Steam lifetime
callbacks while the panel is open. Public Steam CDN capsule URLs are projected
for running apps; local artwork paths and URL query data never cross the bridge.
The [machine-readable evidence](framey-running-context-2026-10-26.json) records
the exact archives, fixtures, browser observations and cleanup results.
| Plugin / scope | Result | Verification |
| --- | --- | --- |
| Discord Status 1.5.0 | **Broken → partial.** Disconnected panel renders; activity construction accepts the synthetic running game without the previous missing-store/artwork errors. | Exact unmodified official frontend in opaque Chromium iframe; backend/network denied. Actual official `is_connected` returns false in real Decky 3.3.0 ARM64, with no Discord IPC present. |
| Pause Games | **Partial.** Running-list initialization reaches `pid_from_appid` for the synthetic game. | Local Chromium fixture; all backend calls, including settings writes and process actions, denied. |
| Notebook | **Works for its previously verified default notebook scope.** Per-game context is now supplied, but real per-game selection remains unverified. | Synthetic game causes the official frontend to request `loadPage`; backend calls denied in this pass. Earlier default save/reopen ARM64 evidence remains separate. |
| PlayTime | **Partial.** Running snapshots do not satisfy its private MobX observation. | Local Chromium explicitly logs that its running-app observer is unavailable. No automatic tracking claim. |
| 14 other focused panels | Labels retained. Initialization alone does not establish backend functionality. | Exact frontends with synthetic nonempty running/library snapshots and backend/network denied; Game Sync still stops on the denied `get_config` read. |
The bridge addresses a shared dependency detected in 34 official frontend
bundles, including hardware-inapplicable plugins whose installation remains
blocked. This is infrastructure coverage, not 34 working plugins. Catalog after
this pass: **3 works / 49 partial / 53 broken / 11 N/A**.
## Real Frame checks and cleanup
Every device probe acquired the atomic `/tmp/frame-test.lock` and released only
its matching owner. Read-only probes verified the actual RunningApps and
MainRunningApp getter implementations, the lifetime API's object with an
`unregister()` method, and a public `https://shared.steamstatic.com` capsule URL.
A private removable user service ran the published Decky 3.3.0 ARM64 executable
and the exact unmodified Discord Status 1.5.0 store archive. Production
`framey_panel.call_method` returned false from `is_connected`. Production
`poll_running` verified two empty running-list reads, an actual lifetime
subscription and explicit close. The device had no running apps: **nonempty
reads and launch/exit callbacks were not observed on hardware**. Decky startup
reinjected its frontend/restarted Steam CEF. No plugin game, power, audio or process-control
action was invoked, and no Discord connection/activity mutation was allowed.
After close, CEF reported zero sessions and a null timer. The test service,
unit and private base were removed. A fresh independently locked audit read
`LoadState=not-found`, `ActiveState=inactive`, `MainPID=0`, absent paths and zero
CEF sessions/timers. Battery stayed **99 → 99**; normal Decky stayed failed and
the normal panel service stayed inactive. No headset wipe or system service
installation was used.
## Local verification and limits
- Full current official store fetch: 116 IDs/versions/digests unchanged at
2026-10-26 05:18:22 UTC.
- Fake Frame: 116 transport cases, 60 install/remove cycles and 56 expected
blockers; private Compose project and volumes removed. Plugin execution was
stubbed.
- Full local Chromium sweep: 103 of 116 initializer-ready with explicitly
synthetic library/running data. No backend/network request was allowed.
The focused Discord Status observation lasted eight seconds and constructed
`Test Running Game` activity; the visible UI correctly remained disconnected.
- Node contract tests cover actual-event queue projection, retry/deduplication,
bounds, expiry, unregister, reconnect, update-before-callback ordering,
invalid data, public-artwork filtering, stale sessions, overlapping reads,
failure clearing stale state and late replies after close. Nonempty app rows
and launch/exit notifications in these tests are explicit synthetic fixtures.
The queue retains at most 64 real notifications per session; overflow can lose
old callbacks. Polling runs about every two seconds with no overlap. At most four
CEF sessions survive; unreachable cleanup expires after 15 seconds, checked
every five seconds. The bridge does not implement private MobX internals,
suspend/input events, game actions or closed-panel automation.
Discord shortcut detection, real IPC connection/activity, local custom artwork,
actual running-game transitions, worn VR and physical controller/keyboard use
remain unverified. None of the new fixture checks promotes a plugin to works.
After #301 merged, running polls and both close paths adopted its pinned SHA-256
receipt guard. Local Python/Node tests verify the parent carries that receipt,
missing/stale receipts are rejected before CEF after plugin replacement, and a
reopened panel can poll and close with the new receipt. Rejected stale closes
retain the tested finite-lease cleanup. This authorization-only follow-up did
not repeat the device checks above; their recorded hardware scope is unchanged.
## Post-merge unworn panel exercise
On merged main `daee952873255b86018cce1d2a4d3bd070ec4341`,
`scripts/framey-vr-check.py --exercise --output /tmp/framey-post304-exercise`
acquired its own `/tmp/frame-test.lock` before any headset read. At 05:54 UTC,
preflight found SteamVR activity 3 (standby), proximity 6.25 and battery 99%.
The temporary sample panel passed the real Decky setting save and fresh-iframe
reopen, a trusted synthetic OpenVR pointer, quick action, slider value 50 and
CDP text entry after OpenVR focus. Stereo compositor SHA-256 changed from
`50fcab1ee25e6d9398106cc0c24ec6616e07bcf4573b52be1774c715b756060b`
to `98241806fb1f775e7f4c5ef0b0564bfd3c552ee896ff225b4d0cb5b7b53f70d1`;
the `/dev/video99` mirror capture hash was
`c284c05f86b60362168c1d1f58fe775974b70ac54bbfd57834802b892eb4509e`.
The command exited 0 with `passed=true`. The original power values (`false` and
`5.000000`) were read back, battery remained 99%, the recovery timer was removed,
cleanup errors were empty and the run released only its own lock. The private
full report is `/tmp/framey-post304-exercise/result.json` on the test host.
This exercise covered the sample panel path after the running-context merge. It
did not observe a nonempty running-game list, a game lifetime callback, Discord
activity, a worn display or physical controller input. Synthetic native scroll
again did not move (`scrollTop=0`, no wheel event).
@@ -0,0 +1,184 @@
{
"checkedAtUtc": "2026-10-26T07:22:08.822239+00:00",
"plugin": {
"id": 84,
"name": "Junk-Store",
"version": "1.1.13",
"sha256": "f7ad2a0103c85bf308929b2e96ec9aeeb0d76bcc6ae97eb1e4b5f31ffba0cbb1",
"license": "BSD-3-Clause",
"sourceModified": false
},
"installer": {
"beforeScriptMode": "0644",
"afterScriptMode": "0744",
"policy": "Add owner execute for a shebang or ZIP-declared executable; do not restore archive privilege/write bits. Existing installs require update or reinstall."
},
"isolatedBackend": {
"runtime": "published Decky 3.3.0 amd64",
"loaderSha256": "e80d03e641d1766a33a817cb27ab8d2e0a4633685e5d27e4de948a6687526181",
"isolation": [
"unprivileged",
"no network",
"no devices",
"no capabilities",
"read-only root",
"private tmpfs home and tmp"
],
"beforeInit": {
"result": {
"Type": "Error",
"Content": {
"Message": "Error parsing json result Expecting value: line 1 column 1 (char 0)",
"Data": "",
"ActionName": "init",
"ActionSet": "init"
}
},
"success": true
},
"afterInit": {
"result": {
"Type": "Success",
"Content": {
"Message": "Initialized"
}
},
"success": true
},
"beforeInitActions": {
"result": {
"Type": "Error",
"Content": {
"Message": "Error parsing json result Expecting value: line 1 column 1 (char 0)",
"Data": "",
"ActionName": "InitActions",
"ActionSet": "init"
}
},
"success": true
},
"afterInitActions": {
"outerSuccess": true,
"innerType": "ActionSet",
"setName": "MainMenu",
"actionCount": 4
}
},
"realFrame": {
"os": "Frame OS 0.5.6",
"arch": "aarch64",
"uid": 1000,
"decky": {
"version": "v3.3.0",
"sha256": "8ef5dcf36cbbbb6e97201a6baed5d79c4cc44ec27789fc9af1c5b6ae8f0b25b6"
},
"transport": "existing trusted SSH via lxso2",
"sharedLock": true,
"service": "private removable user service; not enabled; RuntimeMaxSec=600",
"installedWithProductionApply": true,
"scriptMode": "0o744",
"legendaryInstalled": false,
"backendMethods": [
"execute_action",
"get_websocket_port"
],
"staticActions": [
"JunkStoreActions/GetContent",
"JunkStoreActions/GetEpicActions",
"MainMenu/GetContent",
"MainMenu/JunkStoreInit",
"init/InitActions"
],
"websocketPort": 8765,
"websocketCommandsSent": false,
"librarySnapshotCounts": {
"apps": 1106,
"games": 895
},
"explicitInitActionInvoked": false,
"normalDeckyBeforeAfter": [
"inactive",
"inactive"
],
"normalPanelBeforeAfter": [
"inactive",
"inactive"
],
"batteryBeforeAfter": [
[
99
],
[
100
]
],
"powerSettingChanged": false,
"cleanup": {
"baseAbsent": true,
"unitAbsent": true
},
"lockReleased": true
},
"localBrowser": {
"runtime": "local Chromium, real headset.html/framey-host.js, vendored React, official frontend in opaque iframe",
"backend": "production receipt-bound bridge to actual ARM64 backend",
"steamLibrary": "production receipt-bound GetLibrarySnapshot; only counts retained",
"outerStatusInstallResponses": "fixtures",
"passed": [
"Custom Stores main menu and Games/News/About navigation items render",
"Games route opens static Epic tab",
"Back returns to main menu",
"close/reopen returns to main menu"
],
"remainingFailure": "About: Cannot read properties of undefined (reading 'installed'); detached app overview lacks local_per_client_data.installed",
"deniedActions": [
"fetch_rss_feed",
"EpicActions/GetContent"
],
"denialIsNotBackendFailure": true
},
"cleanupAudit": {
"attempts": 3,
"allPrivateBasesAbsent": true,
"allPrivateUnitsAbsent": true,
"allArchivesAbsent": true,
"allServicesInactive": true,
"freshAuditLockReleased": true
},
"verification": {
"python": {
"run": 1628,
"skipped": 16,
"failures": 0
},
"site": {
"passed": 18
},
"focusedStore": {
"passed": 19
},
"fakeFrame": {
"blocked": 56,
"installed-removed": 60
},
"officialStore": {
"plugins": 116,
"errors": 0,
"identitySetUnchanged": true,
"checkedAtUtc": "2026-10-26T07:22:17.371046+00:00"
},
"staticEntrypointArchives": 28
},
"unverified": [
"Legendary dependency installation",
"Epic login",
"game listing",
"game downloads or launch",
"RSS feed",
"Steam shortcut mutation",
"optional websocket operations",
"deployed headset HTTP endpoint",
"worn VR"
],
"status": "partial"
}
@@ -0,0 +1,76 @@
# Executable script entrypoints: Junk-Store
Junk-Store 1.1.13 (official ID 84) moves **broken → partial**. The exact unmodified
BSD-3-Clause archive is pinned in the [redacted results](framey-script-entrypoints-2026-10-26.json).
No ARM64 rebuild or upstream plugin source patch was needed.
Python ZIP extraction drops execute permissions. This archive also publishes its
shebang scripts as 0644, relying on upstream Decky's recursive permission setup.
Framey's user service disables that setup. Direct script calls therefore fail.
The installer now adds **owner execute only** to shebang scripts or ZIP-declared
executables. It does not restore archive privilege/write bits, grant group/other
execute, or change unmarked data permissions. Archive validation and installation
blocks remain in place. Existing installations need update/reinstall for repair.
The upstream comparison is Decky 3.3.0's
[permission setup](https://github.com/SteamDeckHomebrew/decky-loader/blob/v3.3.0/backend/decky_loader/browser.py)
and [Linux implementation](https://github.com/SteamDeckHomebrew/decky-loader/blob/v3.3.0/backend/decky_loader/localplatform/localplatformlinux.py).
Static inspection found 28 accepted official archives with shebang or declared
executable members. This is shared installer coverage, not 28 functional claims.
## Before and after in an isolated real backend
Both runs used the exact official plugin and published Decky 3.3.0 amd64, with no
network/devices/capabilities, a read-only root and private temporary home/tmp.
The only difference was the production installer before/after this change.
| Check | Before | After |
| --- | --- | --- |
| `scripts/get-json.py` | 0644 | 0744 |
| `execute_action(init, init)` | inner `Type:Error`, empty output / JSON parse error | `Type:Success`, `Initialized` |
| `execute_action(init, InitActions)` | inner `Type:Error` | `Type:ActionSet`, `MainMenu`, four actions |
The legacy wrapper reports `success:true` even for the failing inner error.
Startup log text alone is also insufficient: the baseline emits initialized/started
messages despite those errors. Both private containers were removed.
## Real ARM64 Frame and local Chromium
Under the shared headset lock, the patched production installer installed the
official archive into a private, bounded, removable Decky 3.3.0 ARM64 user service
on Frame OS 0.5.6. The service was not enabled. Legendary was absent before and
after; no dependency installer, login, download, launch or websocket command ran.
The production receipt-bound bridge called the actual backend through live Steam
CEF. Static action sets, main-menu content and the Epic action set returned real
results. `get_websocket_port` returned 8765. Explicit `init/init` success was tested
in the isolated amd64 backend; it was not explicitly invoked in the final Frame run.
Local Chromium ran the actual Framey host and official frontend in an opaque
iframe, relaying only allowed static calls to that ARM64 backend. Main menu,
Games → static Epic tab, Back and fresh close/reopen passed. Steam library reads
used the production receipt-bound bridge; only counts were retained. Outer
status/install responses were fixtures, and the deployed headset HTTP endpoint
was not tested.
The About route still crashes on missing `local_per_client_data.installed` in
the detached app overview. Epic game-list and RSS calls were deliberately denied;
their loading/error views do not demonstrate backend failure. Dependency
installation, login, game operations, RSS, Steam shortcut mutation and worn VR
remain unverified. This supports **partial**, not works.
Three temporary attempts were cleaned, including an initial Steam reinjection
readiness failure and a test-relay omission of the library endpoint. A fresh
shared-lock audit found all three services inactive/unloaded (PID 0), with their
private bases, unit files and archives absent. Normal managed Decky/panel services
stayed inactive; final battery was 99 → 100 while charging. No power-setting action
ran. The audit lock was released, and local relays/browser tabs were closed.
## Verification
- Python suite: 1,628 tests, 16 skipped, no failures; store suite: 19 passed.
- Site suite: 18 passed. Two new regression tests failed before the fix.
- All 116 official plugins: fake-Frame 60 installed/removed, 56 expected blocks;
no actual plugin backends run by this harness. Disposable project cleaned.
- Fresh official-store audit: 116 plugins, zero errors; ID/version/digest set
matches the catalog. Other plugin statuses are unchanged.
@@ -0,0 +1,405 @@
{
"baseCommit": "daee952873255b86018cce1d2a4d3bd070ec4341",
"plugin": {
"id": 58,
"name": "Volume Boost",
"version": "0.0.7",
"sha256": "2cc576a7f60751306d690268e227df626e59d3733680ced753de4c116629a9cc"
},
"runtime": {
"version": "v3.3.0",
"sha256": "8ef5dcf36cbbbb6e97201a6baed5d79c4cc44ec27789fc9af1c5b6ae8f0b25b6",
"os": "Frame OS 0.5.6",
"arch": "aarch64"
},
"verificationLimits": {
"fakeFrame": "Prior exact archive HTTP/SSH install/receipt/removal; plugin execution stubbed, not repeated",
"localBrowser": "Actual headset.html, React and Framey host, exact unmodified official frontend in opaque iframe; direct RPC relay to actual ARM64 backend",
"realFrameSSH": "Production receipt-bound framey_panel.call_method over SSH and live Steam CEF DeckyBackend.call into actual Decky 3.3.0 ARM64 plugin; default speaker sink remained muted throughout",
"wornVR": "Not tested",
"audibleOutput": "Not tested; gain writes and readbacks only",
"deployedHTTP": "Not tested; outer status/install/check responses were fixtures, methods used production call_method with exact current receipt"
},
"catalogRefresh": {
"checkedAtUtc": "2026-10-26T06:10:58.537029+00:00",
"plugins": 116,
"changedIdsVersionsDigests": 0
},
"runs": [
{
"timeUtc": "2026-10-26T06:07:19.370988+00:00",
"locale": "LC_ALL=C for the private Decky service",
"originalSink": {
"mute": true,
"volumes": {
"front-left": 0,
"front-right": 0
}
},
"writes": [
{
"request": {
"value": 125,
"channel": "both"
},
"result": {
"result": true,
"success": true
},
"independentPulseReadback": {
"mute": true,
"volumes": {
"front-left": 81920,
"front-right": 81920
}
},
"backendReadback": {
"left": {
"result": 125,
"success": true
},
"right": {
"result": 125,
"success": true
}
},
"guardArmedBeforeWrite": true,
"exactOriginalRestored": true
},
{
"request": {
"value": 75,
"channel": "both"
},
"result": {
"result": true,
"success": true
},
"independentPulseReadback": {
"mute": true,
"volumes": {
"front-left": 49152,
"front-right": 49152
}
},
"backendReadback": {
"left": {
"result": 75,
"success": true
},
"right": {
"result": 75,
"success": true
}
},
"guardArmedBeforeWrite": true,
"exactOriginalRestored": true
},
{
"request": {
"value": 74,
"channel": "both"
},
"result": {
"result": true,
"success": true
},
"independentPulseReadback": {
"mute": true,
"volumes": {
"front-left": 48496,
"front-right": 48496
}
},
"backendReadback": {
"left": {
"result": 74,
"success": true
},
"right": {
"result": 74,
"success": true
}
},
"guardArmedBeforeWrite": true,
"exactOriginalRestored": true
},
{
"request": {
"value": 33,
"channel": "left"
},
"result": {
"result": true,
"success": true
},
"independentPulseReadback": {
"mute": true,
"volumes": {
"front-left": 21626,
"front-right": 0
}
},
"backendReadback": {
"left": {
"result": 33,
"success": true
},
"right": {
"result": 0,
"success": true
}
},
"guardArmedBeforeWrite": true,
"exactOriginalRestored": true
},
{
"request": {
"value": 67,
"channel": "right"
},
"result": {
"result": true,
"success": true
},
"independentPulseReadback": {
"mute": true,
"volumes": {
"front-left": 0,
"front-right": 43909
}
},
"backendReadback": {
"left": {
"result": 0,
"success": true
},
"right": {
"result": 67,
"success": true
}
},
"guardArmedBeforeWrite": true,
"exactOriginalRestored": true
},
{
"request": {
"value": 33,
"channel": "right"
},
"result": {
"result": true,
"success": true
},
"independentPulseReadback": {
"mute": true,
"volumes": {
"front-left": 0,
"front-right": 21626
}
},
"backendReadback": {
"left": {
"result": 0,
"success": true
},
"right": {
"result": 33,
"success": true
}
},
"guardArmedBeforeWrite": true,
"exactOriginalRestored": true
}
],
"reads": [
{
"id": 58,
"method": "get_volume_state",
"args": [
{
"channel": "left"
}
],
"result": {
"result": 0,
"success": true
}
},
{
"id": 58,
"method": "get_volume_state",
"args": [
{
"channel": "left"
}
],
"result": {
"result": 0,
"success": true
}
}
],
"reopen": "Fresh query/session, ready; UI returned to actual restored zero; no write on mount",
"before": {
"battery": [
100
],
"uid": 1000,
"arch": "aarch64",
"decky": "inactive",
"panel": "inactive"
},
"after": {
"battery": [
100
],
"uid": 1000,
"arch": "aarch64",
"decky": "inactive",
"panel": "inactive"
},
"cleanup": {
"baseAbsent": true,
"unitAbsent": true
},
"exactSinkRestored": true,
"lockReleased": true
},
{
"timeUtc": "2026-10-26T06:12:04.889613+00:00",
"locale": "Inherited user-service environment, matching production unit; no LANG or LC_ALL override",
"originalSink": {
"mute": true,
"volumes": {
"front-left": 0,
"front-right": 0
}
},
"writes": [
{
"request": {
"value": 150,
"channel": "both"
},
"result": {
"result": true,
"success": true
},
"independentPulseReadback": {
"mute": true,
"volumes": {
"front-left": 98304,
"front-right": 98304
}
},
"backendReadback": {
"left": {
"result": 150,
"success": true
},
"right": {
"result": 150,
"success": true
}
},
"guardArmedBeforeWrite": true,
"exactOriginalRestored": true
}
],
"reads": [
{
"id": 58,
"method": "get_volume_state",
"args": [
{
"channel": "left"
}
],
"result": {
"result": 0,
"success": true
}
},
{
"id": 58,
"method": "get_volume_state",
"args": [
{
"channel": "left"
}
],
"result": {
"result": 0,
"success": true
}
}
],
"reopen": "Fresh query/session, ready; UI returned to actual restored zero; no write on mount",
"before": {
"battery": [
100
],
"uid": 1000,
"arch": "aarch64",
"decky": "inactive",
"panel": "inactive"
},
"after": {
"battery": [
100
],
"uid": 1000,
"arch": "aarch64",
"decky": "inactive",
"panel": "inactive"
},
"cleanup": {
"baseAbsent": true,
"unitAbsent": true
},
"exactSinkRestored": true,
"lockReleased": true
}
],
"guard": {
"scope": "Private test helper only; not a shipped feature or plugin patch",
"timeoutSeconds": 60,
"runtimeMaxSec": 120,
"sequence": [
"Capture exact default sink identity, native per-channel volume integers and original mute",
"Persist recovery state; start bounded device-side systemd watchdog before mute/write",
"Require stereo front-left/front-right and mute readback before plugin call",
"Only official get_volume_state/set_volume_state; value 0..150, known channels",
"Read native Pulse volume and mute independently; read both channels through actual plugin",
"Restore exact native volume while muted, verify, then restore original mute and verify exact snapshot",
"Remove recovery state, stop only own watchdog; cleanup own service/base and release matching lock"
],
"automaticTimeoutExercised": false,
"explicitRestoreExercised": true
},
"freshCleanupAudit": {
"exactSinkRestored": true,
"baseAbsent": true,
"unitAbsent": true,
"services": {
"framey-boost-kp0z857q58-b4126054.service": "MainPID=0\nLoadState=not-found\nActiveState=inactive\n",
"framey-boost-kp0z857q58-b4126054-guard.service": "MainPID=0\nLoadState=not-found\nActiveState=inactive\n"
},
"battery": [
100
]
},
"remaining": [
"Audible sound/quality, perceived loudness and physical speaker gain are unverified",
"Worn VR, headset controller input and Bluetooth sinks are unverified",
"No default route change, playback or power-setting action was invoked",
"Decky startup reinjects its frontend and can restart Steam CEF; normal managed services stayed inactive"
],
"repositoryChecks": {
"python": "1,626 tests, 16 skips, no failures",
"site": "18 passed",
"catalogDocCheck": "passed",
"diffCheck": "passed"
}
}
@@ -0,0 +1,96 @@
# Volume Boost sink controls on the ARM64 Frame
Volume Boost 0.0.7 (official store ID 58) now qualifies as **works for sink
controls**. The exact official, unmodified Python backend ran in Decky 3.3.0
ARM64 on Frame OS 0.5.6. Local Chromium rendered its official frontend through
Framey's existing legacy bridge. Actual control changes reached the Frame's
default speaker sink, including above-100% gain, with independent Pulse
readbacks. The sink stayed muted; no audible-output or worn-VR claim is made.
[Digest-pinned observations and cleanup](framey-volume-boost-arm64-2026-10-26.json)
record both runs. This supersedes the Volume Boost read-only limitation in the
[previous ARM64 audio check](framey-audio-arm64-2026-10-26.md). No plugin patch,
native rebuild or new compatibility shim was necessary. Volume Boost's backend
uses Python and the Frame's existing ARM64 `pactl`; its hard-coded UID 1000
matches the actual Frame user.
| Local Chromium action | Actual ARM64 result, independently read back |
| --- | --- |
| Linked numeric input 125% | Native Pulse channel values 81920 / 81920; plugin reads 125 / 125 |
| Linked slider click, then ArrowDown | 75% then 74%; native values 49152 / 49152 then 48496 / 48496 |
| Unlink, set left to 33% | Native values 21626 / 0; plugin reads 33 / 0 |
| Set right to 67% | Native values 0 / 43909; plugin reads 0 / 67 |
| Re-link | Official frontend issues a right-only 33% write; native values 0 / 21626 |
| Close and reopen | New document/session, ready; backend reads and displayed linked value return to the restored 0%; no mount write |
| Repeat linked numeric 150% with inherited service locale | Native values 98304 / 98304; plugin reads 150 / 150; reopen again reads restored zero |
The test restores the original sink state after **each** write. Therefore the
separate-channel checks intentionally begin from 0% / 0%, rather than from the
previous UI value. A right-only re-link request is the published plugin's
behavior. These checks verify channel selection and preservation of the other
channel; they do not claim a continuous unmuted listening session. Pulse's
integer conversion truncates fractional native values; the evidence retains
those integers rather than replacing them with rounded percentages.
## Runtime and transport
Each run separately acquired the atomic `/tmp/frame-test.lock`; neither stole
an existing lock. A unique private Decky user service used its own plugin,
settings and runtime directories, was never enabled, and had an eight-minute
runtime limit. The upstream ARM64 loader matched GitHub's published SHA-256
and ELF machine 183. The plugin ZIP matched the official digest.
The local relay served actual `ui/headset.html`, vendored React and
`framey-host.js`, with exact official frontend bytes in an opaque
`allow-scripts` iframe. Outer status/install/check responses were fixtures.
Plugin calls used production `framey_panel.call_method`, including the current
store receipt, SSH through lxso2, live Steam CEF and `DeckyBackend.call` into the
actual plugin subprocess. Only the temporary install paths were redirected.
This does not verify the deployed headset HTTP endpoint or a VR compositor.
The first private service explicitly used `LC_ALL=C`. The second inherited the
user-service locale without a `LANG` or `LC_ALL` override, matching the
production service configuration, and repeated actual reads, a 150% write and
reopen. Zero alone was not accepted as success: upstream returns zero on
command failure. Every write returned true, both backend channel reads agreed,
and independent native Pulse readbacks confirmed nonzero changes.
## Bounded muted writes and restoration
The default sink was already muted at native channel values 0 / 0. Before each
plugin write, a private device-side helper captured the exact sink identity,
channel values and mute state; persisted recovery state; and started a bounded
60-second systemd watchdog. It then required a successful mute readback before
allowing the plugin call. The relay allowed only `get_volume_state` and
`set_volume_state`, known channels and values from 0 through 150.
After the call, independent `pactl --format=json list sinks` and both plugin
reads checked the result. In `finally`, the helper restored exact native
channel values while muted, checked them, restored the original mute state,
and verified the entire snapshot before removing recovery state and stopping
its watchdog. No default route, playback, existing stream or power setting was
changed. Automatic watchdog expiry was armed but not deliberately exercised;
explicit restoration passed after all seven writes.
Both private Decky services/bases were removed and their locks released.
Fresh-lock cleanup audits found their Decky and guard units unloaded/inactive
with PID 0, paths absent, and the exact original sink/mute restored. Battery
stayed 100 → 100; normal managed Decky and headset-panel services stayed
inactive. Decky startup reinjects its frontend and can restart Steam CEF.
## Catalog and verification limits
The canonical store catalog and generated table report **4 works / 48 partial /
53 broken / 11 N/A**. A fresh full official-store fetch at 06:10:58 UTC found
all 116 IDs, versions and digests unchanged. Historical fake-Frame checks cover
this exact archive's install/receipt/removal with backend execution stubbed;
that test was not repeated or relabelled as real-backend evidence.
Audible gain, sound quality, physical speaker output, Bluetooth sinks,
headset-controller input and worn VR remain unverified. The store row includes
these limits. This change records new real-backend evidence and updates its
compatibility classification; it changes no shipped audio implementation.
The full server suite completed 1,626 tests with no failures (16 skips); all
18 site tests passed. Generated-document consistency and `git diff --check`
also passed. No implementation change followed those tests.
@@ -0,0 +1,48 @@
# Official Web Browser 1.4.3 frontend follow-up
The exact official Decky store archive for ID 59 is 91,859 bytes and matched
SHA-256 `1192864fddc5d72550346b8dff02c21d71f0c40a06ac41c4e67bb6220d267f9d`.
Its legacy bundle calls `deckyFrontendLib.getReactRoot` when attempting to patch
Steam's main menu and search bar. The prior full-catalog audit recorded
`getReactRoot is not a function` and initializer failure.
Framey's isolated iframe has no Steam React root. The host now exposes
`getReactRoot()` with a **null** result, consistent with its existing inert
`findInReactTree()` lookup. It never returns a fabricated Steam tree or permits
patching the user's Steam UI. The focused Node host test checks this contract.
A focused local Chromium run used the verified archive through
`scripts/framey-audit-browser.py`, with every backend method, fetch and
WebSocket denied. The exact frontend reported `ready=true` with no initializer
error after 2.5 seconds, and again during a longer ten-second observation.
It attempted `set_main_tab` and `load`; the audit denied both. Console output
reported that no Steam window or main-menu root was available. The panel did
not show a usable browser view in the audited state.
Static inspection of this archive's published Python backend shows
`set_main_tab()` requesting a Steam GamepadUI CDP tab, then returning a key and
port for its own WebSocket server. That Steam tab and browser injection path
are outside Framey's isolated panel. No real plugin backend was executed, no
browser tab or navigation was verified, and no real Frame or worn-VR check was
performed for this change. The catalog retains **broken** for this digest; its
reason and focused observation now distinguish successful initialization from
the unresolved core browser dependency. The full 116-plugin browser baseline
still refers to its earlier host commit; this is a one-plugin follow-up.
## Unworn Framey regression gate
The exact code commit `d5e62f643d462b7070f27c2c47b11b4aa9982094` ran
`scripts/framey-vr-check.py --exercise` under its own shared-lock owner at
07:26 UTC. Preflight found SteamVR activity 3 (standby), proximity 6.25 and
battery 100%. The real Decky sample setting saved `enabled=true`, survived a
fresh iframe, and the quick action read the saved value. A trusted synthetic
OpenVR pointer, slider value 50, CDP text entry after focus and changed stereo
compositor hashes passed. The mirror capture SHA-256 was
`4f0d977fab7c144a3490b12ebefddacf3126db585057eb022006ffdf844a7498`.
The command exited 0 with `passed=true`; original power settings (`false` and
`5.000000`) were read back, battery ended at 99%, the recovery timer was
removed, cleanup errors were empty and only its own lock was released. The
private full report is `/tmp/framey-web-browser-vr-d5e62f6/result.json` on the
test host. This verifies the sample-panel regression path, not the official
Web Browser plugin or a worn headset. Synthetic native scroll still did not
move.
+62
View File
@@ -0,0 +1,62 @@
# v0.4.42 tagged release, 26 October 2026
The annotated `v0.4.42` tag dereferences to main merge commit
`98909cb54374a7bd7ec1f665f50eda20fa196f32` from PR #302. The app and
lockfile both report 0.4.42. The [tagged release workflow](https://github.com/saphid/frame-control/actions/runs/40276685171)
passed its draft, Linux, Windows, Apple Silicon Mac, Intel Mac and checksum jobs.
The Linux job launched its built AppImage under isolated Xvfb. CI does not prove
a physical Windows or Mac launch.
## Real Frame, exact tag
From an exact tag checkout, `scripts/frame-smoke.sh` acquired its own unique
owner in `/tmp/frame-test.lock` before reading the headset. Preflight found
SteamVR activity 3 (standby), proximity 6.25 and battery 100%. On Frame OS 0.5.6
build `20261022.6153909`, it recorded **12 passed / 1 skipped / 0 failed** and
exited 0. ARM64 and Proton test executables launched. The x86_64 title was
installed and removed, but its launch was skipped because optional Steam Linux
Runtime 4 was absent. Test title/process cleanup passed and only the run's lock
was released. The private result is
`tests/smoke/results/2026-10-26T055940Z-20261022.6153909.json` in the exact tag
checkout.
The separate [unworn Framey candidate exercise](v0442-candidate-2026-10-26.md)
passed real sample setting save/reopen, synthetic OpenVR pointer delivery and
stereo compositor change, with power restored and cleanup complete. It was run
on the release branch's code before the evidence-only final commit. No person
wore the Frame. Neither run verifies official plugin VR panels, physical
controller usability, native scrolling, VR keyboard or optical comfort. The
hourly worn check remains enabled.
## Installer, checksum and updater checks
The draft held ten installers plus `SHA256SUMS`, each with a GitHub SHA-256
digest. `scripts/publish-release.sh --dry-run v0.4.42` checked the exact remote
tag and every installer digest, regenerated the checksum list from those
metadata digests, and matched the uploaded `SHA256SUMS` digest. A fresh
checksum-list download independently matched
`e5c85f5a46d789dccafae5259bd00b9b048dd10ca4f2a6cdc52d1f432a6db989`.
A fresh Linux x86_64 AppImage download matched
`16fc9fbe448bccbe59aa4206bc171c6bfc741973d2b4da0a79652a5c11d837b3`.
`scripts/check-appimage-launch.py` launched those exact bytes under isolated
Xvfb: the bundled server answered HTTP 200 with `FrameControl/1` and then
stopped. Fresh Mac ZIP downloads matched GitHub digests: arm64
`394d6088e732a19289b7b92366dd247f14f7293a345d57ed5c4e9c83a50abad3`
and x64 `66d4606e38210d02af59dc87e4d1216282ad9e968056d7096f247fcc51e72395`.
Both bundles report version 0.4.42; their main Mach-O CPU types are arm64 and
x86_64 respectively. Neither tagged Mac ZIP was launched on a physical Mac.
`scripts/publish-release.sh v0.4.42` published
[v0.4.42](https://github.com/saphid/frame-control/releases/tag/v0.4.42) at
2026-10-26 06:02:20 UTC. GitHub's latest-release API read back `draft=false`,
`prerelease=false`, the expected tag, ten installers, `SHA256SUMS` and
`update.json`. A fresh public latest-download of `update.json` parsed as
version 0.4.42 with the stable release page and ten SHA-256 installer digests.
The tag contains #299, #300, #301 and #303 through #302's release merge.
#304 merged after the tag and is not in this release.
The exact-version official Decky table for v0.4.42 is **3 works / 48 partial /
54 broken / 11 N/A**. The two additional works labels are restricted to tested
per-app silent-stream volume control. Audible playback and official-plugin
worn VR remain unverified.
@@ -0,0 +1,7 @@
# v0.4.43 candidate Frame gate, 26 October 2026
The combined code head `232b253e7a9400aa9829f9fea6a5d2011dd2a8f3` (PR #309 after merging main with PR #308) ran `scripts/frame-smoke.sh` under its own `/tmp/frame-test.lock` owner at 07:57 UTC. Its locked preflight found SteamVR activity 3 (standby), proximity 6.25 and battery 99%. On Frame OS 0.5.6 build `20261022.6153909`, it recorded **12 passed / 1 skipped / 0 failed**. The ARM64 and Proton executables launched; x86_64 launch was skipped because Steam Linux Runtime 4 is absent. It removed its test titles, exited 0 and released its lock. The private detailed result is `/tmp/frame-smoke-pr309/2026-10-26T075752Z-20261022.6153909.json` on the test host. A fresh, separately owned read-only lock then found activity 3 and battery 99%, and was released. PR #310 subsequently changed only the Windows wireless CI smoke test; the release branch merged that test change and bumped the app version, with no further headset runtime code change after this Frame smoke.
PR #308's panel code commit `d5e62f643d462b7070f27c2c47b11b4aa9982094` previously passed the temporary Framey sample `scripts/framey-vr-check.py --exercise` machine check, with cleanup and power restoration; see [Web Browser follow-up](framey-web-browser-2026-10-26.md). That test used synthetic OpenVR input and a repository sample, not the official Web Browser plugin or a person wearing the headset. No wake or VR input exercise ran on this combined code head because wearer confirmation was absent. The current smoke made no worn visual, physical-controller, audible-output or official-plugin VR claim.
This is candidate evidence, not a published-release result. Publication still needs all exact-head PR and release checks, ten tagged installers with matching `SHA256SUMS`, package validation, the publisher dry run, and public updater readback. The exact-version official Decky catalog in this candidate is **4 works / 49 partial / 52 broken / 11 N/A**. Volume Boost's works claim covers tested muted sink controls; Junk-Store remains partial, and Web Browser remains broken.
+79
View File
@@ -0,0 +1,79 @@
# v0.4.43 tagged release, 26 October 2026
The annotated `v0.4.43` tag dereferences to main merge commit
`b4e632b218eed0761db488061e3fe6c503b06a0a` from PR #311. Both the app
and lockfile report 0.4.43. The [tagged release workflow](https://github.com/saphid/frame-control/actions/runs/40292211650)
passed its draft, Linux, Windows, Apple Silicon Mac, Intel Mac and checksum jobs.
The [tagged main checks](https://github.com/saphid/frame-control/actions/runs/40292128084)
passed all seven jobs, including iOS and fake-Frame end-to-end tests. The Linux
builder launched its AppImage under isolated Xvfb; that does not prove a physical
display, FUSE mount or headset connection.
## Frame and package gates
The [combined-head candidate](v0443-candidate-2026-10-26.md) passed the real
Frame smoke before tagging: 12 passed, one optional x86 launch skipped, zero
failed. The only later changes before the tag were the Windows wireless CI
test and version metadata. Its locked preflight found SteamVR activity 3
(standby), proximity 6.25 and battery 99%.
The draft had ten installers plus `SHA256SUMS`. Each installer had a GitHub
SHA-256 digest. A fresh downloaded checksum list contained exactly those ten
names and digests; its own downloaded bytes matched GitHub's digest
`57f211d8bbf76db8aa96cc4a5ae31410ed7e3075ce2e8c0c17394feb9426d01c`.
`scripts/publish-release.sh --dry-run v0.4.43` passed the exact remote tag,
installer and checksum gates after the notes were attached.
Freshly downloaded packages passed these independent checks:
| Package | Result |
| --- | --- |
| Linux x86_64 AppImage | SHA-256 `d5e6c2272a14a2aa77aa932db43f3ce887e09eb78c87bf2de410832b5df4827d`; launched under isolated Xvfb, bundled server returned HTTP 200 with `FrameControl/1`, and stopped on exit. |
| Mac arm64 ZIP | SHA-256 `c6c4a9c74644c3479d1af112ec60d4c2770201fc366f21bca23a4e2fcc86194a`; ZIP CRC passed, bundle version 0.4.43, main Mach-O CPU arm64. |
| Mac x64 ZIP | SHA-256 `58dfc88bb71f1733add7477f9cd888417e4827e608fe910017837ba466a72a40`; ZIP CRC passed, bundle version 0.4.43, main Mach-O CPU x86_64. |
| Windows x64 Setup | SHA-256 `8c17522b0e96d54f53949f4df56a3e1ec1fe2da8df595482a358a3d014b6f1a9`; PE/Nullsoft installer format. The tagged Windows CI also installed and checked its packaged files and Python libraries. |
| Linux arm64 DEB | SHA-256 `c00f141b6b63344c629bbd659a366a74705c52061d83572ae235a75ac6618df0`; package version 0.4.43 and architecture arm64. |
Tagged Mac and Windows packages were not launched on physical machines in this
release gate. Earlier Intel Mac acceptance covers an older version, not these
tagged bytes.
## Publication and later main
`scripts/publish-release.sh v0.4.43` published
[v0.4.43](https://github.com/saphid/frame-control/releases/tag/v0.4.43) at
2026-10-26 08:39:09 UTC. GitHub's latest-release API returned `draft=false`,
`prerelease=false`, the expected tag, ten installers, `SHA256SUMS` and
`update.json`. A fresh public latest-download of `update.json` parsed as
version 0.4.43 with the stable release page, ten hashed assets and the worn-VR
limits in its notes. The release catalog is **4 works / 49 partial / 52 broken /
11 N/A**. Volume Boost's works label covers tested muted sink controls only.
PRs #312 and #313 merged **after** the tag and are absent from v0.4.43. On their
combined main commit `c50f96510fdb42a2aee227436a04c0cfe8817919`, a second
locked `scripts/frame-smoke.sh` run at 08:37 UTC found activity 3, proximity 6.25
and battery 99%, then passed 12 checks with the same optional x86 launch skip
and zero failures. Test title and process cleanup passed; the command exited 0
and released only its owned lock. The private result is
`tests/smoke/results/2026-10-26T083719Z-20261022.6153909.json` in the test
checkout. This post-tag smoke does not test the new Framey scroll buttons or
Junk-Store's deployed headset panel.
The guarded `scripts/framey-vr-check.py --exercise` then ran on the same code
commit, using the existing direct `frame` SSH alias and its own shared-lock owner.
It began at 08:45 UTC in SteamVR standby (activity 3, proximity 6.3125,
battery 99%). The temporary sample panel passed setting save/reopen, quick
action, slider and text checks. Synthetic native OpenVR clicks on the visible
**Scroll plugin down/up** buttons moved the 160-pixel test region 0 → 120 → 0;
the compositor screenshot hash changed. Native synthetic wheel attempts still
produced no wheel event or movement. The private report is
`/tmp/framey-scroll-postmerge-direct-20261026/result.json` on the test host.
The command exited 0, removed its test services and power watchdog, read back
the original `false` / `5` power settings, found battery 99%, and released its
owned lock with no cleanup errors. It did not exercise an official plugin or a
physical controller.
No wearer confirmation was available, so no worn check, wake or input exercise
ran. Physical controller use, optical readability, official-plugin VR panels,
audible output and VR keyboard remain unverified. The hourly worn-check schedule
stays enabled.
@@ -0,0 +1,56 @@
# v0.4.44 candidate gate, 26 October 2026
This candidate starts from main commit
`c50f96510fdb42a2aee227436a04c0cfe8817919`, which contains PR #312's
Framey scroll buttons and PR #313's read-only installed-app metadata projection.
It also carries the documentation-only v0.4.43 release evidence from PR #314.
The app and lockfile are bumped to 0.4.44. No compatibility label changes:
the exact-version official Decky catalog remains **4 works / 49 partial /
52 broken / 11 N/A**. Junk-Store's About and Dependencies pages now render in
the local Chromium test with its actual ARM64 backend; the plugin remains partial.
## Real Frame checks on the combined code
At 08:37 UTC, `scripts/frame-smoke.sh` ran on that main commit under its own
`/tmp/frame-test.lock` owner. Locked preflight found SteamVR activity 3
(standby), proximity 6.25 and battery 99%. On Frame OS 0.5.6 build
`20261022.6153909`, ARM64 and Proton executables launched; the optional x86_64
launch was skipped because Steam Linux Runtime 4 is absent. The result was
**12 passed / 1 skipped / 0 failed**. It removed its test titles, exited 0 and
released its owned lock. The private result is
`tests/smoke/results/2026-10-26T083719Z-20261022.6153909.json` on the test
host.
At 08:45 UTC, `scripts/framey-vr-check.py --exercise` ran the same code under
a separate unique shared-lock owner, beginning in standby (activity 3,
proximity 6.3125, battery 99%). Synthetic native OpenVR clicks on the new
**Scroll plugin down/up** buttons moved a temporary sample region 0 → 120 → 0.
The sample setting save/reopen, quick action, slider, text and compositor-change
checks passed. Native synthetic wheel attempts still produced no event or
movement. Cleanup removed test services and its power watchdog, restored the
original `false` / `5` power values, read battery 99%, and released its lock
with no errors. The private report is
`/tmp/framey-scroll-postmerge-direct-20261026/result.json` on the test host.
This is an unworn sample panel check. It does not prove physical controller
scrolling, optical readability or an official-plugin panel in VR.
## Publication boundary
The version-bumped release branch also ran `scripts/frame-smoke.sh` after its
normal merge of PR #314's documentation. At 08:59 UTC, its locked preflight
found activity 3, proximity 6.3125 and battery 97%. The result was again
**12 passed / 1 skipped / 0 failed** on the same Frame OS build; ARM64 and
Proton launches passed, while the optional x86 launch was skipped for the
missing Steam Linux Runtime 4. Owned test title/process cleanup passed, the
command exited 0, and its lock was released. The private result is
`tests/smoke/results/2026-10-26T085951Z-20261022.6153909.json` on the test
host. This evidence-only paragraph is the sole change after that smoke.
PR #313's own actual ARM64 backend/library test is documented in
[installed-context evidence](framey-installed-context-2026-10-26.md). Its
dependency installation, login, game operations, RSS and optional websocket
remain unverified. The candidate still needs exact-head CI, tagged builds,
ten installer digests with `SHA256SUMS`, downloaded package checks, publisher
dry run and public updater readback before publication. A worn check requires
activity, proximity and reliable wearer confirmation; none was available, so
the hourly schedule stays enabled.
+58
View File
@@ -0,0 +1,58 @@
# v0.4.44 tagged release, 26 October 2026
The annotated `v0.4.44` tag dereferences to main merge commit
`37e25e62ace388ce032f47d08398c26ea8540fdb` from PR #315. The app and
lockfile both report 0.4.44. [Post-merge main checks](https://github.com/saphid/frame-control/actions/runs/40298400256)
passed all seven jobs, including iOS and fake Frame end-to-end tests. The
[tagged release workflow](https://github.com/saphid/frame-control/actions/runs/40299460805)
passed its draft, Linux, Windows, Apple Silicon Mac, Intel Mac and checksum
jobs. The tagged Linux build ran the AppImage under isolated Xvfb; this does
not verify a physical display or headset.
## Frame and package gates
The [candidate evidence](v0444-candidate-2026-10-26.md) records a shared-lock
real Frame smoke on the version-bumped candidate: **12 passed / 1 optional x86
launch skipped / 0 failed**, with owned cleanup and lock release. The x86
launch was skipped because Steam Linux Runtime 4 is absent. The candidate
also records an unworn sample panel exercise: synthetic OpenVR clicks moved a
temporary scroll region down and back, while a synthetic native wheel did
not scroll. No physical controller or wearer-visible result follows from it.
The final candidate edit was evidence only, and the merge commit added no
runtime changes.
The draft contained ten installers and `SHA256SUMS`. Every installer had a
GitHub SHA-256 digest. A freshly downloaded `SHA256SUMS` listed exactly those
ten names and matching digests; its downloaded bytes matched GitHub's digest
`072f8084d6f9f145f4a58457d10f54390b50d11288bece42f6d801a356a69b1c`.
After the release notes were attached, `scripts/publish-release.sh --dry-run
v0.4.44` passed the remote tag, ten installer and checksum gates.
Freshly downloaded packages passed these independent checks:
| Package | Result |
| --- | --- |
| Linux x86_64 AppImage | SHA-256 `d1cb5c3b590a44a9c61cf1d38d546cf89a835c73bece839c639f0d2be70ff139`; launched under isolated Xvfb, bundled server returned HTTP 200 with `FrameControl/1`, and stopped on exit. The first local attempt was blocked by the sandbox; the same downloaded bytes passed outside it. |
| Mac arm64 ZIP | SHA-256 `5a25dfea9b3c645dae7acaf660b7127e9a46a03173a69d02ef4c64b0b39a5934`; ZIP CRC valid, bundle version 0.4.44, main Mach-O CPU arm64. |
| Mac x64 ZIP | SHA-256 `fde1c48eef4e3b4be12a862d32c1154afd1a93fe1f27a32f0799365205655021`; ZIP CRC valid, bundle version 0.4.44, main Mach-O CPU x86_64. |
| Windows x64 Setup | SHA-256 `e8cae0a8d0f8b76a41ca109f5ec4bf9947ca31531a8b93ebb8e326e453602a16`; PE/Nullsoft installer format. The tagged Windows CI also checked its packaged files and Python libraries. |
| Linux arm64 DEB | SHA-256 `40f1704af89b5b824bc28e93328c516306aaefb2e0f441c1866197fc953eb21f`; package version 0.4.44 and architecture arm64. |
The tagged Mac and Windows packages were not launched on physical machines
in this release gate. Earlier Intel Mac acceptance covers older bytes.
## Publication and limits
`scripts/publish-release.sh v0.4.44` published
[v0.4.44](https://github.com/saphid/frame-control/releases/tag/v0.4.44) at
2026-10-26 09:38:50 UTC. GitHub's latest-release API returned `draft=false`,
`prerelease=false`, the expected tag, ten installers, `SHA256SUMS` and
`update.json`. A fresh public latest-download of `update.json` reported
version 0.4.44, the stable release page and ten hashed assets. Its notes
retain the worn-VR limits. The exact-version official Decky catalog remains
**4 works / 49 partial / 52 broken / 11 N/A** out of 116; Junk-Store remains
partial.
No reliable wearer confirmation was available. Official-plugin VR rendering,
physical controller use, optical readability, audible output and VR keyboard
remain unverified. The hourly shared-lock worn-check schedule stays enabled.
+4 -1
View File
@@ -63,8 +63,11 @@ none of these frontend controls has passed a worn-headset check yet.
| Official `@decky/rollup` JSX (`SP_JSX.jsx`, `jsxs`, `Fragment`) | Creates React elements in the isolated panel | Sample bundle smoke test; host unit test |
| `PanelSection`, rows, buttons, toggles, dropdowns | Local touch-sized controls; disabled controls stay inert during a backend call | Local browser and host unit test |
| `TextField`, `SliderField`, grouped `Dropdown` options | Native text, range and select controls in the panel; disabled state, numeric slider callback and basic input types | Host unit test; worn VR use unverified |
| Scrollable panel content | Visible Scroll plugin up/down buttons move the last region reached by a trusted pointer, or the document if that region disappears. Plugins may still use ordinary CSS overflow containers. | Parent/iframe message tests; physical controller scroll remains unverified |
| `Tabs`, `SidebarNavigation`, spinners, dialog/menu wrappers, progress bars | Local tab navigation and ordinary HTML controls; no Steam navigation or controller focus tree | Host tests and catalog frontend smoke checks in local Chromium |
| `SteamClient` subscriptions and `DFL.Router.MainRunningApp` | Explicit inert registration methods return disposable handles; no events or attached running game. Mutating Steam/power methods are unavailable. | Host contract tests; these are safe fallbacks, not live Steam functionality |
| `SteamUIStore` / `DFL.Router.RunningApps`, `MainRunningApp` | Bounded detached Steam CEF reads while the panel is open; refresh about every two seconds; no MobX observation. Failed reads clear stale context. | Locked real Frame empty-list reads; nonempty local Chromium fixtures. Real running-game selection unverified. |
| `GameSessions.RegisterForAppLifetimeNotifications` | Actual Steam callbacks, bounded queue and disposable `.unregister()` handle; no events inferred from snapshot changes; lease cleanup after lost panels | Real Frame registration/poll/close; launch/exit, retry, expiry and duplicate tests use explicit fixtures. Real game transitions unverified. |
| Other `SteamClient` subscriptions | Explicit inert registration methods return disposable handles; no fabricated power/input/game-action events. Mutating Steam/power methods are unavailable. | Host contract tests; safe fallbacks only |
| `DialogCheckbox`, `DialogButtonPrimary` | Native checkbox and button controls in the panel | Host unit test; Storage Cleaner empty-data browser check |
| Optional `settings`, `frameyQuickActions` | Separate settings tab when content also exists; settings-only plugins have no empty Panel tab; up to eight actions with result text | Host unit test; these fields are Framey additions |
| `serverAPI.call` / `callable` | Calls only that plugin's Decky Python backend via the headset service | Fake Frame e2e; real ARM64 Notebook save/reopen across backend restart and Simple Timer settings/recents persistence via local Chromium. No worn-VR claim. |
+10 -10
View File
@@ -1,10 +1,10 @@
# Official Decky store compatibility with Framey
Snapshot: 2026-10-25T23:19:48.854504+00:00; 116 current visible official plugins from https://plugins.deckbrew.xyz/plugins.
Snapshot: 2026-10-26T06:10:58.537029+00:00; 116 current visible official plugins from https://plugins.deckbrew.xyz/plugins.
This table and both store views use [`ui/framey-compatibility.json`](../ui/framey-compatibility.json). Results apply only to the exact store ID, version and SHA-256. A changed digest/version is **unverified**. Regenerate this document with `python3 scripts/framey-compatibility-doc.py`; use `--check` in review.
**Works: 3 · Partial: 48 · Broken: 54 · N/A: 11.**
**Works: 4 · Partial: 49 · Broken: 52 · N/A: 11.**
- **Works** requires verified useful plugin behavior with its real backend. Read the reason for the verified scope and remaining limits; it does not imply worn-VR verification.
- **Partial** means only a subset or a local fixture scope was verified, often just frontend/settings initialization. The remaining backend/device functionality is unverified; read the reason for the exact scope.
@@ -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. A [local audio follow-up](evidence/framey-audio-backends-2026-10-26.md) exercised Volume Mixer, Volume Boost and Mixer through real Decky 3.3.0 amd64 backends on private silent virtual audio targets. Sliders, numeric volume entry, independent server readbacks and Mixer filter persistence across a runtime restart passed. An [ARM64 audio follow-up](evidence/framey-audio-arm64-2026-10-26.md) verified the unmodified official backends in actual Decky 3.3.0 on the Frame user audio session. Volume Mixer and Mixer now qualify as works for per-app volume: Chromium controls changed only a named silent stream, with independent Pulse readbacks. Volume Boost stays partial: default-sink reads matched, but real sink writes were denied. Audible output, Bluetooth playback and worn VR remain unverified. A [library context follow-up](evidence/framey-library-context-2026-10-26.md) read 1,106 app overviews, 895 game IDs and the current public Steam ID from real Frame CEF under the shared lock. Local Chromium replayed this detached snapshot to render Deck Settings game selection, Steamback's empty save panel and PlayTime statistics/settings. Steamback and PlayTime used actual Decky 3.3.0 amd64 backends in an isolated container; their ARM64 backends and automatic game hooks remain unverified. Shotty's frontend now starts, but its published backend fails on missing `click`; it stays broken. A [chunked frontend follow-up](evidence/framey-deck-shelves-browser-2026-10-26.md) loaded Deck Shelves 2.0.0 through a managed JavaScript module route and rendered its settings in an opaque local Chromium panel. Its backend was denied; Steam Home shelves and worn VR remain unverified. A [local module lookup follow-up](evidence/framey-module-lookup-2026-10-26.md) supplies a finite registry of Framey-local controls and CSS. TabMaster QAM/docs now render; Steam library-tab editing remains unavailable. Junk-Store loading/error views render, but its real backend cannot execute the extracted startup script, so it stays broken. SteamGridDB still requires Steam context-menu internals and DeckSP still fails its published MobX fallback. TabMaster/Junk-Store ran in actual isolated amd64 Decky; no ARM64 or worn-VR result is claimed. Beyond the documented checks, 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).
After the VR job released the shared lock, this audit acquired it and used a private temporary Decky 3.3.0 ARM64 user service. Read-only ProtonDB `get_setting` and Steam Status `get_settings` calls succeeded through the live Steam CEF websocket. The service and private base were removed and the lock released; normal managed services stayed inactive, battery was 100 → 99, and no power setting was changed. [Redacted results](evidence/framey-official-backend-2026-10-25.json) record exactly what ran. A [2026-10-26 follow-up](evidence/framey-events-backend-2026-10-26.json) verified default Notebook drawing/save/reopen across a real backend restart and Simple Timer start/cancel, countdown/completion events, a visible toast and persisted recents/subtle mode. Only private test settings were written. Simple Timer remains partial because its own expired-timer startup raises a missing `timer_task` error; sound/suspend are unsupported. A [local audio follow-up](evidence/framey-audio-backends-2026-10-26.md) exercised Volume Mixer, Volume Boost and Mixer through real Decky 3.3.0 amd64 backends on private silent virtual audio targets. Sliders, numeric volume entry, independent server readbacks and Mixer filter persistence across a runtime restart passed. An [ARM64 audio follow-up](evidence/framey-audio-arm64-2026-10-26.md) verified the unmodified official backends in actual Decky 3.3.0 on the Frame user audio session. Volume Mixer and Mixer now qualify as works for per-app volume: Chromium controls changed only a named silent stream, with independent Pulse readbacks. That run left Volume Boost partial because sink writes were denied. A [Volume Boost follow-up](evidence/framey-volume-boost-arm64-2026-10-26.md) now verifies linked/separate channel controls, re-link and reopen through the actual ARM64 backend. Independent Pulse reads confirmed 125%/150% gain writes while the default sink stayed muted; exact original volume/mute was restored. Volume Boost qualifies as works for sink controls. Audible output, Bluetooth playback and worn VR remain unverified. A [running-context follow-up](evidence/framey-running-context-2026-10-26.md) supplies detached running-app/main-app reads and relays actual Steam lifetime callbacks while a panel is open. Real Frame checks verified an empty list, registration/cleanup and the exact Discord Status ARM64 backend reporting no IPC connection. Nonempty games and transitions were synthetic fixtures; real game events, Discord activity and worn VR remain unverified. Discord Status moves to partial; no new works claim is made. A [library context follow-up](evidence/framey-library-context-2026-10-26.md) read 1,106 app overviews, 895 game IDs and the current public Steam ID from real Frame CEF under the shared lock. Local Chromium replayed this detached snapshot to render Deck Settings game selection, Steamback's empty save panel and PlayTime statistics/settings. Steamback and PlayTime used actual Decky 3.3.0 amd64 backends in an isolated container; their ARM64 backends and automatic game hooks remain unverified. Shotty's frontend now starts, but its published backend fails on missing `click`; it stays broken. A [chunked frontend follow-up](evidence/framey-deck-shelves-browser-2026-10-26.md) loaded Deck Shelves 2.0.0 through a managed JavaScript module route and rendered its settings in an opaque local Chromium panel. Its backend was denied; Steam Home shelves and worn VR remain unverified. A [local module lookup follow-up](evidence/framey-module-lookup-2026-10-26.md) supplies a finite registry of Framey-local controls and CSS. TabMaster QAM/docs now render; Steam library-tab editing remains unavailable. That pass exposed Junk-Store executable-script extraction failures; the follow-up below repairs them. SteamGridDB still requires Steam context-menu internals and DeckSP still fails its published MobX fallback. That pass tested TabMaster/Junk-Store in actual isolated amd64 Decky, without ARM64 or worn-VR coverage. A [script entrypoint follow-up](evidence/framey-script-entrypoints-2026-10-26.md) grants owner execute to shebang scripts and ZIP-declared executables without copying archive privilege/write bits. Junk-Store now passes static menu/backend calls in actual Decky 3.3.0 ARM64 and local Chromium Games/Epic navigation, back and reopen. It moves to partial: About still crashes on missing installed-app metadata, Legendary is absent, and login/game operations remain unverified. Existing installations require update/reinstall to repair permissions. A [read-only installed-metadata follow-up](evidence/framey-installed-context-2026-10-26.md) now repairs the About crash. The library snapshot copies only Steam-supplied installed booleans into detached metadata, preserving unknown values as undefined. Junk-Store About/Dependencies, Back and reopen render in local Chromium with actual Frame library data and the official ARM64 backend. It remains partial: dependency/login/game operations, RSS, the optional Line truncated
## Every current plugin
@@ -40,11 +40,11 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 10: vibrantDeck 2.0.1 | legacy / legacy | N/A | Not supported on Frame: Changes X11 gamescope color properties, not the Frame SteamVR compositor. | static archive, local browser, fake-Frame |
| 11: Fantastic 0.5.1 | legacy / legacy | N/A | Not supported on Frame: Targets Steam Deck fan hardware; no Frame fan-control mapping is provided. | static archive, local browser, fake-Frame |
| 12: MusicControl 1.1.6 | legacy / legacy | partial | Legacy media controls initialize; player discovery and playback are unverified. | static archive, local browser, fake-Frame |
| 13: Pause Games 1.0.3 | 1 / 2 | partial | Settings render locally with inert system-key subscriptions and empty detached Steam context. Live-game discovery, process pause/resume and suspend integration are unsupported/unverified; no backend actions ran. | static archive, local browser, fake-Frame |
| 13: Pause Games 1.0.3 | 1 / 2 | partial | Settings and running-game list initialize in local Chromium with an explicit synthetic game; live Frame empty-list reads and lifetime registration/cleanup passed. Process pause/resume, automatic pausing and suspend integration remain unverified/unsupported; backend writes were denied. | static archive, local browser, fake-Frame |
| 14: ProtonDB Badges 1.3.4 | 2 / 2 | partial | Settings render locally and read-only get_setting RPC returned the default through real Decky 3.3.0 on ARM64 Frame. Steam game-page badges have no patch target; live writes and VR controls unverified. | static archive, local browser, fake-Frame, real Frame SSH |
| 16: Audio Loader 1.6.1 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. Steam UI sound lookup is present as explicitly unsupported; the frontend still requires Steam m_GamepadUIAudioStore.m_AudioPlaybackManager, which is unavailable in the isolated panel. | static archive, local browser, fake-Frame |
| 18: Bluetooth 3.1.1 | 1 / 2 | partial | Controls render with fixture replies; actual Bluetooth discovery, labels and connections remain unverified. | static archive, local browser, fake-Frame |
| 19: Notebook 0.1.5 | legacy / legacy | works | Default notebook drawing, save and reopen verified in Chromium with real Decky 3.3.0 ARM64, including backend restart. Per-game auto-selection and background artwork unavailable; worn-VR input unverified. | static archive, local browser, fake-Frame, real Frame SSH |
| 19: Notebook 0.1.5 | legacy / legacy | works | Default notebook drawing, save and reopen verified in Chromium with real Decky 3.3.0 ARM64, including backend restart. Running-game context is now supplied; per-game selection was attempted with a synthetic game and backend calls denied. Real per-game selection, background artwork and worn-VR input remain unverified. | static archive, local browser, fake-Frame, real Frame SSH |
| 21: PowerTools 2.0.3 | legacy / legacy | N/A | Not supported on Frame: Targets AMD Ryzen power controls; Frame uses an ARM64 SoC. | static archive, local browser, fake-Frame |
| 22: Memory Deck 0.1.8 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: memory-deck/bin/libscanmem.so.1.0.0. | static archive, local browser, fake-Frame |
| 23: Animation Changer 1.3.2 | legacy / legacy | partial | Initializer succeeds; forced backend failure prevents loading animations. Boot-video changes untested. | static archive, local browser, fake-Frame |
@@ -56,7 +56,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 32: AutoFlatpaks 1.6.8 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
| 35: MoonDeck 1.13.0 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: moondeck/python/externals/yarl/_quoting_c.cpython-313-x86_64-linux-gnu.so. Frontend initializes; native CPython wheels and executables in the published archive are x86_64. | static archive, local browser, fake-Frame |
| 36: SteamGridDB 1.8.1 | 1 / 2 | broken | Local module mapping is available; frontend still requires the Steam LibraryContextMenu implementation, which has no target in the isolated Framey panel. | static archive, local browser, fake-Frame |
| 37: Discord Status 1.5.0 | 1 / 2 | broken | Detached app overviews are now available, but RunningApps/lifetime observations and Steam artwork helpers still lack a live mapping. Discord activity integration remains unavailable/unverified. | static archive, local browser, fake-Frame, real Frame SSH |
| 37: Discord Status 1.5.0 | 1 / 2 | partial | Disconnected panel renders; running-activity construction accepts a synthetic game in local Chromium. Exact official is_connected returned false in real Decky 3.3.0 ARM64. Empty live running-list reads and lifetime registration/cleanup passed. Real game transitions, Discord shortcut detection/connection/activity, local custom artwork and worn VR remain unverified/unsupported. | static archive, local browser, fake-Frame, real Frame SSH |
| 38: Volume Mixer 1.2.1-1 | legacy / legacy | works | Per-app stream discovery and slider/keyboard volume changes pass in local Chromium through real Decky 3.3.0 ARM64 on the Frame audio session, with independent Pulse readback of a silent test stream. Bluetooth playback, audible output and worn-VR controls unverified. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
| 40: Controller Tools 2.0.2 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: ControllerTools/bin/backend. Frontend audit failed: Audit blocks WebSocket. | static archive, local browser, fake-Frame |
| 43: Tailscale Control 0.1.3 | legacy / legacy | partial | Settings render locally after the SuspensefulImage fallback. Tailscale daemon/state, connection and exit-node changes unverified; bundled localhost QR asset remains unavailable. No network settings were changed. | static archive, local browser, fake-Frame |
@@ -68,15 +68,15 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 52: Game Theme Music 1.7.1-1 | 1 / 2 | broken | Frontend audit failed: Cannot read properties of null (reading 'type'). | static archive, local browser, fake-Frame |
| 53: Shotty 0.1.0 | legacy / legacy | broken | Library snapshot fixes the frontend allApps crash. Actual published Decky 3.3.0 amd64 backend fails importing click.get_app_dir (ModuleNotFoundError); the archive does not bundle click. Screenshot aggregation/rescue was not run. | static archive, local browser, fake-Frame, real Frame SSH |
| 56: MangoPeel 0.0.7 | 1 / 2 | partial | Initializer succeeds; gamescope/MangoHud actions and backend are unverified. | static archive, local browser, fake-Frame |
| 58: Volume Boost 0.0.7 | legacy / legacy | partial | Real Decky 3.3.0 ARM64 default-sink reads match independent Frame pactl channel volumes; controls render locally. Sliders/numeric writes passed on an isolated amd64 virtual sink only. Real default-sink writes, boost above 100%, audible output and worn VR unverified. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
| 59: Web Browser 1.4.3 | legacy / legacy | broken | Frontend audit failed: deckyFrontendLib.getReactRoot is not a function. | static archive, local browser, fake-Frame |
| 58: Volume Boost 0.0.7 | legacy / legacy | works | Default-sink controls pass in local Chromium through real Decky 3.3.0 ARM64: linked slider/keyboard and numeric writes, separate channels, re-link and reopen, with independent Pulse readback including 125%/150% while muted. Exact original volume/mute restored. Audible output, Bluetooth sinks and worn VR unverified. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
| 59: Web Browser 1.4.3 | legacy / legacy | broken | The exact frontend now initializes with a null Steam React-root lookup. The browser itself still requires Steam GamepadUI CDP injection and a backend WebSocket unavailable in Framey; no working browser tab was verified. | static archive, local browser, fake-Frame, focused local browser |
| 60: DeckyFileServer 1.2.0 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: DeckyFileServer/bin/backend. | static archive, local browser, fake-Frame |
| 61: TabMaster 2.15.1 | 1 / 2 | partial | Local Chromium rendered the QAM and navigable Overview/Filters documentation with an actual Decky 3.3.0 amd64 backend. Library-tab editing remains unavailable: Steam login/library patches and MobX reactions have no target. ARM64 and worn VR unverified. | static archive, local browser, fake-Frame |
| 62: TS3 QuickAccess 1.2.0 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: ts3-qs4sd/bin/ts3-qs4sd.so. | static archive, local browser, fake-Frame |
| 63: Free Loader 1.6.0 | 1 / 2 | broken | Unsafe plugin archive member. | static archive, local browser, fake-Frame |
| 64: Syncthing 0.2.7 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: decky-syncthing/bin/decky-syncthing-watchdog. | static archive, local browser, fake-Frame |
| 65: Decky Terminal 0.5.3 | 1 / 2 | partial | Local routes initialize after parameter-route support; no shell commands were run. | static archive, local browser, fake-Frame |
| 66: PlayTime 3.2.5 | 1 / 2 | partial | Statistics and routed settings render with a detached real Frame library/user snapshot and actual Decky 3.3.0 amd64 backend in isolation. User selection and empty statistics reads pass. ARM64 backend, live MobX/running-game observations and automatic recording remain unverified/unsupported. | static archive, local browser, fake-Frame, real Frame SSH |
| 66: PlayTime 3.2.5 | 1 / 2 | partial | Statistics and routed settings render with a detached real Frame library/user snapshot and actual Decky 3.3.0 amd64 backend in isolation. User selection and empty statistics reads pass. Running snapshots are now exposed, but its private MobX observation is unsupported; automatic recording and ARM64 backend remain unverified. | static archive, local browser, fake-Frame, real Frame SSH |
| 67: Battery Tracker 0.2.0 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
| 68: Wine Cellar 0.2.1 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-wine-cellar/bin/backend. Frontend audit failed: Audit blocks WebSocket. | static archive, local browser, fake-Frame |
| 70: MicroSDeck 0.11.2 | legacy / 2 | broken | Plugin contains a non-ARM64 Linux binary: MicroSDeck/bin/backend. | static archive, local browser, fake-Frame |
@@ -91,7 +91,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 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 | 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 | Local CSS lookup fixes the loading view crash. Actual Decky 3.3.0 amd64 startup/static InitActions return a script execution/JSON error; the published scripts are not executable after extraction. Store install/login/game launch and ARM64 remain unverified. | static archive, local browser, fake-Frame |
| 84: Junk-Store 1.1.13 | legacy / legacy | partial | Official Decky 3.3.0 ARM64 static menu/backend calls pass; local Chromium Games/Epic navigation, About/Dependencies tabs, Back and reopen render with real Frame library metadata. Legendary is absent; dependency install, login, game operations, RSS, optional websocket and worn VR remain unverified. Existing installs need update/reinstall for script permissions. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
| 85: Decky-Undervolt 1.0.8 | 1 / 2 | N/A | Not supported on Frame: RyzenAdj Curve Optimizer requires an AMD CPU; Frame is ARM64. | static archive, local browser, fake-Frame |
| 87: ScreenSaver 1.1.2 | legacy / legacy | partial | Initializer succeeds; global screen overlay and idle/suspend integration have no verified VR target. | static archive, local browser, fake-Frame |
| 88: steamdeck-input-disabler 1.0.2 | legacy / legacy | N/A | Not supported on Frame: Targets the built-in Steam Deck USB gamepad; Frame has different controllers. | static archive, local browser, fake-Frame |
+43 -8
View File
@@ -16,6 +16,10 @@ plugins → Open official store**. Search the catalogue, confirm an install, and
choose **Open panel** from the installed plugin list. The panel contains the
plugin's own controls and settings. Update and Remove affect only plugin
folders that Frame Control's store manager installed.
Use **Scroll plugin up** and **Scroll plugin down** above an open panel when
wheel scrolling is unavailable. Point at a scrollable part of the plugin first
to move that section; otherwise the buttons move the panel document. Each
press moves about three quarters of the visible section and stops at its edge.
Removing a plugin keeps its Decky settings. Removing the Frame Control headset
panel or Decky backend keeps plugin folders until each is removed separately.
@@ -82,7 +86,11 @@ record the resulting TabMaster documentation support and remaining blockers.
The host currently supplies sections, rows, buttons, toggles, text fields,
sliders, grouped dropdowns,
scroll containers, a modal, locale information, local panel routes and toasts,
and backend calls for the plugin that owns the panel. Steam library/store page
and backend calls for the plugin that owns the panel. The headset's scroll
buttons send only a direction to the isolated iframe. They target the last
scrollable region reached by a trusted pointer move and fall back to the iframe
document when that region disappears. A result message reports whether content
moved; this bridge has no backend or Steam privilege. Steam library/store page
patches, global Steam UI components, file pickers, and privileged root methods
have no VR target. A plugin that depends on them can render only part of its UI
or show an incompatibility message. A Decky plugin's Python backend still runs as
@@ -115,9 +123,9 @@ Its cleanup actions were not tested; they may remove local game saves.
Framey also supplies local tabs/sidebar pages, spinners, dialog/menu wrappers
and progress bars. Its explicit SteamClient subscription fallbacks return
disposable handles without emitting fabricated events. There is no attached
running game in `DFL.Router`, and power, app launch and hardware mutation methods
are not supplied. Plugins that need live Steam stores still require integration
disposable handles without emitting fabricated events. Running-game reads and
lifetime notifications use the scoped bridge described below; power, app launch
and hardware mutation methods are not supplied. Other live Steam stores require integration
work. IndexedDB is hidden because the browser exposes it but denies access in
an opaque iframe; localForage can then select Framey's bounded localStorage.
Direct IndexedDB users remain unsupported.
@@ -125,13 +133,40 @@ Direct IndexedDB users remain unsupported.
`SuspensefulImage` renders a native image. SteamClient `Storage.GetJSON` and
`SetObject` use the same bounded per-plugin cache, including panel reopen
persistence; they do not read Steam's account/global JSON store. Missing keys
reject with `Not found`. Detached `SteamUIStore.RunningApps` is a frozen empty
list, and the suspend store has no methods. Optional Steam app/account globals
are undefined. These fallbacks let settings render without inventing live
reject with `Not found`. The suspend store has no methods. Optional Steam app/account globals
are undefined until a successful library read. These fallbacks let settings render without inventing live
games or account identity. [The exact-bundle follow-up](evidence/framey-ui-fallbacks-2026-10-26.json)
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.
MobX game observers for automatic tracking.
Panels referencing running-game APIs now receive a detached, read-only
`SteamUIStore.RunningApps` / `MainRunningApp` snapshot, also exposed through
`DFL.Router`. The authenticated headset-only `/api/framey/running` endpoint
checks installed store ownership; the parent checks the active panel session.
Poll and close requests also carry the panel's pinned SHA-256 receipt. A missing
or changed installed receipt is rejected before any Steam CEF request; a rejected
stale close relies on the existing finite subscription lease for cleanup.
Fixed bundled code runs in Steam CEF. It copies at most 64 running app overviews, a main app and
public Steam CDN capsule URLs; it never copies local artwork paths, launch
commands or Steam objects. The host refreshes about every two seconds without
overlapping requests. Failure clears stale context and sets
`SteamUIStore.FrameyRunningAvailable` to false.
`GameSessions.RegisterForAppLifetimeNotifications` relays actual Steam
`{unAppID, bRunning}` callbacks through a bounded 64-event queue. Snapshot
changes never fabricate launch/exit callbacks. Handles support `.unregister()`;
panel close/replacement removes the CEF subscription, with a 15-second lease
(checked every five seconds) if cleanup cannot reach Steam. At most four panel
sessions are retained. These are open-panel observations, not closed-panel
automation or a MobX implementation. Queue overflow can lose old callbacks.
`appStore` capsule helpers expose only projected public URLs for running apps;
unavailable or local custom artwork returns an empty value.
[Running-context evidence](evidence/framey-running-context-2026-10-26.md) separates
real Frame empty-list/subscription/cleanup reads, the exact Discord Status
ARM64 backend connection check, and synthetic nonempty/transition fixtures.
Real game transitions, Discord activity and worn-VR operation remain unverified.
Three parameterless SteamClient reads use Steam's actual CEF context:
`User.GetIPCountry`, `InstallFolder.GetInstallFolders` and `System.GetSystemInfo`.
+4
View File
@@ -59,6 +59,10 @@ pointer over it, then attempts OpenVR smooth and discrete scroll events. It
reports region geometry, trusted pointer events, wheel events and `scrollTop`.
No wheel event or offset change means this synthetic injection path remains
unverified; it does not establish whether a physical controller can scroll.
The exercise then clicks the visible **Scroll plugin down/up** buttons through
the same native OpenVR pointer transport and requires the test region to move
down and return to the top. This proves the button path only; it does not
identify a physical controller or repair native wheel delivery.
Temporary controls exercise the shipped
`DFL.SliderField` and `DFL.TextField`; their values remain UI-only. Text uses
CDP `Input.insertText` after OpenVR pointer focus. This is synthetic input,
+17 -5
View File
@@ -36,7 +36,7 @@ Ready means initializer completion, not useful plugin behavior. No plugin backen
<button id=run>Run catalog</button> <button id=download>Download observations</button><p id=progress></p>
<iframe id=panel sandbox="allow-scripts" style="width:1100px;height:740px"></iframe>
<script>
const plugins=CATALOG_DATA, librarySnapshot=LIBRARY_DATA;
const plugins=CATALOG_DATA, librarySnapshot=LIBRARY_DATA, runningSnapshot=RUNNING_DATA;
window.results=[];let active=null,running=false;
addEventListener('message',event=>{
if(event.source!==panel.contentWindow||!active)return;
@@ -45,6 +45,8 @@ 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:running')event.source.postMessage({type:'framey:reply',id:data.id,
...(runningSnapshot?{result:runningSnapshot}:{error:'Read-only compatibility audit: live running context blocked'})},'*');
if(data.type==='framey:steam')event.source.postMessage({type:'framey:reply',id:data.id,
...(data.method==='GetLibrarySnapshot'&&librarySnapshot?
{result:librarySnapshot}:{error:'Read-only compatibility audit: live Steam API blocked'})},'*');
@@ -53,13 +55,14 @@ addEventListener('message',event=>{
event.source.postMessage({type:'framey:reply',id:data.id,error:'Read-only compatibility audit: backend method blocked'},'*');}
if(data.type==='framey:cache:init')event.source.postMessage({type:'framey:cache:seed',session:data.session,entries:{}},'*');
});
window.runAudit=async(ids)=>{
window.runAudit=async(ids,settleMs=1200)=>{
if(!Number.isInteger(settleMs)||settleMs<1200||settleMs>10000)throw Error('Invalid audit settle time');
if(running)throw Error('Audit already running');running=true;run.disabled=true;
try{for(const plugin of plugins.filter(plugin=>!ids||ids.includes(plugin.id))){
active={id:plugin.id,name:plugin.name,ready:false,error:null,logs:[],methods:[],
library:librarySnapshot?'snapshot replay':'unavailable'};
library:librarySnapshot?'snapshot replay':'unavailable',running:runningSnapshot?'snapshot replay':'unavailable'};
progress.textContent=plugin.id+' '+plugin.name;panel.src='/panel/'+plugin.id;
await new Promise(resolve=>setTimeout(resolve,1200));results.push(active);active=null;
await new Promise(resolve=>setTimeout(resolve,settleMs));results.push(active);active=null;
panel.src='about:blank';await new Promise(resolve=>setTimeout(resolve,30));
}}finally{running=false;run.disabled=false;progress.textContent='Done: '+results.length;}
return results;
@@ -77,6 +80,8 @@ def main():
parser.add_argument('--port', type=int, default=18765)
parser.add_argument('--library-snapshot', type=Path,
help='Optional detached Steam library JSON; replay only, not live Steam/backend evidence')
parser.add_argument('--running-snapshot', type=Path,
help='Optional detached running-app JSON; replay only, not live Steam/backend evidence')
args = parser.parse_args()
plugins = json.loads(args.catalog.read_text())['plugins']
records = {p['id']: p for p in plugins}
@@ -86,6 +91,12 @@ def main():
if len(raw) > 2*1024*1024:
parser.error('library snapshot exceeds 2 MiB')
library = json.loads(raw)
running = None
if args.running_snapshot:
raw = args.running_snapshot.read_bytes()
if len(raw) > 256*1024:
parser.error('running snapshot exceeds 256 KiB')
running = json.loads(raw)
class Handler(BaseHTTPRequestHandler):
def log_message(self, *_):
@@ -99,7 +110,8 @@ def main():
# Only names/IDs are needed by the UI; never embed raw executable source.
catalog = json.dumps([{'id': p['id'], 'name': p['name']} for p in plugins]).replace('<', '\\u003c')
body = PAGE.replace('CATALOG_DATA', catalog).replace(
'LIBRARY_DATA', json.dumps(library).replace('<', '\\u003c')).encode()
'LIBRARY_DATA', json.dumps(library).replace('<', '\\u003c')).replace(
'RUNNING_DATA', json.dumps(running).replace('<', '\\u003c')).encode()
elif path.startswith('/panel/'):
try:
record = records[int(path.split('/')[-1])]
+4 -2
View File
@@ -54,7 +54,8 @@ def render(data):
'A [local audio follow-up](evidence/framey-audio-backends-2026-10-26.md) exercised Volume Mixer, Volume Boost and Mixer '
'through real Decky 3.3.0 amd64 backends on private silent virtual audio targets. Sliders, numeric volume entry, '
'independent server readbacks and Mixer filter persistence across a runtime restart passed. '
'An [ARM64 audio follow-up](evidence/framey-audio-arm64-2026-10-26.md) verified the unmodified official backends in actual Decky 3.3.0 on the Frame user audio session. Volume Mixer and Mixer now qualify as works for per-app volume: Chromium controls changed only a named silent stream, with independent Pulse readbacks. Volume Boost stays partial: default-sink reads matched, but real sink writes were denied. Audible output, Bluetooth playback and worn VR remain unverified. '
'An [ARM64 audio follow-up](evidence/framey-audio-arm64-2026-10-26.md) verified the unmodified official backends in actual Decky 3.3.0 on the Frame user audio session. Volume Mixer and Mixer now qualify as works for per-app volume: Chromium controls changed only a named silent stream, with independent Pulse readbacks. That run left Volume Boost partial because sink writes were denied. A [Volume Boost follow-up](evidence/framey-volume-boost-arm64-2026-10-26.md) now verifies linked/separate channel controls, re-link and reopen through the actual ARM64 backend. Independent Pulse reads confirmed 125%/150% gain writes while the default sink stayed muted; exact original volume/mute was restored. Volume Boost qualifies as works for sink controls. Audible output, Bluetooth playback and worn VR remain unverified. '
'A [running-context follow-up](evidence/framey-running-context-2026-10-26.md) supplies detached running-app/main-app reads and relays actual Steam lifetime callbacks while a panel is open. Real Frame checks verified an empty list, registration/cleanup and the exact Discord Status ARM64 backend reporting no IPC connection. Nonempty games and transitions were synthetic fixtures; real game events, Discord activity and worn VR remain unverified. Discord Status moves to partial; no new works claim is made. '
'A [library context follow-up](evidence/framey-library-context-2026-10-26.md) read 1,106 app overviews, 895 game IDs '
'and the current public Steam ID from real Frame CEF under the shared lock. Local Chromium replayed this detached snapshot '
'to render Deck Settings game selection, Steamback\'s empty save panel and PlayTime statistics/settings. '
@@ -63,7 +64,7 @@ def render(data):
'A [chunked frontend follow-up](evidence/framey-deck-shelves-browser-2026-10-26.md) loaded Deck Shelves 2.0.0 '
'through a managed JavaScript module route and rendered its settings in an opaque local Chromium panel. '
'Its backend was denied; Steam Home shelves and worn VR remain unverified. '
'A [local module lookup follow-up](evidence/framey-module-lookup-2026-10-26.md) supplies a finite registry of Framey-local controls and CSS. TabMaster QAM/docs now render; Steam library-tab editing remains unavailable. Junk-Store loading/error views render, but its real backend cannot execute the extracted startup script, so it stays broken. SteamGridDB still requires Steam context-menu internals and DeckSP still fails its published MobX fallback. TabMaster/Junk-Store ran in actual isolated amd64 Decky; no ARM64 or worn-VR result is claimed. Beyond the documented checks, official backends remain unexecuted. '
'A [local module lookup follow-up](evidence/framey-module-lookup-2026-10-26.md) supplies a finite registry of Framey-local controls and CSS. TabMaster QAM/docs now render; Steam library-tab editing remains unavailable. That pass exposed Junk-Store executable-script extraction failures; the follow-up below repairs them. SteamGridDB still requires Steam context-menu internals and DeckSP still fails its published MobX fallback. That pass tested TabMaster/Junk-Store in actual isolated amd64 Decky, without ARM64 or worn-VR coverage. A [script entrypoint follow-up](evidence/framey-script-entrypoints-2026-10-26.md) grants owner execute to shebang scripts and ZIP-declared executables without copying archive privilege/write bits. Junk-Store now passes static menu/backend calls in actual Decky 3.3.0 ARM64 and local Chromium Games/Epic navigation, back and reopen. It moves to partial: About still crashes on missing installed-app metadata, Legendary is absent, and login/game operations remain unverified. Existing installations require update/reinstall to repair permissions. A [read-only installed-metadata follow-up](evidence/framey-installed-context-2026-10-26.md) now repairs the About crash. The library snapshot copies only Steam-supplied installed booleans into detached metadata, preserving unknown values as undefined. Junk-Store About/Dependencies, Back and reopen render in local Chromium with actual Frame library data and the official ARM64 backend. It remains partial: dependency/login/game operations, RSS, the optional websocket and worn VR are unverified. Beyond the documented checks, 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 '
@@ -71,6 +72,7 @@ def render(data):
'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. '
'A [Web Browser frontend follow-up](evidence/framey-web-browser-2026-10-26.md) verified its exact archive now initializes with an inert null Steam React-root lookup. Its GamepadUI CDP/browser backend path remains unavailable, so it stays broken. '
'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 '
+24 -1
View File
@@ -71,10 +71,26 @@ def exercise(profile, plugin_id, folder):
if not hit:
raise RuntimeError('Native click hit another element: ' + str(panel.eval('({tag:__vrLastTarget?.tagName,text:__vrLastTarget?.textContent?.slice(0,100)})')))
def click_parent(selector):
parent.eval(f'document.querySelector({json.dumps(selector)}).scrollIntoView({{block:"center"}})')
time.sleep(.3)
pos = parent.eval(f'''(()=>{{let r=document.querySelector({json.dumps(selector)}).getBoundingClientRect();
return {{x:r.x+r.width/2,y:r.y+r.height/2,w:innerWidth,h:innerHeight}}}})()''')
sx, sy = vr.snapshot()['input']['scale']
x, y = pos['x'] * sx / pos['w'], sy - pos['y'] * sy / pos['h']
count = parent.eval('window.__vrParentClickCount')
vr.post_mouse(300, x, y)
vr.post_mouse(301, x, y, 1)
vr.post_mouse(302, x, y, 1)
wait(lambda:parent.eval('window.__vrParentClickCount'), lambda n:n > count, seconds=3)
if not parent.eval(f'!!window.__vrParentLastTarget?.closest({json.dumps(selector)})'):
raise RuntimeError('Native click missed parent control ' + selector)
try:
parent.eval(f'(async()=>{{await refreshDecky();openFrameyPanel({plugin_id},"Framey VR Check");return true}})()')
wait(lambda:parent.eval('byId("frameyPanelStatus").innerText'), lambda s:s == 'Plugin panel ready')
panel = connect_panel()
parent.eval('window.__vrParentClickCount=0;addEventListener("pointerup",e=>{window.__vrParentClickCount++;window.__vrParentLastTarget=e.target;});true')
initial = wait(text, lambda s:'Saved setting: off' in s or 'Could not read setting:' in s)
if 'Could not read setting:' in initial: raise RuntimeError(initial)
result['initialText'] = initial
@@ -149,6 +165,13 @@ def exercise(profile, plugin_id, folder):
result['scrollAfter'] = result['scrollAttempts'][-1]['offset']
result['scrollEvents'] = panel.eval('__vrWheelEvents')
result['nativeScrollMoved'] = result['scrollAfter'] != result['scrollBefore']
click_parent('#frameyScrollDown')
result['buttonScrollAfterDown'] = wait(
lambda:panel.eval('document.getElementById("vr-scroll-probe").scrollTop'), lambda n:n > 0)
click_parent('#frameyScrollUp')
result['buttonScrollAfterUp'] = wait(
lambda:panel.eval('document.getElementById("vr-scroll-probe").scrollTop'), lambda n:n == 0)
result['buttonScrollMoved'] = result['buttonScrollAfterDown'] > 0 and result['buttonScrollAfterUp'] == 0
panel.eval('document.getElementById("vr-scroll-probe").remove()')
panel.eval('document.getElementById("vr-controls").remove()')
# Scroll saved status into the visible iframe for compositor evidence.
@@ -156,7 +179,7 @@ def exercise(profile, plugin_id, folder):
result['finalOverlay'] = vr.snapshot()
result['passed'] = (result['trustedOverlayPointer'] and result['diskSetting'] == {'enabled':True}
and result['reopenedChecked'] and result['sliderValue'] != '10'
and result['textValue'] == 'framey-ci')
and result['textValue'] == 'framey-ci' and result['buttonScrollMoved'])
return result
except Exception as exc:
result['error'] = str(exc)
+7 -2
View File
@@ -10,8 +10,13 @@ ARG BASE=archlinux:base
FROM ${BASE}
RUN (pacman-key --init && pacman-key --populate) >/dev/null 2>&1; \
pacman -Syu --noconfirm --needed openssh rsync python nodejs curl iproute2 procps-ng util-linux \
&& pacman -Scc --noconfirm
attempts=0; \
until pacman -Syu --noconfirm --needed openssh rsync python nodejs curl iproute2 procps-ng util-linux; do \
attempts=$((attempts + 1)); \
[ "$attempts" -lt 5 ] || exit 1; \
sleep 2; \
done; \
pacman -Scc --noconfirm
# The Frame's user is steamos (docs/ssh.md). Its password stands in for the
# Developer Mode password.
+99 -2
View File
@@ -3,7 +3,7 @@ import {readFileSync} from 'node:fs';
import vm from 'node:vm';
const source=readFileSync(new URL('../../ui/framey-host.js',import.meta.url),'utf8');
const testSource=source.replace(/\n start\(\);\n\}\)\(\);\s*$/, '\n window.__surface=PluginSurface; window.__legacy=legacyServerAPI; window.__library=setLibrarySnapshot;\n})();');
const testSource=source.replace(/\n start\(\);\n\}\)\(\);\s*$/, '\n window.__surface=PluginSurface; window.__legacy=legacyServerAPI; window.__library=setLibrarySnapshot; window.__running=setRunningSnapshot; window.__refreshRunning=refreshRunning;\n})();');
assert.notEqual(testSource,source);
const states=[];
const React={Fragment:'fragment',Component:class {},createElement:(type,props,...children)=>({type,props:props||{},children}),
@@ -21,9 +21,13 @@ const window={React,ReactDOM:{createRoot:node=>{
const listeners=[];const messages=[];const parent={postMessage:message=>messages.push(message)};
const context=vm.createContext({window,document:{getElementById:id=>{
if(!elements.has(id))elements.set(id,new Element());return elements.get(id);},createElement:()=>new Element()},
navigator:{language:'en'}, parent,DOMException,addEventListener:(type,handler)=>listeners.push({type,handler}),
navigator:{language:'en'}, parent,DOMException,getComputedStyle:node=>({overflowY:node.overflowY||'visible'}),
addEventListener:(type,handler)=>listeners.push({type,handler}),
setTimeout:()=>1,clearTimeout(){}});
vm.runInContext(testSource,context);
// No Steam React tree exists inside the isolated Framey iframe.
assert.equal(window.DFL.getReactRoot({}),null);
assert.equal(window.DFL.findInReactTree(window.DFL.getReactRoot(null),()=>true),null);
// Official @decky/rollup bundles use the automatic JSX runtime, including keyed lists.
const jsx=window.SP_JSX.jsx('p',{id:'message',children:'Saved'},'message-key');
assert.equal(jsx.type,'p');
@@ -254,6 +258,7 @@ assert.equal(window.SteamClient.System.ShutdownPC,undefined);
assert.equal(window.DFL.Router.MainRunningApp,undefined);
assert.equal(window.DFL.Router.RunningApps.length,0);
assert.equal(window.DFL.Router.RunningApps,window.SteamUIStore.RunningApps);
assert.equal(window.SteamUIStore.FrameyRunningAvailable,false);
assert.ok(Object.isFrozen(window.SteamUIStore));
assert.ok(Object.isFrozen(window.SuspendResumeStore));
assert.equal(window.SuspendResumeStore.InitiateSleep,undefined);
@@ -261,6 +266,17 @@ for(const name of ['App','appStore','appInfoStore','collectionStore'])assert.equ
const snapshot={apps:[[10,'Game Ten','ten',1,'10',180,60,1234],
[20,'Shortcut','shortcut',1073741824,'85899345920',0,0,0]],gameIds:[10]};
window.__library(snapshot);
assert.equal(window.appStore.GetAppOverviewByAppID(10).local_per_client_data,undefined);
snapshot.apps[0].push(true);snapshot.apps[1].push(null);
window.__library(snapshot);
assert.equal(window.appStore.GetAppOverviewByAppID(10).local_per_client_data.installed,true);
assert.deepEqual(Object.keys(window.appStore.GetAppOverviewByAppID(10).local_per_client_data),['installed']);
assert.equal(window.appStore.GetAppOverviewByAppID(20).local_per_client_data.installed,undefined);
window.appStore.GetAppOverviewByAppID(10).local_per_client_data.installed=false;
assert.equal(snapshot.apps[0][8],true); // Detached from the wire data.
window.__library({...snapshot,apps:[snapshot.apps[0].slice(0,8).concat(false),snapshot.apps[1]]});
assert.equal(window.appStore.GetAppOverviewByAppID(10).local_per_client_data.installed,false);
window.__library(snapshot);
assert.equal(window.appStore.allApps.length,2);
assert.equal(window.appStore.GetAppOverviewByAppID('10').display_name,'Game Ten');
assert.equal(window.appStore.GetAppOverviewByGameID('85899345920').appid,20);
@@ -279,10 +295,52 @@ for(const invalid of [null,{apps:[],gameIds:[10]},
{...snapshot,apps:[snapshot.apps[0],snapshot.apps[0]]},
{...snapshot,apps:[[10,'Name','sort',1,'undefined',0,0,0]]},
{...snapshot,apps:[[10,'Name','sort',1,'10',-1,0,0]]},
...['true',1,{},undefined].map(installed=>({...snapshot,apps:[[10,'Name','sort',1,'10',0,0,0,installed]]})),
{apps:Array(10001).fill(snapshot.apps[0]),gameIds:[]}]){
assert.throws(()=>window.__library(invalid),/Invalid Steam/);
assert.equal(window.appStore.allApps[0].display_name,'Plugin-local patch');
}
const running={artwork:[[10,'https://shared.steamstatic.com/steam/apps/10/library.jpg',[]]],apps:[snapshot.apps[0].slice(0,8)],runningIds:[10],mainAppId:10,events:[],cursor:0,lifetimeAvailable:true};
const lifetimes=[];
const lifetimeHandle=window.SteamClient.GameSessions.RegisterForAppLifetimeNotifications(event=>{
lifetimes.push({...event,running:Array.from(window.DFL.Router.RunningApps,app=>app.appid)});
});
window.__running(running);await Promise.resolve();
assert.equal(lifetimes.length,0); // Initial snapshots are not launch events.
assert.equal(window.SteamUIStore.FrameyRunningAvailable,true);
assert.equal(window.DFL.Router.MainRunningApp.display_name,'Game Ten');
assert.equal(window.appStore.GetAppOverviewByAppID(10).local_per_client_data.installed,true);
assert.equal(window.appStore.GetVerticalCapsuleURLForApp({appid:10}),'https://shared.steamstatic.com/steam/apps/10/library.jpg');
assert.equal(window.appStore.GetCustomVerticalCapsuleURLs({appid:10}).length,0);
assert.equal(window.DFL.Router.RunningApps,window.SteamUIStore.RunningApps);
window.DFL.Router.MainRunningApp.display_name='Detached';
assert.equal(running.apps[0][1],'Game Ten');
const launch={...running,events:[{sequence:1,unAppID:10,bRunning:true}],cursor:1};
window.__running(launch);await Promise.resolve();
assert.equal(lifetimes.length,1);assert.deepEqual(lifetimes[0].running,[10]);
window.__running(launch);await Promise.resolve();assert.equal(lifetimes.length,1);
for(const invalid of [null,{...running,runningIds:[99]}, {...running,mainAppId:99},
{...running,runningIds:[10,10]}, {...running,artwork:[[10,'file:///private',[]]]}, {...running,cursor:-1},
{...running,events:[{sequence:1,unAppID:10,bRunning:'yes'}],cursor:1}]){
assert.throws(()=>window.__running(invalid),/Invalid Steam/);
assert.equal(window.DFL.Router.RunningApps.length,1);
}
const exited={...running,artwork:[],apps:[],runningIds:[],mainAppId:null,
events:[{sequence:2,unAppID:10,bRunning:false}],cursor:2};
window.__running(exited);await Promise.resolve();
assert.deepEqual(lifetimes[1].running,[]);assert.equal(lifetimes[1].bRunning,false);
lifetimeHandle.unregister();lifetimeHandle();
window.__running({...launch,cursor:3,events:[{sequence:3,unAppID:10,bRunning:true}]});
await Promise.resolve();assert.equal(lifetimes.length,2);
// A failed live read clears stale running context without inventing an exit.
const refresh=window.__refreshRunning();
const runningRequest=messages.at(-1);assert.equal(runningRequest.type,'framey:running');
listeners.find(l=>l.type==='message').handler({source:parent,data:{type:'framey:reply',id:runningRequest.id,error:'offline'}});
await refresh;
assert.equal(window.SteamUIStore.FrameyRunningAvailable,false);
assert.equal(window.DFL.Router.RunningApps.length,0);
assert.equal(window.DFL.Router.MainRunningApp,undefined);
assert.equal(lifetimes.length,2);
for(const register of [window.SteamClient.System.UI.RegisterForSystemKeyEvents,
window.SteamClient.Apps.RegisterForGameActionUserRequest]){
const dispose=register(()=>eventCount++);assert.equal(dispose.unregister,dispose);dispose();
@@ -330,6 +388,35 @@ assert.equal(nodes(window.__surface({plugin}),GameRoute).length,0);
// Event subscription is acknowledged before a method that immediately emits.
const onMessage=listeners.find(x=>x.type==='message').handler;
// The headset's two scroll buttons target the last trusted pointer's nested
// scroll region, with a document fallback after that region is removed.
const documentScroll={scrollTop:0,clientHeight:500,scrollHeight:900,isConnected:true,overflowY:'auto'};
const nestedScroll={scrollTop:0,clientHeight:160,scrollHeight:716,isConnected:true,
overflowY:'scroll',parentElement:documentScroll};
const child={scrollTop:0,clientHeight:0,scrollHeight:0,parentElement:nestedScroll};
context.document.scrollingElement=documentScroll;
const pointerMove=listeners.find(x=>x.type==='pointermove').handler;
pointerMove({isTrusted:true,target:child});
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'down'}});
assert.equal(nestedScroll.scrollTop,120);
assert.equal(messages.at(-1).moved,true);
pointerMove({isTrusted:false,target:documentScroll});
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'up'}});
assert.equal(nestedScroll.scrollTop,0,'Untrusted movement cannot retarget scroll');
nestedScroll.scrollTop=550;
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'down'}});
assert.equal(nestedScroll.scrollTop,556,'Scroll stops at the content end');
assert.equal(messages.at(-1).moved,true);
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'down'}});
assert.equal(messages.at(-1).moved,false);
const scrollReplies=messages.length;
onMessage({source:parent,data:{type:'framey:scroll',session:'stale',direction:'up'}});
onMessage({source:{},data:{type:'framey:scroll',session:'',direction:'up'}});
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'sideways'}});
assert.equal(messages.length,scrollReplies);
nestedScroll.isConnected=false;
onMessage({source:parent,data:{type:'framey:scroll',session:'',direction:'down'}});
assert.equal(documentScroll.scrollTop,375);
const eventValues=[];
const callback=(...args)=>eventValues.push(args);
assert.equal(api.addEventListener('timer:tick',callback),callback);
@@ -359,3 +446,13 @@ assert.throws(()=>api.addEventListener('bad event',callback),/Invalid Decky even
assert.throws(()=>api.addEventListener('tick',null),/Invalid Decky event/);
listeners.find(x=>x.type==='unload').handler();
assert.equal(messages.at(-1).type,'framey:events:close');
const finalRefresh=window.__refreshRunning();
assert.equal(window.__refreshRunning(),finalRefresh,'Running reads must not overlap');
const finalRequest=messages.at(-1);
listeners.find(x=>x.type==='pagehide').handler();
assert.equal(messages.at(-1).type,'framey:running:close');
onMessage({source:parent,data:{type:'framey:reply',id:finalRequest.id,result:launch}});
await finalRefresh;
assert.equal(window.SteamUIStore.FrameyRunningAvailable,false,'Closed panels ignore late snapshots');
const messageCount=messages.length;
await window.__refreshRunning();assert.equal(messages.length,messageCount);
+42
View File
@@ -69,3 +69,45 @@ 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);
// Running reads are tied to the current iframe and session, with no method path.
session=vm.runInContext('frameySession',context);
const running={type:'framey:running',id:20,session,args:[0]};
const beforeRunning=requests.length;
for(const change of [{session:'old'},{args:[-1]},{args:[true]},{args:[0,'extra']}])
await send({...running,...change});
await send(running,{});assert.equal(requests.length,beforeRunning);
const pendingRunning=send(running);
assert.equal(requests.at(-1).path,'/api/framey/running');
assert.equal(requests.at(-1).body.active,true);
assert.equal(requests.at(-1).body.receipt,'a'.repeat(64));
const oldRunning=requests.at(-1);
vm.runInContext('openFrameyPanel(96,"Timer")',context);
assert.equal(requests.at(-1).body.active,false);
assert.equal(requests.at(-1).body.receipt,'a'.repeat(64));
assert.equal(requests.at(-1).body.session,session);
const beforeStaleReply=replies.length;
oldRunning.resolve({ok:true,json:async()=>({result:{apps:[],events:[],cursor:0}})});
await pendingRunning;assert.equal(replies.length,beforeStaleReply);
session=vm.runInContext('frameySession',context);
const finalRunning=send({...running,id:21,session});
requests.at(-1).resolve({ok:true,json:async()=>({result:{apps:[],events:[],cursor:0}})});
await finalRunning;
await send({type:'framey:running:close',session});
assert.equal(requests.at(-1).body.receipt,'a'.repeat(64));
assert.equal(requests.at(-1).body.active,false);
const scrollSession=vm.runInContext('frameySession',context);
el('frameyScrollDown').onclick();
assert.equal(replies.at(-1).type,'framey:scroll');
assert.equal(replies.at(-1).session,scrollSession);
assert.equal(replies.at(-1).direction,'down');
await send({type:'framey:scroll:result',session:'old',direction:'down',moved:true});
assert.equal(el('frameyPanelStatus').textContent,'Loading plugin panel…');
await send({type:'framey:scroll:result',session:scrollSession,direction:'down',moved:true});
assert.equal(el('frameyPanelStatus').textContent,'Scrolled plugin down');
await send({type:'framey:scroll:result',session:scrollSession,direction:'down',moved:false});
assert.equal(el('frameyPanelStatus').textContent,'No more plugin content in that direction');
el('frameyClose').onclick();
const replyCount=replies.length;
el('frameyScrollUp').onclick();
assert.equal(replies.length,replyCount);
+56
View File
@@ -0,0 +1,56 @@
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import vm from 'node:vm';
// Real Frame getter source and unregister-handle shape were probed over SSH.
// Nonempty rows and launch/exit notifications here are explicit test fixtures.
const source=readFileSync(new URL('../../ui/framey-running.js',import.meta.url),'utf8');
const listeners=new Set(),timers=new Map();let now=1000,nextTimer=0;
const api={RegisterForAppLifetimeNotifications(listener){
listeners.add(listener);return {unregister:()=>listeners.delete(listener)};
}};
const app={appid:10,display_name:'Test game',sort_as:'test',app_type:1,gameid:'10',
minutes_playtime_forever:30,minutes_playtime_last_two_weeks:5,rt_last_time_played:123,
private_path:'PRIVATE',owner:'PRIVATE'};
const window={SteamClient:{GameSessions:api},SteamUIStore:{RunningApps:[app],MainRunningApp:app}};
window.appStore={GetVerticalCapsuleURLForApp:()=> 'https://shared.steamstatic.com/steam/apps/10/library.jpg?t=123',
GetCustomVerticalCapsuleURLs:()=>['file:///PRIVATE/path','https://example.com/PRIVATE','https://shared.steamstatic.com/steam/apps/10/custom.jpg']};
const context=vm.createContext({window,URL,Date:{now:()=>now},
setInterval:fn=>{timers.set(++nextTimer,fn);return nextTimer;},clearInterval:id=>timers.delete(id)});
const poll=(session='one',pluginId=13,active=true,cursor=0)=>
JSON.parse(JSON.stringify(vm.runInContext(source,context)({session,pluginId,active,cursor})));
const emit=event=>{for(const listener of listeners)listener(event);};
let data=poll();
assert.deepEqual(data.apps,[[10,'Test game','test',1,'10',30,5,123]]);
assert.deepEqual(data.artwork,[[10,'https://shared.steamstatic.com/steam/apps/10/library.jpg',['https://shared.steamstatic.com/steam/apps/10/custom.jpg']]]);
assert.deepEqual(data.runningIds,[10]);assert.equal(data.mainAppId,10);
assert.equal(data.lifetimeAvailable,true);assert.deepEqual(data.events,[]);
assert.equal(listeners.size,1);
// A nonempty snapshot does not invent a game launch notification.
assert.equal(data.cursor,0);assert.deepEqual(poll().events,[]);
emit({unAppID:10,bRunning:true,private_token:'PRIVATE'});
assert.deepEqual(poll().events,[{sequence:1,unAppID:10,bRunning:true}]);
assert.deepEqual(poll('one',13,true,1).events,[]);
assert.equal(poll().events.length,1); // Retry a dropped HTTP response.
assert.equal(listeners.size,1); // Polls never duplicate registration.
poll('two',37);assert.equal(listeners.size,2);
assert.throws(()=>poll('one',37),/another plugin/);
for(const event of [{unAppID:'10',bRunning:true},{unAppID:10,bRunning:1},
{unAppID:NaN,bRunning:true},{unAppID:-1,bRunning:false}])emit(event);
assert.equal(poll().cursor,1);
for(let n=0;n<100;n++)emit({unAppID:10,bRunning:!!(n%2)});
assert.equal(poll().events.length,64);
assert.equal(poll().cursor,101);
window.SteamUIStore.RunningApps=[];window.SteamUIStore.MainRunningApp=undefined;
assert.deepEqual(poll().runningIds,[]);assert.equal(poll().mainAppId,null);
assert.equal(poll().cursor,101); // Reading a closed game invents no exit event.
assert.equal(poll('one',13,false),null);assert.equal(listeners.size,1);
now+=16000;for(const timer of [...timers.values()])timer();
assert.equal(listeners.size,0);assert.equal(timers.size,0);
poll();assert.equal(listeners.size,1);
window.SteamClient.GameSessions={...api};poll();assert.equal(listeners.size,1);
poll('one',13,false);assert.equal(listeners.size,0);
window.SteamUIStore.RunningApps=Array(65).fill(app);
assert.throws(()=>poll(),/Too many/);assert.equal(listeners.size,0);
window.SteamUIStore=undefined;
assert.throws(()=>poll(),/unavailable/);assert.equal(listeners.size,0);
+25
View File
@@ -138,6 +138,31 @@ class StoreTransaction(unittest.TestCase):
self.assertNotEqual(result.returncode, 0)
self.assertTrue(external.exists())
def test_installed_script_entrypoints_execute_without_zip_exec_bits(self):
data, _ = archive(extra=('Example/scripts/start.py',
b'#!/bin/sh\nprintf "Framey script: %s\\n" "$1"\n'))
result = self.apply('install', data)
self.assertEqual(result.returncode, 0, result.stderr)
script = self.folder / 'scripts/start.py'
self.assertTrue(script.stat().st_mode & 0o100)
self.assertFalse(script.stat().st_mode & 0o6111 & ~0o100)
ran = subprocess.run([str(script), 'ready'], text=True, capture_output=True, timeout=5)
self.assertEqual(ran.returncode, 0, ran.stderr)
self.assertEqual(ran.stdout, 'Framey script: ready\n')
self.assertFalse((self.folder / 'plugin.json').stat().st_mode & 0o111)
self.assertFalse((self.folder / 'dist/index.js').stat().st_mode & 0o111)
def test_declared_executable_is_owner_only_without_privilege_bits(self):
info = zipfile.ZipInfo('Example/bin/entrypoint')
info.create_system = 3
info.external_attr = 0o104777 << 16
data, _ = archive(extra=(info, b'entrypoint payload'))
result = self.apply('install', data)
self.assertEqual(result.returncode, 0, result.stderr)
executable = self.folder / 'bin/entrypoint'
self.assertEqual(executable.stat().st_mode & 0o6111, 0o100)
self.assertEqual(executable.read_bytes(), b'entrypoint payload')
def test_remove_while_service_is_stopped_does_not_restart_it(self):
data, _ = archive()
self.assertEqual(self.apply('install', data).returncode, 0)
+61 -3
View File
@@ -22,6 +22,54 @@ RECEIPT = 'a' * 64
class Panel(unittest.TestCase):
def test_running_queue_and_host_contract(self):
result = subprocess.run(['node', str(Path(__file__).parent/'page/framey_running.mjs')],
capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
def test_running_bridge_is_installed_scoped_bounded_and_headset_only(self):
def fake_ssh(command, stdin, **_):
self.assertEqual(command, 'python3 -')
compile(stdin, '<running read>', 'exec')
self.assertIn('RegisterForAppLifetimeNotifications', stdin)
self.assertIn('262144', stdin)
self.assertNotIn('__FRAMEY_STEAM_', stdin)
return '{"result":{"apps":[],"events":[],"cursor":0}}'
with patch.object(framey_panel.frame_decky, 'status', return_value={'plugins':[{'storeId':13,'sha256':'a'*64}]}):
self.assertEqual(framey_panel.poll_running(fake_ssh,13,'test-1',True,receipt='a'*64)['result']['cursor'],0)
for plugin_id,session,active,cursor in [(True,'test',True,0),(0,'test',True,0),
(14,'test',True,0),(13,'bad session',True,0),(13,'test',1,0),
(13,'test',True,True),(13,'test',True,-1),(13,'test',True,2**53)]:
with self.subTest(args=(plugin_id,session,active,cursor)):
with self.assertRaises(framey_panel.PanelError):
framey_panel.poll_running(lambda *a,**k:self.fail('Unexpected SSH'),
plugin_id,session,active,cursor,receipt='a'*64)
with patch.object(server, 'HEADSET_RUNTIME', False), patch.dict(server.os.environ, {'FRAME_E2E_CONTAINER':'0'}):
with self.assertRaises(server.Failure):
server.framey_running_action({'id':13,'session':'test','active':True})
with patch.object(server, 'HEADSET_RUNTIME', True), patch.object(framey_panel, 'poll_running',return_value={'result':None}) as poll:
self.assertEqual(server.framey_running_action({'id':13,'session':'test','active':False,'receipt':'a'*64}),{'result':None})
poll.assert_called_once_with(server.ssh,13,'test',False,0,'a'*64)
def test_running_rejects_stale_or_missing_receipt_after_plugin_replacement(self):
installed = {'storeId':13,'sha256':'a'*64}
def safe_ssh(command, stdin, **_):
self.assertEqual(command,'python3 -')
compile(stdin,'<running read>','exec')
return '{"result":null}'
with patch.object(framey_panel.frame_decky,'status',side_effect=lambda _: {'plugins':[installed]}):
framey_panel.poll_running(safe_ssh,13,'old-panel',True,receipt='a'*64)
installed['sha256'] = 'b'*64 # Another manager replaces the installed plugin.
for active in (True,False):
for receipt in (None,'','bad','a'*64):
with self.subTest(active=active,receipt=receipt):
with self.assertRaisesRegex(framey_panel.PanelError,'receipt|changed'):
framey_panel.poll_running(lambda *a,**k:self.fail('Stale running request reached CEF'),
13,'old-panel',active,receipt=receipt)
# A reopened panel uses the new receipt for both polling and cleanup.
framey_panel.poll_running(safe_ssh,13,'new-panel',True,receipt='b'*64)
framey_panel.poll_running(safe_ssh,13,'new-panel',False,receipt='b'*64)
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','sha256':RECEIPT}]}):
@@ -64,12 +112,22 @@ class Panel(unittest.TestCase):
script = '''const assert=require('node:assert/strict');
const app={appid:10,display_name:'Game',sort_as:'game',app_type:1,gameid:'10',
minutes_playtime_forever:60,minutes_playtime_last_two_weeks:20,rt_last_time_played:123,
owner_account_id:'PRIVATE',per_client_data:{path:'PRIVATE'}};
owner_account_id:'PRIVATE',per_client_data:{path:'PRIVATE'},
local_per_client_data:{installed:true,clientid:'PRIVATE',install_path:'PRIVATE'}};
global.window=global;global.appStore={allApps:[app]};
global.App={m_CurrentUser:{strSteamID:'76561198000000001',strAccountBalance:'PRIVATE'}};
global.collectionStore={allGamesCollection:{allApps:[app]}};
const value=eval(EXPRESSION);
assert.deepEqual(value,{apps:[[10,'Game','game',1,'10',60,20,123]],gameIds:[10],steamId:'76561198000000001'});
assert.deepEqual(value,{apps:[[10,'Game','game',1,'10',60,20,123,true]],gameIds:[10],steamId:'76561198000000001'});
assert.equal(JSON.stringify(value).includes('PRIVATE'),false);
app.local_per_client_data.installed=false;
assert.equal(eval(EXPRESSION).apps[0][8],false);
delete app.local_per_client_data.installed;
assert.equal(eval(EXPRESSION).apps[0][8],null);
app.local_per_client_data=undefined;
assert.equal(eval(EXPRESSION).apps[0][8],null);
app.local_per_client_data={installed:'true'};
assert.equal(eval(EXPRESSION).apps[0][8],null);
appStore.allApps=Array(10001).fill(app);
assert.throws(()=>eval(EXPRESSION),/snapshot limit/);
appStore=undefined;
@@ -281,7 +339,7 @@ assert.throws(()=>eval(EXPRESSION),/library is unavailable/);
with tarfile.open(fileobj=io.BytesIO(gzip.decompress(packed))) as archive:
names = set(archive.getnames())
self.assertTrue({'ui/framey_panel.py','ui/framey-host.js',
'ui/framey-events.js',
'ui/framey-events.js','ui/framey-running.js',
'ui/vendor/react-18.3.1.min.js','ui/vendor/react-dom-18.3.1.min.js',
'ui/vendor/REACT-LICENSE.txt','ui/vendor/REACT-DOM-LICENSE.txt'} <= names)
+6
View File
@@ -192,6 +192,12 @@ class Probe(unittest.TestCase):
@unittest.skipUnless(wireless.frame_host.WINDOWS, 'Live read-only Windows inventory runs on Windows CI')
def test_live_windows_inventory(self):
r = wireless.diagnose()
# Shared Windows runners can spend the entire probe budget starting
# PowerShell. Retry only that transient timeout; a second failure must
# still fail this live inventory gate.
if (r['state'] == 'unavailable' and
r.get('message') == 'Windows wireless inventory timed out. No settings were changed.'):
r = wireless.diagnose()
self.assertIn(r['state'], ('ok', 'partial'), r)
self.assertEqual(r['sources']['adapters'], 'ok', r['sources'])
self.assertTrue(r['devices'], 'Windows runner should expose at least its virtual network interface')
+10
View File
@@ -177,6 +177,16 @@ try:
seen.add(f.filename)
stage.mkdir(mode=0o700)
z.extractall(stage)
# zipfile drops execute bits. Legacy plugins also publish shebang
# entrypoints as 0644 because Decky's installer chmods recursively.
# Grant only owner execute for scripts or declared executables;
# never restore archive privilege/write bits or mark other data executable.
for member in files:
if member.is_dir(): continue
entry=stage/member.filename
with entry.open('rb') as source: script=source.read(2)==b'#!'
if script or (member.external_attr>>16)&0o111:
entry.chmod(entry.stat().st_mode|0o100)
unpacked=stage/folder
if not unpacked.is_dir(): raise RuntimeError('Missing plugin folder')
# Recheck under the transaction lock. Another store operation may
+141 -13
View File
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"checkedAtUtc": "2026-10-25T23:19:48.854504+00:00",
"checkedAtUtc": "2026-10-26T06:10:58.537029+00:00",
"source": "https://plugins.deckbrew.xyz/plugins",
"hostCommit": "779f9c5",
"method": "Exact SHA-256 verified archives. All frontends attempted in opaque-iframe Chromium for 1.2 s with backend calls and network denied; ready means initializer only. Backend calls were denied in the baseline browser audit. Scoped live backend follow-ups are recorded separately in docs/framey-compatibility.md.",
@@ -580,7 +580,7 @@
"https://github.com/wynn1212/SDH-PauseGames.git"
],
"status": "partial",
"reason": "Settings render locally with inert system-key subscriptions and empty detached Steam context. Live-game discovery, process pause/resume and suspend integration are unsupported/unverified; no backend actions ran.",
"reason": "Settings and running-game list initialize in local Chromium with an explicit synthetic game; live Frame empty-list reads and lifetime registration/cleanup passed. Process pause/resume, automatic pausing and suspend integration remain unverified/unsupported; backend writes were denied.",
"blockInstall": false,
"verification": [
"static archive",
@@ -613,6 +613,11 @@
"sha256": "4aefdfb0ef25dea9c6d9fd037731c8b99ee8a1be966c2f7d5b0602112ef29f62",
"archiveValidation": "accepted",
"result": "installed-removed"
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
@@ -1006,7 +1011,7 @@
"https://github.com/popsUlfr/SDH-Notebook.git"
],
"status": "works",
"reason": "Default notebook drawing, save and reopen verified in Chromium with real Decky 3.3.0 ARM64, including backend restart. Per-game auto-selection and background artwork unavailable; worn-VR input unverified.",
"reason": "Default notebook drawing, save and reopen verified in Chromium with real Decky 3.3.0 ARM64, including backend restart. Running-game context is now supplied; per-game selection was attempted with a synthetic game and backend calls denied. Real per-game selection, background artwork and worn-VR input remain unverified.",
"blockInstall": false,
"verification": [
"static archive",
@@ -1080,6 +1085,11 @@
"existingNotesTouched": false
},
"method": "local Chromium + real Frame SSH; private actual Decky 3.3.0 ARM64 service"
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
@@ -2870,8 +2880,8 @@
"https://github.com/andrewburgess/steamdeck-discord-status",
"https://github.com/andrewburgess/steamdeck-discord-status.git"
],
"status": "broken",
"reason": "Detached app overviews are now available, but RunningApps/lifetime observations and Steam artwork helpers still lack a live mapping. Discord activity integration remains unavailable/unverified.",
"status": "partial",
"reason": "Disconnected panel renders; running-activity construction accepts a synthetic game in local Chromium. Exact official is_connected returned false in real Decky 3.3.0 ARM64. Empty live running-list reads and lifetime registration/cleanup passed. Real game transitions, Discord shortcut detection/connection/activity, local custom artwork and worn VR remain unverified/unsupported.",
"blockInstall": false,
"verification": [
"static archive",
@@ -2917,6 +2927,11 @@
"evidence": "docs/evidence/framey-library-context-2026-10-26.json",
"mode": "real Frame read-only CEF projection; local Chromium snapshot replay; scoped actual amd64 backends",
"frontendReady": true
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
@@ -3404,6 +3419,11 @@
"evidence": "docs/evidence/framey-library-context-2026-10-26.json",
"mode": "real Frame read-only CEF projection; local Chromium snapshot replay; scoped actual amd64 backends",
"frontendReady": true
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
@@ -3985,6 +4005,11 @@
"sha256": "71dc21ce5f9f4ee0573bd927e9b444d45dc47327888ca78ed063c7bcc57bfac1",
"archiveValidation": "accepted",
"result": "installed-removed"
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
@@ -4030,8 +4055,8 @@
"https://github.com/saumya-banthia/volume-boost",
"https://github.com/saumya-banthia/volume-boost.git"
],
"status": "partial",
"reason": "Real Decky 3.3.0 ARM64 default-sink reads match independent Frame pactl channel volumes; controls render locally. Sliders/numeric writes passed on an isolated amd64 virtual sink only. Real default-sink writes, boost above 100%, audible output and worn VR unverified.",
"status": "works",
"reason": "Default-sink controls pass in local Chromium through real Decky 3.3.0 ARM64: linked slider/keyboard and numeric writes, separate channels, re-link and reopen, with independent Pulse readback including 125%/150% while muted. Exact original volume/mute restored. Audible output, Bluetooth sinks and worn VR unverified.",
"blockInstall": false,
"verification": [
"static archive",
@@ -4159,12 +4184,13 @@
"https://github.com/jessebofill/DeckWebBrowser.git"
],
"status": "broken",
"reason": "Frontend audit failed: deckyFrontendLib.getReactRoot is not a function.",
"reason": "The exact frontend now initializes with a null Steam React-root lookup. The browser itself still requires Steam GamepadUI CDP injection and a backend WebSocket unavailable in Framey; no working browser tab was verified.",
"blockInstall": false,
"verification": [
"static archive",
"local browser",
"fake-Frame"
"fake-Frame",
"focused local browser"
],
"browser": {
"id": 59,
@@ -4216,6 +4242,17 @@
"sha256": "1192864fddc5d72550346b8dff02c21d71f0c40a06ac41c4e67bb6220d267f9d",
"archiveValidation": "accepted",
"result": "installed-removed"
},
"focusedBrowser": {
"hostChange": "DFL.getReactRoot returns null without exposing a Steam tree",
"ready": true,
"error": null,
"methods": [
"set_main_tab",
"load"
],
"backendAndNetwork": "denied",
"scope": "initializer only; no usable browser tab or real backend verified"
}
},
{
@@ -4447,6 +4484,11 @@
"sha256": "e08333859a32b0c177b8e9a7959c5eeeafe38f946578f7f1d161c721f0e05da5",
"archiveValidation": "accepted",
"result": "installed-removed"
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
@@ -4964,7 +5006,7 @@
"https://github.com/your-username/PlayTime"
],
"status": "partial",
"reason": "Statistics and routed settings render with a detached real Frame library/user snapshot and actual Decky 3.3.0 amd64 backend in isolation. User selection and empty statistics reads pass. ARM64 backend, live MobX/running-game observations and automatic recording remain unverified/unsupported.",
"reason": "Statistics and routed settings render with a detached real Frame library/user snapshot and actual Decky 3.3.0 amd64 backend in isolation. User selection and empty statistics reads pass. Running snapshots are now exposed, but its private MobX observation is unsupported; automatic recording and ARM64 backend remain unverified.",
"blockInstall": false,
"verification": [
"static archive",
@@ -5016,6 +5058,11 @@
"evidence": "docs/evidence/framey-library-context-2026-10-26.json",
"mode": "real Frame read-only CEF projection; local Chromium snapshot replay; scoped actual amd64 backends",
"frontendReady": true
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
@@ -6470,13 +6517,15 @@
"https://github.com/ebenbruyns/junkstore",
"https://github.com/ebenbruyns/junkstore.git"
],
"status": "broken",
"reason": "Local CSS lookup fixes the loading view crash. Actual Decky 3.3.0 amd64 startup/static InitActions return a script execution/JSON error; the published scripts are not executable after extraction. Store install/login/game launch and ARM64 remain unverified.",
"status": "partial",
"reason": "Official Decky 3.3.0 ARM64 static menu/backend calls pass; local Chromium Games/Epic navigation, About/Dependencies tabs, Back and reopen render with real Frame library metadata. Legendary is absent; dependency install, login, game operations, RSS, optional websocket and worn VR remain unverified. Existing installs need update/reinstall for script permissions.",
"blockInstall": false,
"verification": [
"static archive",
"local browser",
"fake-Frame"
"fake-Frame",
"isolated Linux backend",
"real Frame SSH"
],
"browser": {
"id": 84,
@@ -6508,6 +6557,30 @@
"sha256": "f7ad2a0103c85bf308929b2e96ec9aeeb0d76bcc6ae97eb1e4b5f31ffba0cbb1",
"archiveValidation": "accepted",
"result": "installed-removed"
},
"localBackend": {
"runtime": "Decky 3.3.0 amd64",
"evidence": "docs/evidence/framey-script-entrypoints-2026-10-26.json",
"before": "init and InitActions return inner Type:Error",
"after": "init Type:Success; InitActions Type:ActionSet/MainMenu"
},
"realFrame": {
"runtime": "Decky 3.3.0 ARM64",
"evidence": "docs/evidence/framey-script-entrypoints-2026-10-26.json",
"staticActions": [
"JunkStoreActions/GetContent",
"JunkStoreActions/GetEpicActions",
"MainMenu/GetContent",
"MainMenu/JunkStoreInit",
"init/InitActions"
],
"browser": "main menu; Games/Epic static tab; back; close/reopen",
"installedContext": {
"evidence": "docs/evidence/framey-installed-context-2026-10-26.json",
"field": "local_per_client_data.installed",
"browser": "About/Dependencies, Back, close/reopen; real library projection; static ARM64 backend calls",
"installedUnknown": "preserved as undefined; no fabricated false"
}
}
},
{
@@ -6920,6 +6993,11 @@
"sha256": "2d431a89826ed5cf3fd37258373eb3b46c841c39795aa8b25f83e59f4eee87c7",
"archiveValidation": "accepted",
"result": "installed-removed"
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
@@ -7289,6 +7367,11 @@
"evidence": "docs/evidence/framey-library-context-2026-10-26.json",
"mode": "real Frame read-only CEF projection; local Chromium snapshot replay; scoped actual amd64 backends",
"frontendReady": true
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
@@ -7719,6 +7802,11 @@
"sha256": "49309a02fa028d26c77230cc9f70fec9bcce91d59e887bfe8f9d3e42f8a19139",
"archiveValidation": "accepted",
"result": "installed-removed"
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
@@ -7906,6 +7994,11 @@
"sha256": "b6c514d147cc46bad5331ca9da36628eb9c330976e13735687499a4a9f17f6d4",
"archiveValidation": "accepted",
"result": "installed-removed"
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
@@ -8607,6 +8700,11 @@
"sha256": "8a69f45aadce763406b5c27bfef194761006d5e01c5e8841badeb13a27dd0d88",
"archiveValidation": "accepted",
"result": "installed-removed"
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
@@ -8716,6 +8814,11 @@
"sha256": "65e021ebb37d2ebfbfbe3dbc8af95aef9556cf484730362c6b26c4ecc3e62ccd",
"archiveValidation": "accepted",
"result": "installed-removed"
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": false
}
},
{
@@ -9710,6 +9813,11 @@
"sha256": "ec59fc8aceb4e1abe2066b3a0707ed6313cabc4421b7c17765397903e2f8fd26",
"archiveValidation": "accepted",
"result": "installed-removed"
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
@@ -10845,6 +10953,11 @@
"sha256": "17d4166a9e98979df5fdb07c7eae42317acd54a453d3b82757508f971193c0fe",
"archiveValidation": "accepted",
"result": "installed-removed"
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
@@ -11207,6 +11320,11 @@
"sha256": "3def99993c412b669933376b6420e44c68cd1fe9d9baf14b77a1f231242bd7e8",
"archiveValidation": "accepted",
"result": "installed-removed"
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
@@ -11691,6 +11809,11 @@
"sha256": "248294b5d6b9d5a5d635ce35f4b5185ce59d4e4fd59353e24df4b29d74c2ade4",
"archiveValidation": "accepted",
"result": "installed-removed"
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
@@ -13228,6 +13351,11 @@
"sha256": "33fb674993c9b2bd119f0c6118c465e379f166ec1539c8183fcf7580f1d89577",
"archiveValidation": "accepted",
"result": "installed-removed"
},
"runningFollowUp": {
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
"mode": "synthetic nonempty running/library replay; backend and network denied",
"frontendReady": true
}
},
{
+143 -18
View File
@@ -15,7 +15,14 @@
// user identity or suspend state in this isolated document.
for (const name of ['App','appStore','appInfoStore','collectionStore']) window[name] = undefined;
window.SuspendResumeStore = Object.freeze({});
window.SteamUIStore = Object.freeze({RunningApps:Object.freeze([]), MainRunningApp:undefined});
let runningApps = Object.freeze([]), mainRunningApp, runningAvailable = false;
let runningArtwork = new Map();
const runningListeners = new Set();
let runningCursor = 0, runningRequest = null, runningTimer = null, runningClosed = false;
window.SteamUIStore = Object.freeze({
get RunningApps() {return runningApps;}, get MainRunningApp() {return mainRunningApp;},
get FrameyRunningAvailable() {return runningAvailable;},
});
// Steam normally fills these token maps. Official plugins can safely use
// their own fallback strings when the isolated panel has no Steam tokens.
window.LocalizationManager = {m_rgLocalesToUse:[navigator.language || 'en'],
@@ -27,6 +34,25 @@
const root = document.getElementById('framey-root');
const error = document.getElementById('framey-error');
const toastRoot = document.getElementById('framey-toasts');
let lastScrollTarget = null;
function scrollTarget(element) {
for (let node = element; node && node !== document; node = node.parentElement) {
if (node.scrollHeight <= node.clientHeight + 1) continue;
if (node === document.scrollingElement || /^(auto|scroll|overlay)$/.test(getComputedStyle(node).overflowY))
return node;
}
return document.scrollingElement;
}
function scrollPlugin(direction) {
if (direction !== 'up' && direction !== 'down') return false;
const target = lastScrollTarget?.isConnected ? lastScrollTarget : document.scrollingElement;
if (!target) return false;
const before = target.scrollTop;
const maximum = Math.max(0, target.scrollHeight - target.clientHeight);
const step = Math.max(120, Math.round(target.clientHeight * 0.75));
target.scrollTop = Math.max(0, Math.min(maximum, before + (direction === 'down' ? step : -step)));
return target.scrollTop !== before;
}
const pending = new Map();
const routes = new Map();
const routeListeners = new Set();
@@ -132,6 +158,14 @@
return request(method, args, legacy);
}
addEventListener('message', event => {
if (event.source === parent && event.data?.type === 'framey:scroll' &&
event.data.session === cacheSession) {
if (event.data.direction !== 'up' && event.data.direction !== 'down') return;
const moved = scrollPlugin(event.data.direction);
parent.postMessage({type:'framey:scroll:result', session:cacheSession,
direction:event.data.direction, moved}, '*');
return;
}
if (event.source === parent && event.data?.type === 'framey:cache:seed' &&
event.data.session === cacheSession && acceptSeed) {
const entries = event.data.entries;
@@ -158,6 +192,9 @@
addEventListener('pointerup', event => {
if (event.isTrusted) parent.postMessage({type:'framey:input'}, '*');
}, {passive:true});
addEventListener('pointermove', event => {
if (event.isTrusted) lastScrollTarget = scrollTarget(event.target);
}, {passive:true});
const passthrough = tag => ({children, ...props}) => R.createElement(tag, props, children);
const Field = ({label, description, children}) => R.createElement('label', null,
@@ -359,7 +396,10 @@
showModal, showContextMenu:showModal,
// Steam app, library, and store patches have no target inside this panel.
afterPatch:noPatch, beforePatch:noPatch, replacePatch:noPatch, createReactTreePatcher:noPatch,
findInReactTree: () => null, findModule, findModuleChild, findModuleDetailsByExport,
// The isolated panel has no Steam React root. Legacy plugins can inspect
// this null result and keep their own controls without patching Steam UI.
getReactRoot: () => null, findInReactTree: () => null,
findModule, findModuleChild, findModuleDetailsByExport,
findModuleByExport:(filter, minExports)=>findModuleDetailsByExport(filter,minExports)[0],
findModuleExport:(filter, minExports)=>findModuleDetailsByExport(filter,minExports)[1],
createModuleMapping, findAllModules:filter=>[...createModuleMapping(filter).values()],
@@ -383,8 +423,8 @@
PlayAudioURL:()=>{throw Error('Steam UI sound playback is not supported in Framey');},
})}),
})}));
// No Steam game is attached to an isolated panel. Subscriptions have a
// disposable handle but emit no fabricated game, power, or input events.
// Unsupported subscriptions remain inert. Lifetime callbacks below relay
// actual Steam notifications; no power, input or launch events are invented.
const subscription = () => {const dispose=()=>{};dispose.unregister=dispose;return dispose;};
const register = names => Object.fromEntries(names.map(name=>[name,subscription]));
const steamLanguage = () => {
@@ -403,7 +443,14 @@
...register(['RegisterForSettingsChanges'])},
Apps:register(['RegisterForAppDetails','RegisterForGameActionTaskChange',
'RegisterForGameActionStart','RegisterForGameActionEnd','RegisterForGameActionUserRequest']),
GameSessions:register(['RegisterForAppLifetimeNotifications','RegisterForScreenshotNotification']),
GameSessions:{...register(['RegisterForScreenshotNotification']),
RegisterForAppLifetimeNotifications:listener=>{
if (typeof listener !== 'function') throw Error('Invalid Steam lifetime listener');
if (runningListeners.size >= 32) throw Error('Too many Steam lifetime listeners');
runningListeners.add(listener);
const dispose=()=>runningListeners.delete(listener);
dispose.unregister=dispose; return dispose;
}},
System:{...register(['RegisterForOnResumeFromSuspend','RegisterForOnSuspendRequest']),
GetSystemInfo:()=>request('GetSystemInfo',[],false,'framey:steam'),
UI:register(['RegisterForFocusChangeEvents','RegisterForSystemKeyEvents'])},
@@ -428,7 +475,9 @@
},
},
};
DFL.Router = {...DFL.Navigation, ...window.SteamUIStore};
DFL.Router = {...DFL.Navigation,
get RunningApps() {return runningApps;}, get MainRunningApp() {return mainRunningApp;},
};
DFL.DialogButton.render = () => ({type: DFL.DialogButton});
const toaster = {toast: ({title, body, subtext, icon, logo, duration, showToast=true, onClick}={}) => {
const data = {title, body, subtext, icon, logo};
@@ -541,6 +590,26 @@
},
fetchNoCors: () => Promise.reject(Error('Decky proxy fetch is unavailable in Framey')),
};
function projectApps(rows, limit) {
if (!Array.isArray(rows) || rows.length > limit) throw Error('Invalid Steam app overview');
return rows.map(row => {
if (!Array.isArray(row) || (row.length !== 8 && row.length !== 9) || !Number.isSafeInteger(row[0]) || row[0] <= 0 ||
typeof row[1] !== 'string' || row[1].length > 2048 ||
(row[2] != null && typeof row[2] !== 'string') || !Number.isSafeInteger(row[3]) ||
typeof row[4] !== 'string' || !/^\d{1,20}$/.test(row[4]) ||
row.slice(5,8).some(value=>value != null && (!Number.isFinite(value) || value < 0)) ||
(row.length === 9 && row[8] !== null && typeof row[8] !== 'boolean'))
throw Error('Invalid Steam app overview');
const [appid,display_name,sort_as,app_type,gameid,
minutes_playtime_forever,minutes_playtime_last_two_weeks,rt_last_time_played] = row;
return {appid,display_name,sort_as,app_type,gameid,
minutes_playtime_forever,minutes_playtime_last_two_weeks,rt_last_time_played,
// null means Steam supplied no installed boolean, not false. Keep the
// detached metadata object for legacy readers; never copy paths/client IDs.
// Older/running snapshots omit this field and must not erase library data.
...(row.length === 9 ? {local_per_client_data:row[8] === null ? {} : {installed:row[8]}} : {})};
});
}
function setLibrarySnapshot(snapshot) {
// These are detached plugin-local values. Patching an overview or its Map
// must never mutate Steam's library. Live MobX observations are unsupported.
@@ -548,28 +617,82 @@
!Array.isArray(snapshot.gameIds) || snapshot.gameIds.length > 10000 ||
(snapshot.steamId != null && (typeof snapshot.steamId !== 'string' || !/^\d{17}$/.test(snapshot.steamId))))
throw Error('Invalid Steam library snapshot');
const apps = snapshot.apps.map(row => {
if (!Array.isArray(row) || row.length !== 8 || !Number.isSafeInteger(row[0]) || row[0] <= 0 ||
typeof row[1] !== 'string' || row[1].length > 2048 ||
(row[2] != null && typeof row[2] !== 'string') || !Number.isSafeInteger(row[3]) ||
typeof row[4] !== 'string' || !/^\d{1,20}$/.test(row[4]) ||
row.slice(5).some(value=>value != null && (!Number.isFinite(value) || value < 0)))
throw Error('Invalid Steam app overview');
const [appid,display_name,sort_as,app_type,gameid,
minutes_playtime_forever,minutes_playtime_last_two_weeks,rt_last_time_played] = row;
return {appid,display_name,sort_as,app_type,gameid,
minutes_playtime_forever,minutes_playtime_last_two_weeks,rt_last_time_played};
});
const apps = projectApps(snapshot.apps, 10000);
const map = new Map(apps.map(app=>[app.appid,app]));
if (map.size !== apps.length || snapshot.gameIds.some(id=>!Number.isSafeInteger(id) || !map.has(id)))
throw Error('Invalid Steam game collection');
window.appStore = {allApps:apps, m_mapApps:map,
GetAppOverviewByAppID:id=>map.get(Number(id)),
GetAppOverviewByGameID:id=>apps.find(app=>app.gameid === String(id)),
GetVerticalCapsuleURLForApp:app=>runningArtwork.get(Number(app?.appid))?.[0] || '',
GetCustomVerticalCapsuleURLs:app=>[...(runningArtwork.get(Number(app?.appid))?.[1] || [])],
};
window.collectionStore = {allGamesCollection:{allApps:snapshot.gameIds.map(id=>map.get(id))}};
window.App = snapshot.steamId ? {m_CurrentUser:{strSteamID:snapshot.steamId}} : undefined;
}
function setRunningSnapshot(data) {
if (!data || !Array.isArray(data.runningIds) || data.runningIds.length > 64 ||
!Array.isArray(data.events) || data.events.length > 64 ||
!Array.isArray(data.artwork) || data.artwork.length > 65 ||
!Number.isSafeInteger(data.cursor) || data.cursor < 0 || typeof data.lifetimeAvailable !== 'boolean')
throw Error('Invalid Steam running snapshot');
const apps = projectApps(data.apps, 65), map = new Map(apps.map(app=>[app.appid,app]));
if (map.size !== apps.length || new Set(data.runningIds).size !== data.runningIds.length ||
data.runningIds.some(id=>!Number.isSafeInteger(id) || !map.has(id)) ||
(data.mainAppId !== null && (!Number.isSafeInteger(data.mainAppId) || !map.has(data.mainAppId))))
throw Error('Invalid Steam running apps');
let sequence = 0;
const publicArt = value => typeof value === 'string' && value.length <= 2048 &&
/^https:\/\/(?:[a-z0-9-]+\.)*steamstatic\.com\/steam\/apps\/[^?#\s\\]+$/.test(value);
const artwork = new Map();
for (const row of data.artwork) {
if (!Array.isArray(row) || row.length !== 3 || !map.has(row[0]) || artwork.has(row[0]) ||
(row[1] !== null && !publicArt(row[1])) || !Array.isArray(row[2]) || row[2].length > 8 ||
row[2].some(url=>!publicArt(url))) throw Error('Invalid Steam public artwork');
artwork.set(row[0],[row[1],[...row[2]]]);
}
for (const event of data.events) {
if (!Number.isSafeInteger(event?.sequence) || event.sequence <= sequence || event.sequence > data.cursor ||
!Number.isSafeInteger(event.unAppID) || event.unAppID <= 0 || typeof event.bRunning !== 'boolean')
throw Error('Invalid Steam lifetime event');
sequence = event.sequence;
}
// Update detached stores before callbacks read Router.RunningApps.
runningApps = Object.freeze(data.runningIds.map(id=>map.get(id)));
mainRunningApp = data.mainAppId === null ? undefined : map.get(data.mainAppId);
runningAvailable = true;
runningArtwork = artwork;
if (window.appStore) for (const app of apps) {
const existing = window.appStore.m_mapApps.get(app.appid);
if (existing) Object.assign(existing,app);
else {window.appStore.m_mapApps.set(app.appid,app);window.appStore.allApps.push(app);}
}
const previous = runningCursor;
runningCursor = data.cursor;
for (const event of data.events) if (event.sequence > previous)
for (const listener of runningListeners)
Promise.resolve().then(()=>runningListeners.has(listener) && listener({unAppID:event.unAppID,bRunning:event.bRunning}))
.catch(failure=>console.warn('Steam lifetime listener failed',failure.message));
}
function refreshRunning() {
if (runningClosed) return Promise.resolve();
if (runningRequest) return runningRequest;
runningRequest = request('running',[runningCursor],false,'framey:running')
.then(data=>{if (!runningClosed) setRunningSnapshot(data);})
.catch(failure=>{
runningAvailable = false;
runningApps = Object.freeze([]); mainRunningApp = undefined; runningArtwork = new Map();
console.warn('Framey Steam running context unavailable',failure.message);
}).finally(()=>{
runningRequest = null;
if (!runningClosed) runningTimer = setTimeout(refreshRunning,2000);
});
return runningRequest;
}
addEventListener('pagehide',()=>{
runningClosed = true; clearTimeout(runningTimer); runningListeners.clear();
parent.postMessage({type:'framey:running:close',session:cacheSession},'*');
});
async function start() {
try {
await seeded;
@@ -582,6 +705,8 @@
try {setLibrarySnapshot(await request('GetLibrarySnapshot',[],false,'framey:steam'));}
catch (failure) {console.warn('Framey Steam library unavailable', failure.message);}
}
if (/\b(?:RunningApps|MainRunningApp|RegisterForAppLifetimeNotifications)\b/.test(bundleText))
await refreshRunning();
// Old Decky builds are expressions returning the initializer, rather than
// ES modules. A module wrapper preserves the sandbox and avoids eval.
const legacy = /^\s*\(function\s*\(/.test(bundleText);
+68
View File
@@ -0,0 +1,68 @@
/* Fixed, read-only projection executed in Steam CEF. No plugin source runs here. */
(function(payload) {
const api = window.SteamClient?.GameSessions;
let bridge = window.__frameyRunningBridgeV1;
if (bridge && bridge.api !== api) {
for (const key of [...bridge.sessions.keys()]) bridge.remove(key);
bridge = null;
}
if (!bridge) {
const sessions = new Map();
const remove = key => {
const item = sessions.get(key);
item?.handle?.unregister();
sessions.delete(key);
if (!sessions.size) {clearInterval(bridge.timer); bridge.timer = null;}
};
bridge = window.__frameyRunningBridgeV1 = {api, sessions, remove, timer:null};
}
let item = bridge.sessions.get(payload.session);
if (item && item.pluginId !== payload.pluginId) throw Error('Framey running session belongs to another plugin');
if (!payload.active) {bridge.remove(payload.session); return null;}
const store = window.SteamUIStore;
if (!Array.isArray(store?.RunningApps)) throw Error('Steam running apps are unavailable');
const apps = store.RunningApps;
if (apps.length > 64) throw Error('Too many running Steam apps');
const main = store.MainRunningApp;
const project = app => [app.appid, app.display_name, app.sort_as, app.app_type, String(app.gameid),
app.minutes_playtime_forever, app.minutes_playtime_last_two_weeks, app.rt_last_time_played];
// Include a selected main app even if Steam's list changes between the reads.
const rows = apps.map(project);
if (main && !apps.some(app => app.appid === main.appid)) rows.push(project(main));
const publicArt = value => {
if (typeof value !== 'string' || value.length > 2048) return null;
try {
const url = new URL(value);
if (url.protocol !== 'https:' || url.username || url.password || url.port ||
!/(^|\.)steamstatic\.com$/.test(url.hostname) || !url.pathname.startsWith('/steam/apps/')) return null;
return url.origin + url.pathname; // Never copy local artwork paths or query data.
} catch (_) {return null;}
};
const artwork = [...apps, ...(main && !apps.some(app=>app.appid === main.appid) ? [main] : [])].map(app => {
const art = window.appStore;
const vertical = publicArt(art?.GetVerticalCapsuleURLForApp?.(app));
const custom = art?.GetCustomVerticalCapsuleURLs?.(app);
return [app.appid, vertical, Array.isArray(custom) ? custom.slice(0,8).map(publicArt).filter(Boolean) : []];
});
if (!item) {
if (bridge.sessions.size >= 4) throw Error('Too many Framey running sessions');
item = {pluginId:payload.pluginId, queue:[], sequence:0, touched:Date.now(), handle:null};
if (typeof api?.RegisterForAppLifetimeNotifications === 'function') {
item.handle = api.RegisterForAppLifetimeNotifications(event => {
// This queue contains actual Steam notifications, never snapshot diffs.
if (!Number.isSafeInteger(event?.unAppID) || event.unAppID <= 0 || typeof event.bRunning !== 'boolean') return;
item.queue.push({sequence:++item.sequence, unAppID:event.unAppID, bRunning:event.bRunning});
if (item.queue.length > 64) item.queue.shift();
});
}
bridge.sessions.set(payload.session, item);
if (!bridge.timer) bridge.timer = setInterval(() => {
for (const [key, state] of bridge.sessions)
if (Date.now() - state.touched > 15000) bridge.remove(key);
}, 5000);
}
item.touched = Date.now();
return {apps:rows, artwork, runningIds:apps.map(app => app.appid), mainAppId:main?.appid ?? null,
events:item.queue.filter(event => event.sequence > payload.cursor), cursor:item.sequence,
lifetimeAvailable:!!item.handle};
})
+19 -1
View File
@@ -183,7 +183,8 @@ STEAM_READS = {
const apps = Array.from(appStore.allApps);
if (apps.length > 10000) throw Error('Steam library exceeds the Framey snapshot limit');
return {apps:apps.map(a=>[a.appid,a.display_name,a.sort_as,a.app_type,String(a.gameid),
a.minutes_playtime_forever,a.minutes_playtime_last_two_weeks,a.rt_last_time_played]),
a.minutes_playtime_forever,a.minutes_playtime_last_two_weeks,a.rt_last_time_played,
typeof a.local_per_client_data?.installed === 'boolean' ? a.local_per_client_data.installed : null]),
gameIds:Array.from(collectionStore.allGamesCollection.allApps,a=>a.appid),
steamId:window.App?.m_CurrentUser?.strSteamID || null};
})()''',
@@ -244,3 +245,20 @@ def poll_events(ssh, plugin_id, session, events, cursor=0, receipt=None):
source = Path(__file__).with_name('framey-events.js').read_text(encoding='utf-8')
script = EVENT_SCRIPT.replace('__FRAMEY_EVENTS_SOURCE__', repr(source))
return json.loads(ssh('python3 - ' + shlex.quote(json.dumps(payload)), stdin=script, timeout=25))
def poll_running(ssh, plugin_id, session, active, cursor=0, receipt=None):
"""Project running apps and actual lifetime events for one installed panel."""
if type(plugin_id) is not int or plugin_id <= 0:
raise PanelError('Choose an installed Decky store plugin')
if not isinstance(session, str) or not SESSION.fullmatch(session):
raise PanelError('Invalid Framey running session')
if type(active) is not bool or type(cursor) is not int or not 0 <= cursor <= 9007199254740991:
raise PanelError('Invalid Framey running request')
_current_plugin(ssh, plugin_id, receipt)
payload = {'pluginId':plugin_id, 'session':session, 'active':active, 'cursor':cursor}
source = Path(__file__).with_name('framey-running.js').read_text(encoding='utf-8')
expression = '(' + source + ')(' + json.dumps(payload) + ')'
script = STEAM_READ_SCRIPT.replace('__FRAMEY_STEAM_READ__', repr(expression)).replace(
'__FRAMEY_STEAM_LIMIT__', str(256*1024))
return json.loads(ssh('python3 -', stdin=script, timeout=25))
+38 -2
View File
@@ -53,7 +53,8 @@ iframe{display:block;width:100%;height:560px;background:#0d1925;border:1px solid
<label>Show <select id="deckyStoreFilter"><option value="all">All plugins</option><option value="compatible">No known install blocker</option><option value="installed">Installed plugins</option></select></label></div>
<p id="deckyStoreSummary" role="status" aria-live="polite"></p><div id="deckyStoreList" class="stack"></div>
<div class="row" style="margin-top:14px"><button id="deckyStorePrevious" class="secondary">Previous</button><span id="deckyStorePage"></span><button id="deckyStoreNext" class="secondary">Next</button></div></div>
<div id="frameyPanel" hidden><div class="row" style="margin-top:16px"><strong id="frameyTitle"></strong><button id="frameyClose" class="secondary">Close plugin panel</button></div>
<div id="frameyPanel" hidden><div class="row" style="margin-top:16px"><strong id="frameyTitle"></strong><button id="frameyClose" class="secondary">Close plugin panel</button>
<button id="frameyScrollUp" class="secondary">Scroll plugin up</button><button id="frameyScrollDown" class="secondary">Scroll plugin down</button></div>
<p id="frameyCompatibility"></p>
<p id="frameyPanelStatus" role="status">Loading plugin panel…</p>
<iframe id="frameyFrame" title="Framey Decky plugin panel" sandbox="allow-scripts allow-popups" referrerpolicy="no-referrer"></iframe></div>
@@ -237,8 +238,12 @@ function openFrameyPanel(id,name){
setTimeout(()=>{if(activeFrameyId===id&&byId('frameyPanelStatus').textContent==='Loading plugin panel…')
byId('frameyPanelStatus').textContent='No compatible panel loaded. This plugin may need Steam UI features Framey cannot provide.';},12000);
}
let frameyEventsActive=false;
let frameyEventsActive=false,frameyRunningActive=false;
function closeFrameyEvents(){
if(frameyRunningActive&&activeFrameyId!==null&&frameySession)
api('/api/framey/running',{id:activeFrameyId,receipt:activeFrameyReceipt?.sha256,
session:frameySession,active:false}).catch(()=>{});
frameyRunningActive=false;
if(frameyEventsActive&&activeFrameyId!==null&&frameySession)
api('/api/framey/events',{id:activeFrameyId,receipt:activeFrameyReceipt?.sha256,
session:frameySession,events:[],cursor:0}).catch(()=>{});
@@ -247,6 +252,12 @@ function closeFrameyEvents(){
function closeFrameyPanel(){closeFrameyEvents();activeFrameyId=null;activeFrameyReceipt=null;frameySession=null;
byId('frameyFrame').removeAttribute('src');byId('frameyPanel').hidden=true;}
byId('frameyClose').onclick=closeFrameyPanel;
function scrollFramey(direction){
if(activeFrameyId===null||!frameySession||!['up','down'].includes(direction))return;
byId('frameyFrame').contentWindow?.postMessage({type:'framey:scroll',session:frameySession,direction},'*');
}
byId('frameyScrollUp').onclick=()=>scrollFramey('up');
byId('frameyScrollDown').onclick=()=>scrollFramey('down');
function frameyCacheKey(id){return `framey-plugin-cache-v1:${id}`;}
function frameyCacheRead(id){
try{const data=JSON.parse(localStorage.getItem(frameyCacheKey(id))||'{}');
@@ -267,6 +278,12 @@ function frameyCacheSave(id,entries){
window.addEventListener('message',async event=>{
if(activeFrameyId===null||event.source!==byId('frameyFrame').contentWindow)return;
const req=event.data;if(!req||typeof req!=='object')return;
if(req.type==='framey:scroll:result'&&req.session===frameySession){
if(['up','down'].includes(req.direction)&&typeof req.moved==='boolean')
byId('frameyPanelStatus').textContent=req.moved?'Scrolled plugin '+req.direction:
'No more plugin content in that direction';
return;
}
if(req.type==='framey:cache:init'&&req.session===frameySession){
event.source.postMessage({type:'framey:cache:seed',session:frameySession,
entries:frameyCacheRead(activeFrameyId)},'*');return;
@@ -279,6 +296,25 @@ window.addEventListener('message',async event=>{
api('/api/framey/check',{kind:'panel',pluginId:activeFrameyId}).catch(()=>{});return;}
if(req.type==='framey:input'){api('/api/framey/check',{kind:'input'}).catch(()=>{});return;}
if(req.type==='framey:error'){byId('frameyPanelStatus').textContent=`Plugin panel unavailable: ${String(req.message).slice(0,200)}`;return;}
if(req.type==='framey:running:close'){
if(req.session===frameySession&&frameyRunningActive){
frameyRunningActive=false;
api('/api/framey/running',{id:activeFrameyId,receipt:activeFrameyReceipt?.sha256,
session:frameySession,active:false}).catch(()=>{});
}return;
}
if(req.type==='framey:running'){
if(req.session!==frameySession||!Number.isSafeInteger(req.id)||req.id<1||
!Array.isArray(req.args)||req.args.length!==1||!Number.isSafeInteger(req.args[0])||req.args[0]<0)return;
const pluginId=activeFrameyId,session=frameySession,receipt=activeFrameyReceipt?.sha256,
reply={type:'framey:reply',id:req.id};
frameyRunningActive=true;
try{const data=await api('/api/framey/running',{id:pluginId,receipt,session,active:true,cursor:req.args[0]});reply.result=data.result;}
catch(error){reply.error=error.message;}
if(pluginId===activeFrameyId&&session===frameySession&&event.source===byId('frameyFrame').contentWindow)
event.source.postMessage(reply,'*');
return;
}
if(req.type==='framey:events:close'){
if(req.session===frameySession)closeFrameyEvents();return;
}
+11
View File
@@ -606,6 +606,16 @@ def framey_events_action(body):
raise Failure(str(exc), 400) from exc
def framey_running_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.poll_running(ssh, body.get('id'), body.get('session'),
body.get('active'), body.get('cursor', 0), body.get('receipt'))
except framey_panel.PanelError as exc:
raise Failure(str(exc), 400) from exc
def framey_check_action(body):
"""Record only a live, explicitly started worn-headset check."""
if not HEADSET_RUNTIME or body.get('kind') not in ('input', 'panel'):
@@ -3474,6 +3484,7 @@ POST = {
"/api/framey/method": framey_method_action,
"/api/framey/steam": framey_steam_action,
"/api/framey/events": framey_events_action,
"/api/framey/running": framey_running_action,
"/api/framey/check": framey_check_action,
"/api/headset": headset_action,
"/api/headset/quick": headset_quick,