mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-27 00:02:12 +01:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1e526cb4b | ||
|
|
8230fdb19d | ||
|
|
2937a12864 | ||
|
|
71fe4a3ebb | ||
|
|
42f3b2e1e7 | ||
|
|
ca4c370b33 | ||
|
|
c50f96510f | ||
|
|
43e801091a | ||
|
|
b4e632b218 | ||
|
|
d84491c0ec | ||
|
|
83b8d13846 | ||
|
|
340ca88359 | ||
|
|
e62589417b | ||
|
|
03b3796d8d | ||
|
|
93da4e466f | ||
|
|
05093ea4d4 | ||
|
|
232b253e7a | ||
|
|
de8abe32c6 | ||
|
|
f50139fe6a | ||
|
|
698a643eb7 | ||
|
|
d0cef31656 | ||
|
|
c91cfff797 | ||
|
|
d5e62f643d |
No files matched your search
Generated
+2
-2
@@ -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
@@ -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,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.
|
||||
@@ -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,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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -63,6 +63,7 @@ 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 |
|
||||
| `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. |
|
||||
|
||||
@@ -4,7 +4,7 @@ Snapshot: 2026-10-26T06:10:58.537029+00:00; 116 current visible official plugins
|
||||
|
||||
This table and both store views use [`ui/framey-compatibility.json`](../ui/framey-compatibility.json). Results apply only to the exact store ID, version and SHA-256. A changed digest/version is **unverified**. Regenerate this document with `python3 scripts/framey-compatibility-doc.py`; use `--check` in review.
|
||||
|
||||
**Works: 4 · Partial: 48 · Broken: 53 · 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. 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. 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-Line truncated
|
||||
After the VR job released the shared lock, this audit acquired it and used a private temporary Decky 3.3.0 ARM64 user service. Read-only ProtonDB `get_setting` and Steam Status `get_settings` calls succeeded through the live Steam CEF websocket. The service and private base were removed and the lock released; normal managed services stayed inactive, battery was 100 → 99, and no power setting was changed. [Redacted results](evidence/framey-official-backend-2026-10-25.json) record exactly what ran. A [2026-10-26 follow-up](evidence/framey-events-backend-2026-10-26.json) verified default Notebook drawing/save/reopen across a real backend restart and Simple Timer start/cancel, countdown/completion events, a visible toast and persisted recents/subtle mode. Only private test settings were written. Simple Timer remains partial because its own expired-timer startup raises a missing `timer_task` error; sound/suspend are unsupported. A [local audio follow-up](evidence/framey-audio-backends-2026-10-26.md) exercised Volume Mixer, Volume Boost and Mixer through real Decky 3.3.0 amd64 backends on private silent virtual audio targets. Sliders, numeric volume entry, independent server readbacks and Mixer filter persistence across a runtime restart passed. An [ARM64 audio follow-up](evidence/framey-audio-arm64-2026-10-26.md) verified the unmodified official backends in actual Decky 3.3.0 on the Frame user audio session. Volume Mixer and Mixer now qualify as works for per-app volume: Chromium controls changed only a named silent stream, with independent Pulse readbacks. That run left Volume Boost partial because sink writes were denied. A [Volume Boost follow-up](evidence/framey-volume-boost-arm64-2026-10-26.md) now verifies linked/separate channel controls, re-link and reopen through the actual ARM64 backend. Independent Pulse reads confirmed 125%/150% gain writes while the default sink stayed muted; exact original volume/mute was restored. Volume Boost qualifies as works for sink controls. Audible output, Bluetooth playback and worn VR remain unverified. A [running-context follow-up](evidence/framey-running-context-2026-10-26.md) supplies detached running-app/main-app reads and relays actual Steam lifetime callbacks while a panel is open. Real Frame checks verified an empty list, registration/cleanup and the exact Discord Status ARM64 backend reporting no IPC connection. Nonempty games and transitions were synthetic fixtures; real game events, Discord activity and worn VR remain unverified. Discord Status moves to partial; no new works claim is made. A [library context follow-up](evidence/framey-library-context-2026-10-26.md) read 1,106 app overviews, 895 game IDs and the current public Steam ID from real Frame CEF under the shared lock. Local Chromium replayed this detached snapshot to render Deck Settings game selection, Steamback's empty save panel and PlayTime statistics/settings. Steamback and PlayTime used actual Decky 3.3.0 amd64 backends in an isolated container; their ARM64 backends and automatic game hooks remain unverified. Shotty's frontend now starts, but its published backend fails on missing `click`; it stays broken. A [chunked frontend follow-up](evidence/framey-deck-shelves-browser-2026-10-26.md) loaded Deck Shelves 2.0.0 through a managed JavaScript module route and rendered its settings in an opaque local Chromium panel. Its backend was denied; Steam Home shelves and worn VR remain unverified. A [local module lookup follow-up](evidence/framey-module-lookup-2026-10-26.md) supplies a finite registry of Framey-local controls and CSS. TabMaster QAM/docs now render; Steam library-tab editing remains unavailable. That pass exposed Junk-Store executable-script extraction failures; the follow-up below repairs them. SteamGridDB still requires Steam context-menu internals and DeckSP still fails its published MobX fallback. That pass tested TabMaster/Junk-Store in actual isolated amd64 Decky, without ARM64 or worn-VR coverage. A [script entrypoint follow-up](evidence/framey-script-entrypoints-2026-10-26.md) grants owner execute to shebang scripts and ZIP-declared executables without copying archive privilege/write bits. Junk-Store now passes static menu/backend calls in actual Decky 3.3.0 ARM64 and local Chromium Games/Epic navigation, back and reopen. It moves to partial: About still crashes on missing installed-app metadata, Legendary is absent, and login/game operations remain unverified. Existing installations require update/reinstall to repair permissions. A [read-only installed-metadata follow-up](evidence/framey-installed-context-2026-10-26.md) now repairs the About crash. The library snapshot copies only Steam-supplied installed booleans into detached metadata, preserving unknown values as undefined. Junk-Store About/Dependencies, Back and reopen render in local Chromium with actual Frame library data and the official ARM64 backend. It remains partial: dependency/login/game operations, RSS, the optional Line truncated
|
||||
|
||||
## Every current plugin
|
||||
|
||||
@@ -69,7 +69,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 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 | 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 | Frontend audit failed: deckyFrontendLib.getReactRoot is not a function. | static archive, local browser, fake-Frame |
|
||||
| 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 |
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -64,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 '
|
||||
@@ -72,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 '
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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');
|
||||
@@ -262,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);
|
||||
@@ -280,11 +295,12 @@ 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]],runningIds:[10],mainAppId:10,events:[],cursor:0,lifetimeAvailable:true};
|
||||
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)});
|
||||
@@ -293,6 +309,7 @@ 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);
|
||||
@@ -371,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);
|
||||
|
||||
@@ -69,7 +69,6 @@ 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]};
|
||||
@@ -96,3 +95,19 @@ 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);
|
||||
@@ -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)
|
||||
|
||||
@@ -112,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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4184,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,
|
||||
@@ -4241,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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -6505,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,
|
||||
@@ -6543,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"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+42
-4
@@ -34,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();
|
||||
@@ -139,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;
|
||||
@@ -165,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,
|
||||
@@ -366,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()],
|
||||
@@ -560,16 +593,21 @@
|
||||
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 || !Number.isSafeInteger(row[0]) || row[0] <= 0 ||
|
||||
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).some(value=>value != null && (!Number.isFinite(value) || value < 0)))
|
||||
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};
|
||||
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) {
|
||||
|
||||
+2
-1
@@ -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};
|
||||
})()''',
|
||||
|
||||
+14
-1
@@ -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>
|
||||
@@ -251,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))||'{}');
|
||||
@@ -271,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;
|
||||
|
||||
Reference in new issue
Block a user