Compare commits

..
Author SHA1 Message Date
SAPHID fc978a2afd Record exact v0.4.46 candidate Frame smoke 2026-10-26 12:19:32 +00:00
SAPHID 9aff6adcec Merge remote-tracking branch 'origin/main' into release/v0.4.46 2026-10-26 12:17:07 +00:00
SAPHID 2d8b70d607 Prepare Frame Control v0.4.46 candidate 2026-10-26 12:16:54 +00:00
Alex Southwell 80657b8a8e Merge pull request #325 from saphid/fix/framey-identical-zip-members
Accept identical Decky ZIP members for Free Loader
2026-10-26 12:16:23 +00:00
SAPHID 21b85ba99d Record full Free Loader regression run 2026-10-26 12:07:13 +00:00
SAPHID a38d598f14 Merge remote-tracking branch 'origin/main' into fix/framey-identical-zip-members 2026-10-26 12:03:23 +00:00
SAPHID 08567ca571 Accept identical Decky ZIP members for Free Loader 2026-10-26 12:03:13 +00:00
Alex Southwell 4fc5b83873 Merge pull request #324 from saphid/fix/framey-worn-proximity
Require proximity telemetry before worn Framey input
2026-10-26 12:00:16 +00:00
Alex Southwell d96330554a Merge pull request #323 from saphid/fix/framey-decky-user-path
Expose user tools to managed Decky plugins
2026-10-26 11:49:33 +00:00
SAPHID 385ae8290b Require proximity telemetry before worn Framey input 2026-10-26 11:45:51 +00:00
SAPHID 858edaf7eb Expose user tools to managed Decky plugins 2026-10-26 11:40:01 +00:00
Alex Southwell f5213b7c44 Merge pull request #322 from saphid/fix/framey-vr-activity-row
Read SteamVR activity from the status row
2026-10-26 11:28:39 +00:00
SAPHID 0219cd10f0 Read SteamVR activity from the status row 2026-10-26 11:18:03 +00:00
Alex Southwell b0c0599f45 Merge pull request #321 from saphid/docs/v0445-release-evidence
Record v0.4.45 release verification
2026-10-26 11:13:32 +00:00
23 changed files with 428 additions and 47 deletions

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "frame-control",
"version": "0.4.45",
"version": "0.4.46",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frame-control",
"version": "0.4.45",
"version": "0.4.46",
"license": "MIT",
"devDependencies": {
"electron": "^44.4.5",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "frame-control",
"productName": "Frame Control",
"version": "0.4.45",
"version": "0.4.46",
"description": "Desktop app for managing a Valve Steam Frame over SSH",
"private": true,
"main": "main.js",
@@ -0,0 +1,49 @@
# Free Loader duplicate archive entries, 26 October 2026
The exact official Free Loader 1.6.0 archive is store ID 63, SHA-256
`05208aee6f6ba824391d8ddc81b1168b2599a9711e7528cdaa789f6cc710b42a`.
It has 23 ZIP entries. Seven names under `free-loader/py_modules/` occur
twice. Each duplicate pair has identical uncompressed bytes and file type;
five Python files and `.keep` differ only between mode `0644` and `0755`.
The directory entry is identical. No path escape, symlink or alternate content
was accepted.
The installer now accepts an exact duplicate name only when its bytes match
and its mode differs at most in execute bits. Both desktop validation and the
headset transaction enforce that rule before extraction. The existing member
count, unpacked-size, per-member-size, root, path and ARM64 checks still run.
Synthetic tests reject changed duplicate payloads and non-execute mode changes.
## Scoped verification
- The exact archive passed the revised validator. A private local transaction
installed and removed its `free-loader` folder and receipt; cleanup left no
plugin folder. The focused store suite passed 21/21 tests, including a
duplicate-member install/remove and permission readback.
- After merging current main, the full local Python suite passed **1,637
tests, 16 skipped**. The generated compatibility document check and
whitespace check passed.
- The pinned official fake-Frame HTTP/SSH install/remove test passed with this
archive alongside the other pinned fixtures. It ran in a uniquely named
disposable Compose project; its containers, network and volumes were removed.
The fake does not execute the plugin's Python backend.
- The current Framey opaque Chromium host on main `d963305` initialized the
exact frontend. Its **Free Games** view rendered, then displayed “Failed to
load games!” because the audit intentionally denied all backend/network
calls. It attempted `settings_read`, `read_deals` and logger methods.
The browser audit's former `local_bundle` stub had returned HTTP 404 after
receipt-bound panel lookup was introduced; the audit now supplies the
digest-checked bundle and receipt to the shipped panel renderer.
- Exact official Python backend code ran in a disposable Python 3.12 container
with network disabled, read-only root, no capabilities, a private data mount
and a minimal `decky` API stub. `_main`, `settings_read` and `read_deals`
succeeded: 14 default settings, a 12-hour update frequency, an empty deals
map, and only private `settings.json`/`deal_db.json` files. The container
and its private files were removed. No feed refresh or notification ran.
This makes **partial** the accurate digest-scoped status. The earlier full
116-plugin fake-Frame catalog audit blocked this archive before the validator
change; the focused pinned flow now passes. Actual Decky 3.3.0 backend load,
live giveaway feeds, notification delivery, headset install, worn UI and
physical controller input remain unverified. No real Frame session or service
was touched for this change.
@@ -0,0 +1,33 @@
# Tailscale Control read-only backend and Decky PATH, 26 October 2026
The audited archive was official Tailscale Control 0.1.3, store ID 43,
SHA-256 `06411711b27101d438339000da3d91202d682862710b02ef3ddf2462e642448a`.
Earlier local Chromium evidence showed its settings panel. This check focused
on the Frame's CLI lookup; it did not run a live Framey panel RPC.
Each real Frame check acquired `/tmp/frame-test.lock` atomically with its own
random owner, read SteamVR activity and proximity, and released only that
owner's lock. The headset was in standby. The installed Tailscale CLI wrapper
resides in `~/.local/bin`, which the systemd user manager did not put in the
Decky service's inherited `PATH`.
| Check | Result |
| --- | --- |
| Exact official backend with the current service PATH | Three read-only status methods returned `None`; `tailscale` was not found. |
| Same backend in a disposable process with `~/.local/bin` prepended | The read-only methods returned active state, two exit-node entries and 16 device entries. |
| Bounded transient systemd user unit with the proposed service PATH | Found `tailscale` and completed `tailscale status`. The unit ended inactive, PID 0, `LoadState=not-found`. |
The Frame Control managed Decky unit now sets `PATH` to `~/.local/bin` plus
standard system directories when installed or updated. The install transaction
rolls its prior unit back on failure. No Tailscale `up`, `down`, routing or
exit-node change was run. No persistent test service, headset wake, controller
input or settings write was used.
This supports CLI discovery and read-only backend data on the real Frame. It
does not prove the production Framey-to-Decky RPC, connection controls, QR
asset, worn panel visibility or physical controller usability. Tailscale
Control remains **partial** in the digest-scoped catalog.
A later independent hourly read-only check found SteamVR activity 3
(standby), proximity raw 3.0625 and battery 93%. It released its owned lock;
no worn test was possible and the hourly schedule remains enabled.
@@ -0,0 +1,28 @@
# Read-only Framey activity gate, 26 October 2026
At 11:12 UTC, a read-only probe acquired `/tmp/frame-test.lock` by atomic
`mkdir` with a random owner token before reading the Frame. SteamVR
returned two `vrcmd --stats` rows: performance metrics first and the status
row second. The status row reported activity level **1**. The original
`framey-worn-check.py` looked only at row zero and reported unknown activity;
`framey-vr-check.py` used the same row-zero assumption for its wake gate. The
fixed gates select the unique status row and fail closed if it is missing,
duplicated or malformed.
The first locked read found 90% battery, proximity raw 222.25, no active
managed headset panel service and no managed Decky plugin. A subsequent locked
read found activity 1 and proximity raw 223.625. No reliable wearer
confirmation was available. No wake, service install, settings write or input
was attempted.
While activity was 1, a separate owned-lock read captured four seconds from
`/dev/video99` into a private 640×360 H.264 MP4. It decoded successfully and
contained active video imagery. The inspected sample did **not** establish
room passthrough, Frame Control panel visibility, ProtonDB Badges UI or
controller interaction. The private recording and decoded frame were removed
after inspection; no room or media images are committed. Each read released
only its owner-token lock. The hourly worn-check schedule remains enabled.
The local focused safety suite covers a metrics row before the status row,
awake activity, ambiguous/missing status and foreign-lock refusal. A genuine
worn visual and physical-controller check remains outstanding.
@@ -0,0 +1,20 @@
# Interactive Framey worn-check proximity gate, 26 October 2026
The interactive `scripts/framey-worn-check.py` already required an explicit
operator wear confirmation and a unique `/tmp/frame-test.lock` owner before
reading SteamVR activity. It did not read proximity. This change makes its
locked preflight read the raw proximity sensor and stop before its interaction
window when the reading is missing or invalid. It checks activity, proximity
and battery again before input. The same owned-lock cleanup applies on failure.
The focused local gate suite passes 9/9 tests, including missing and nonfinite
proximity, proximity disappearing between reads, metrics preceding the SteamVR status row,
standby, low battery, no operator confirmation and a foreign lock. No headset
input was sent for this change. A separate owner-locked read-only Frame check
found SteamVR activity 3 (standby), proximity raw 3.0625 and battery 93%; it
released its lock without a worn test.
The raw proximity value is not calibrated as proof of wear. The operator's
in-person confirmation remains required; awake activity, proximity and trusted
synthetic input alone do not prove wearer-visible pixels or physical controller
usability. The hourly worn-check schedule remains enabled.
@@ -0,0 +1,44 @@
# v0.4.46 candidate gate, 26 October 2026
This candidate includes merged Free Loader PR #325, the Decky user-tool PATH
fix (#323), and the interactive worn-check proximity gate (#324).
The official 116-plugin catalog is **4 works / 50 partial / 51 broken /
11 N/A**. Free Loader is partial for its verified package, local frontend
and isolated settings/empty-deals scope. No live feed or worn-VR claim follows.
## Locked real Frame smoke before the version bump
At 12:12 UTC, source commit `21b85ba99dfd742a56e2c20259f56f35d83590fc`
ran `scripts/frame-smoke.sh` on the real Frame under a unique owner of
`/tmp/frame-test.lock`. Locked preflight found SteamVR activity **3**
(standby), proximity raw **3.0** and battery **100%** on Frame OS 0.5.6 build
`20261022.6153909`. Test-owned ARM64 and Windows/Proton titles installed,
launched and were removed. The optional x86_64 title installed and was
removed; launch was **skipped** because Steam Linux Runtime 4 is absent.
The result was **12 passed / 1 skipped / 0 failed**. Cleanup passed and the
command exited 0, which includes release of its owned lock. The detailed
result remains private at
`tests/smoke/results/2026-10-26T121228Z-20261022.6153909.json` in the
candidate checkout.
This smoke checks release plumbing and temporary titles, not an installed
official Decky plugin. No wearer was confirmed. The interactive worn-check
script was not run, and no headset wake or controller input was sent. Free
Loader's actual ARM64 Decky backend, live feeds, panel visibility to a wearer,
physical controllers, keyboard and optical comfort remain unverified. The
hourly worn-headset check stays enabled.
## Publication boundary
At 12:17 UTC, exact version-bumped candidate commit
`9aff6adcec71852ce9bedf0ae37364bd32bb6627` ran the same locked
`scripts/frame-smoke.sh`. Preflight again found activity **3** standby,
proximity raw **3.0** and battery **100%**. The result was **12 passed /
1 optional x86 launch skipped / 0 failed**. Owned title/process cleanup
passed and the command exited 0 after releasing its lock. Its private result
is `tests/smoke/results/2026-10-26T121721Z-20261022.6153909.json` in the
candidate checkout. This paragraph is the only change after that smoke.
Green release-PR and post-merge CI, tagged installer build, all ten installer
digests with `SHA256SUMS`, independent package checks, publisher dry run and
public `update.json` readback remain required before publication.
+5 -3
View File
@@ -4,7 +4,7 @@ Snapshot: 2026-10-26T06:10:58.537029+00:00; 116 current visible official plugins
This table and both store views use [`ui/framey-compatibility.json`](../ui/framey-compatibility.json). Results apply only to the exact store ID, version and SHA-256. A changed digest/version is **unverified**. Regenerate this document with `python3 scripts/framey-compatibility-doc.py`; use `--check` in review.
**Works: 4 · Partial: 49 · Broken: 52 · N/A: 11.**
**Works: 4 · Partial: 50 · Broken: 51 · 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.
@@ -25,6 +25,8 @@ python3 scripts/framey-audit-browser.py --cache /tmp/framey-archives
Open the browser audit URL in local Chromium, run `runAudit()` and inspect/download `results`. All backend/network actions remain denied.
A [Free Loader duplicate-entry follow-up](evidence/framey-free-loader-duplicates-2026-10-26.md) accepts only byte-identical duplicate ZIP members whose modes differ at most in execute bits. The exact archive passed private local and focused fake-Frame install/remove; its frontend rendered in opaque Chromium and isolated Python settings/empty-deals reads passed with a Decky stub. The earlier full 116-plugin fake-Frame audit blocked this archive before the fix. Actual Decky, feed updates and worn VR remain unverified.
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. 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
@@ -59,7 +61,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 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 |
| 43: Tailscale Control 0.1.3 | legacy / legacy | partial | Settings render locally after the SuspensefulImage fallback. On the real Frame, exact official read-only backend status calls returned live state, exit-node and device entries when ~/.local/bin was in PATH; a transient user unit found the CLI with the proposed managed-service PATH. Live Framey Decky RPC, connection/exit-node changes and worn VR remain unverified; the bundled localhost QR asset is unavailable. No network settings were changed. | static archive, local browser, fake-Frame, real Frame read-only backend |
| 44: DeckMTP 1.0.5 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: DeckMTP/bin/umtprd. | static archive, local browser, fake-Frame |
| 46: Steamback 1.0.0-1 | legacy / legacy | partial | Panel opens using the real Frame current-user/library snapshot. Account setup and empty save-list reads pass through actual Decky 3.3.0 amd64 in isolation. ARM64 backend, supported-save discovery, automatic backups and restore remain unverified; no backup/restore ran. | static archive, local browser, fake-Frame, real Frame SSH |
| 48: Quick Launch 1.2.1 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
@@ -73,7 +75,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
| 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 |
| 63: Free Loader 1.6.0 | 1 / 2 | partial | The exact archive's duplicate names contain identical bytes and differ only in execute bits; the guarded installer now accepts them, and a private local install/remove transaction passed. The current opaque Chromium host renders Free Games but cannot load deals with backend/network denied. Exact official Python settings and empty local-deals methods passed in a no-network container with a Decky stub. Real Decky, feed refresh, notifications and worn VR remain unverified. | static archive, local browser, fake-Frame pinned install/remove, local exact-archive transaction, isolated Python backend |
| 64: Syncthing 0.2.7 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: decky-syncthing/bin/decky-syncthing-watchdog. | static archive, local browser, fake-Frame |
| 65: Decky Terminal 0.5.3 | 1 / 2 | partial | Local routes initialize after parameter-route support; no shell commands were run. | static archive, local browser, fake-Frame |
| 66: PlayTime 3.2.5 | 1 / 2 | partial | Statistics and routed settings render with a detached real Frame library/user snapshot and actual Decky 3.3.0 amd64 backend in isolation. User selection and empty statistics reads pass. Running snapshots are now exposed, but its private MobX observation is unsupported; automatic recording and ARM64 backend remain unverified. | static archive, local browser, fake-Frame, real Frame SSH |
+4 -2
View File
@@ -112,8 +112,10 @@ the same-plugin navigation regression without a headset.
interaction window, use the physical controller to open the sample and
toggle **Example setting**. Confirm the laser aims where expected and the
script records a fresh input/panel event and visible stereo output. The flag
is the operator’s statement; sensor activity and trusted pointers can also
be produced synthetically and cannot identify controller hardware.
is the operator’s statement. The script requires valid locked SteamVR activity
and proximity readings before input; proximity alone cannot establish wear.
Sensor activity and trusted pointers can also be produced synthetically and
cannot identify controller hardware.
3. **1:15–2:15:** Use the controller to open Settings, return to Panel, invoke
**Read saved setting**, close/reopen the plugin, and confirm the value
remains. Try controller scroll, a slider and a harmless text field in an
+13 -7
View File
@@ -29,6 +29,18 @@ window.fetch=()=>Promise.reject(Error('Audit blocks plugin network'));
window.WebSocket=class{constructor(){throw Error('Audit blocks WebSocket');}};
</script>'''
def render_panel(record, data):
if hashlib.sha256(data).hexdigest() != record['sha256']:
raise ValueError('Archive hash mismatch')
with zipfile.ZipFile(io.BytesIO(data)) as archive:
bundle = archive.read(record['folder']+'/dist/index.js')
# The audit supplies only digest-checked archive bytes, not an installed
# receipt. Stub the receipt-bound lookup while using the shipped renderer.
with patch.object(framey_panel, '_owned_dist', return_value=(None, bundle, record['sha256'])):
return framey_panel.panel(record['id']).replace(
b'<script id="framey-bundle"', PRELUDE.encode()+b'<script id="framey-bundle"')
PAGE = '''<!doctype html><meta charset=utf-8><title>Framey catalog frontend audit</title>
<h1>Framey catalog frontend audit</h1>
<p>Exact archived frontend code runs in an opaque iframe. All backend calls and network requests are denied.
@@ -116,13 +128,7 @@ def main():
try:
record = records[int(path.split('/')[-1])]
data = (args.cache/(record['sha256']+'.zip')).read_bytes()
if hashlib.sha256(data).hexdigest() != record['sha256']:
raise ValueError('Archive hash mismatch')
with zipfile.ZipFile(io.BytesIO(data)) as archive:
bundle = archive.read(record['folder']+'/dist/index.js')
with patch.object(framey_panel, 'local_bundle', return_value=bundle):
body = framey_panel.panel(record['id']).replace(
b'<script id="framey-bundle"', PRELUDE.encode()+b'<script id="framey-bundle"')
body = render_panel(record, data)
except (KeyError, ValueError, OSError, zipfile.BadZipFile):
self.send_error(404, 'Verified frontend archive unavailable')
return
+1
View File
@@ -40,6 +40,7 @@ def render(data):
'python3 scripts/framey-audit-browser.py --cache /tmp/framey-archives',
'```', '',
'Open the browser audit URL in local Chromium, run `runAudit()` and inspect/download `results`. All backend/network actions remain denied.', '',
'A [Free Loader duplicate-entry follow-up](evidence/framey-free-loader-duplicates-2026-10-26.md) accepts only byte-identical duplicate ZIP members whose modes differ at most in execute bits. The exact archive passed private local and focused fake-Frame install/remove; its frontend rendered in opaque Chromium and isolated Python settings/empty-deals reads passed with a Decky stub. The earlier full 116-plugin fake-Frame audit blocked this archive before the fix. Actual Decky, feed updates and worn VR remain unverified.', '',
'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. '
+10 -3
View File
@@ -90,6 +90,15 @@ def settings(state):
return pause, timeout
def steamvr_activity(rows):
"""Read the unique status row; metrics can precede it in --stats output."""
if not isinstance(rows, list):
return None
levels = [row.get('activity_level') for row in rows
if isinstance(row, dict) and row.get('operation') == 'status']
return levels[0] if len(levels) == 1 and type(levels[0]) is int else None
def diagnostic_source():
return (ROOT / 'scripts/framey_openvr.py').read_text() + '''
import json
@@ -183,9 +192,7 @@ def main(argv=None):
if not before['battery'] or min(before['battery']) < 20:
raise RuntimeError('Battery unknown or below 20%')
if args.wake or args.exercise:
stats = before.get('stats')
if (not isinstance(stats, list) or not stats or
not isinstance(stats[0], dict) or stats[0].get('activity_level') != 3):
if steamvr_activity(before.get('stats')) != 3:
raise RuntimeError('Frame is awake or SteamVR activity is unknown; active session untouched')
if args.wake or args.exercise:
# Independent systemd timer restores power even if the controller is
+19 -6
View File
@@ -36,16 +36,25 @@ def remote(command, stdin=None, timeout=15):
PROBE = r'''
import json, pathlib, subprocess
import json, math, pathlib, subprocess
power=[]
for item in pathlib.Path('/sys/class/power_supply').glob('*/capacity'):
try:
if (item.parent/'type').read_text().strip() == 'Battery':
power.append({'name':item.parent.name,'percent':int(item.read_text().strip())})
except (OSError,ValueError): pass
proximity=[]
for item in pathlib.Path('/sys/bus/iio/devices').glob('iio:device*/in_proximity_raw'):
try:
value=float(item.read_text().strip())
if math.isfinite(value) and value >= 0: proximity.append(value)
except (OSError,ValueError): pass
stats=subprocess.run(['/opt/steamvr/bin/linuxarm64/vrcmd','--stats'],capture_output=True,text=True,timeout=5)
try: activity=json.loads(stats.stdout)[0]['activity_level'] if stats.returncode == 0 else None
except (ValueError,KeyError,IndexError,TypeError): activity=None
try:
rows=json.loads(stats.stdout) if stats.returncode == 0 else None
levels=[row.get('activity_level') for row in rows if isinstance(row,dict) and row.get('operation')=='status'] if isinstance(rows,list) else []
activity=levels[0] if len(levels)==1 and type(levels[0]) is int else None
except (ValueError,TypeError): activity=None
panels=subprocess.run(['/opt/steamvr/bin/linuxarm64/vrcmd','--overlays'],capture_output=True,text=True,timeout=5)
panel=next((line.strip() for line in panels.stdout.splitlines() if 'valve.steam.desktopgame.2001947813' in line),None)
base=pathlib.Path.home()/'.local/share/frame-control/decky/plugins'
@@ -59,7 +68,7 @@ if base.is_dir() and not base.is_symlink():
if isinstance(p.get('storeId'),int): plugins.append({'id':p['storeId'],'name':manifest['name']})
except (OSError,ValueError,KeyError): pass
service=subprocess.run(['systemctl','--user','is-active','frame-control-headset.service'],capture_output=True,text=True)
print(json.dumps({'battery':power,'activity':activity,'panel':panel,
print(json.dumps({'battery':power,'proximity':proximity,'activity':activity,'panel':panel,
'panelService':service.returncode==0,'plugins':plugins}))
'''
@@ -117,11 +126,15 @@ def main(worn_confirmed=False):
raise RuntimeError('Battery capacity unavailable or below 20%; no device session started')
if before['activity'] is None or before['activity'] >= 3:
raise RuntimeError('SteamVR is in standby; no worn-headset check started')
if not before.get('proximity'):
raise RuntimeError('Proximity reading unavailable; no worn-headset check started')
if not before['panelService'] or not before['plugins']:
raise RuntimeError('Install and open the Frame Control panel and a Framey plugin before this check')
after_lock = probe()
if after_lock['activity'] >= 3 or min(x['percent'] for x in after_lock['battery']) < 20:
raise RuntimeError('Activity or battery changed after acquiring the lock')
if (not isinstance(after_lock.get('activity'), int) or after_lock['activity'] >= 3 or
not after_lock.get('proximity') or not after_lock.get('battery') or
min(x['percent'] for x in after_lock['battery']) < 20):
raise RuntimeError('Activity, proximity or battery changed after acquiring the lock')
state = json.dumps({'nonce':token,'started':time.time()})
code = "import pathlib; p=pathlib.Path.home()/'.local/share/frame-control/headset/worn-check.json'; p.write_text(" + repr(state) + ")"
remote('python3 -c ' + shlex.quote(code))
Binary file not shown.
+2
View File
@@ -17,6 +17,8 @@ PLUGINS = (
'folder': 'hltb-for-deck', 'sha256': 'bd54a23b0b8ec0798aa590e5be236522a593529d7680e77fb888e2971bf3d558'},
{'id': 36, 'name': 'SteamGridDB', 'version': '1.8.1', 'api_version': 1,
'folder': 'decky-steamgriddb', 'sha256': 'e84f5b1f111af8862b06218479491fbf61036e1bfd9f16f93f9901e9b9ebc5e0'},
{'id': 63, 'name': 'Free Loader', 'version': '1.6.0', 'api_version': 1,
'folder': 'free-loader', 'sha256': '05208aee6f6ba824391d8ddc81b1168b2599a9711e7528cdaa789f6cc710b42a'},
{'id': 135, 'name': 'Deck Shelves', 'version': '2.0.0', 'api_version': 1,
'folder': 'Deck-Shelves', 'sha256': 'b566280ad38fd3ecd584b5b46a855f301539eb9a86063834c64b7045f7063f91'},
)
+6 -1
View File
@@ -76,17 +76,22 @@ class DeckyServiceScript(unittest.TestCase):
self.assertEqual((self.base / 'PluginLoader').read_bytes(), first)
receipt = self.base / '.frame-control/receipt.json'
self.assertEqual(json.loads(receipt.read_text())['version'], 'v3.3.0')
self.assertTrue((Path(self.tmp.name) / '.config/systemd/user/frame-control-decky.service').is_file())
service = Path(self.tmp.name) / '.config/systemd/user/frame-control-decky.service'
first_unit = service.read_text()
self.assertIn('Environment=PATH='+str(Path(self.tmp.name) / '.local/bin')+
':/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\n', first_unit)
second = arm_binary(b'B')
result = self.apply('update', second, version='v3.3.1', fail=True)
self.assertNotEqual(result.returncode, 0)
self.assertEqual((self.base / 'PluginLoader').read_bytes(), first)
self.assertEqual(json.loads(receipt.read_text())['version'], 'v3.3.0')
self.assertEqual(service.read_text(), first_unit)
result = self.apply('update', second, version='v3.3.1')
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual((self.base / 'PluginLoader').read_bytes(), second)
self.assertIn('Environment=PATH=', service.read_text())
result = self.apply('remove')
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse((self.base / 'PluginLoader').exists())
+33
View File
@@ -30,6 +30,21 @@ def archive(flags=(), extra=None):
return data, hashlib.sha256(data).hexdigest()
def duplicate_archive(second=b'#!/bin/sh\nexit 0\n', second_mode=0o100755):
stream = io.BytesIO()
with warnings.catch_warnings(), zipfile.ZipFile(stream, 'w') as bundle:
warnings.simplefilter('ignore', UserWarning)
bundle.writestr('Example/plugin.json', json.dumps({'name': 'Example', 'flags': []}))
bundle.writestr('Example/dist/index.js', 'export default {};' + ' ' * 1200)
for mode, payload in ((0o100644, b'#!/bin/sh\nexit 0\n'), (second_mode, second)):
info = zipfile.ZipInfo('Example/bin/helper')
info.create_system = 3
info.external_attr = mode << 16
bundle.writestr(info, payload)
data = stream.getvalue()
return data, hashlib.sha256(data).hexdigest()
class ArchiveTests(unittest.TestCase):
def test_catalog_selects_valid_visible_latest_versions(self):
records = [{'id': 42, 'name': 'Example', 'author': 'Panel Author', 'visible': True, 'versions':
@@ -80,6 +95,14 @@ class ArchiveTests(unittest.TestCase):
with self.assertRaisesRegex(ValueError, 'manifest'):
frame_decky_store.archive_info(data, hashlib.sha256(data).hexdigest(), 'Example')
def test_only_identical_duplicate_payloads_with_execute_bit_difference_are_accepted(self):
data, digest = duplicate_archive()
self.assertEqual(frame_decky_store.archive_info(data, digest, 'Example'), 'Example')
for data, digest in (duplicate_archive(second=b'#!/bin/sh\nexit 1\n'),
duplicate_archive(second_mode=0o100666)):
with self.subTest(digest=digest), self.assertRaisesRegex(ValueError, 'Unsafe'):
frame_decky_store.archive_info(data, digest, 'Example')
@unittest.skipIf(os.name == 'nt', 'remote plugin transaction runs on Linux')
class StoreTransaction(unittest.TestCase):
@@ -152,6 +175,16 @@ class StoreTransaction(unittest.TestCase):
self.assertFalse((self.folder / 'plugin.json').stat().st_mode & 0o111)
self.assertFalse((self.folder / 'dist/index.js').stat().st_mode & 0o111)
def test_identical_duplicate_archive_installs_without_widening_permissions(self):
data, _ = duplicate_archive()
result = self.apply('install', data)
self.assertEqual(result.returncode, 0, result.stderr)
helper = self.folder / 'bin/helper'
self.assertEqual(helper.read_bytes(), b'#!/bin/sh\nexit 0\n')
self.assertTrue(helper.stat().st_mode & 0o100)
self.assertFalse(helper.stat().st_mode & 0o6000)
self.assertEqual(self.apply('remove').returncode, 0)
def test_declared_executable_is_owner_only_without_privilege_bits(self):
info = zipfile.ZipInfo('Example/bin/entrypoint')
info.create_system = 3
+32
View File
@@ -0,0 +1,32 @@
"""The local frontend audit still renders receipt-bound Framey panels."""
import sandbox # noqa: F401
import base64
import hashlib
import importlib.util
import io
from pathlib import Path
import unittest
import zipfile
MODULE = Path(__file__).resolve().parents[1]/'scripts/framey-audit-browser.py'
spec = importlib.util.spec_from_file_location('framey_audit_browser', MODULE)
audit = importlib.util.module_from_spec(spec)
spec.loader.exec_module(audit)
class ReceiptBoundAudit(unittest.TestCase):
def test_digest_checked_archive_renders_without_installed_plugin(self):
bundle = b"import './chunks/frontend.js';"
stream = io.BytesIO()
with zipfile.ZipFile(stream, 'w') as archive:
archive.writestr('Example/dist/index.js', bundle)
data = stream.getvalue()
digest = hashlib.sha256(data).hexdigest()
record = {'id':42, 'folder':'Example', 'sha256':digest}
page = audit.render_panel(record, data)
self.assertIn(base64.b64encode(bundle), page)
self.assertIn(f'/framey/module/42/{digest}/index.js'.encode(), page)
self.assertIn(b'const report=(kind,message)', page)
with self.assertRaisesRegex(ValueError, 'hash mismatch'):
audit.render_panel({**record, 'sha256':'0'*64}, data)
+16 -2
View File
@@ -15,6 +15,18 @@ spec.loader.exec_module(check)
class Safety(unittest.TestCase):
def test_status_row_can_follow_metrics_and_ambiguous_status_fails_closed(self):
self.assertEqual(check.steamvr_activity([
{'operation':'performance','frame_presents':12},
{'operation':'status','activity_level':3}]), 3)
self.assertEqual(check.steamvr_activity([
{'operation':'performance','frame_presents':12},
{'operation':'status','activity_level':1}]), 1)
self.assertIsNone(check.steamvr_activity([
{'operation':'status','activity_level':3},
{'operation':'status','activity_level':1}]))
self.assertIsNone(check.steamvr_activity([{'activity_level':3}]))
def test_overlay_wait_is_bounded_and_stops_when_visible(self):
from unittest.mock import Mock
remote = Mock()
@@ -61,7 +73,7 @@ class Safety(unittest.TestCase):
def test_power_restore_after_partial_setter_failure(self):
state = {'battery':[99], 'pause':'power.pauseCompositorOnStandby=true',
'timeout':'power.turnOffScreensTimeout=17.000000','dashboardVisible':False,
'stats':[{'activity_level':3}], 'proximity':[6.25]}
'stats':[{'operation':'status','activity_level':3}], 'proximity':[6.25]}
with tempfile.TemporaryDirectory() as folder, patch.object(check, 'Remote') as factory:
remote = factory.return_value
remote.python.return_value = state
@@ -78,7 +90,9 @@ class Safety(unittest.TestCase):
self.assertIn('rmdir /tmp/frame-test.lock', commands[-1])
def test_awake_or_unknown_activity_refuses_wake_and_install(self):
for stats in ([{'activity_level':1}], [{'activity_level':2}], [], None):
for stats in ([{'operation':'status','activity_level':1}],
[{'operation':'performance'}, {'operation':'status','activity_level':2}],
[], None):
state = {'battery':[99], 'pause':'power.pauseCompositorOnStandby=true',
'timeout':'power.turnOffScreensTimeout=17.000000',
'dashboardVisible':False, 'stats':stats, 'proximity':[0.0]}
+58 -3
View File
@@ -3,7 +3,9 @@ import sandbox # noqa: F401
import contextlib
import importlib.util
import io
import json
from pathlib import Path
from types import SimpleNamespace
import unittest
from unittest.mock import patch
@@ -30,7 +32,7 @@ class WornGate(unittest.TestCase):
self.assertIn('trusted pointer events alone do not prove',out.getvalue())
def test_standby_is_read_only_under_owned_lock_and_releases_it(self):
state = {'battery':[{'percent':99}], 'activity':3, 'panelService':True,
state = {'battery':[{'percent':99}], 'activity':3, 'proximity':[6.25], 'panelService':True,
'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None}
code, report, remote, probe = self.run_gate(state)
self.assertEqual(code, 1)
@@ -41,7 +43,7 @@ class WornGate(unittest.TestCase):
self.assertIn('rmdir /tmp/frame-test.lock', remote.call_args_list[1].args[0])
def test_low_battery_releases_owned_lock(self):
state = {'battery':[{'percent':19}], 'activity':0, 'panelService':True,
state = {'battery':[{'percent':19}], 'activity':0, 'proximity':[6.25], 'panelService':True,
'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None}
code, report, remote, probe = self.run_gate(state)
self.assertEqual(code, 1)
@@ -50,7 +52,7 @@ class WornGate(unittest.TestCase):
self.assertEqual(remote.call_count, 2)
def test_foreign_lock_prevents_all_sensor_reads_and_is_never_removed(self):
state = {'battery':[{'percent':99}], 'activity':0, 'panelService':True,
state = {'battery':[{'percent':99}], 'activity':0, 'proximity':[6.25], 'panelService':True,
'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None}
code, report, remote, probe = self.run_gate(state, RuntimeError('File exists'))
self.assertEqual(code, 1)
@@ -61,3 +63,56 @@ class WornGate(unittest.TestCase):
def test_screenshot_palette_counts_distinct_colors(self):
self.assertEqual(worn.color_count('0 #ff00aa 1 #ff00aa 2 #00ff00'), 2)
def test_missing_proximity_stops_before_panel_or_input_and_releases_lock(self):
state = {'battery':[{'percent':99}], 'activity':0, 'proximity':[],
'panelService':True, 'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None}
code, report, remote, probe = self.run_gate(state)
self.assertEqual(code, 1)
self.assertIn('Proximity reading unavailable', report)
probe.assert_called_once()
self.assertEqual(remote.call_count, 2)
self.assertIn('rmdir /tmp/frame-test.lock', remote.call_args_list[-1].args[0])
def test_proximity_disappearing_on_second_read_stops_before_input(self):
first = {'battery':[{'percent':99}], 'activity':0, 'proximity':[200.0],
'panelService':True, 'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None}
second = {**first, 'proximity':[]}
out = io.StringIO()
with patch.object(worn, 'probe', side_effect=[first, second]) as probe, \
patch.object(worn, 'remote') as remote, contextlib.redirect_stdout(out):
code = worn.main(worn_confirmed=True)
self.assertEqual(code, 1)
self.assertIn('Activity, proximity or battery changed', out.getvalue())
self.assertEqual(probe.call_count, 2)
self.assertEqual(remote.call_count, 2)
def test_probe_reads_activity_from_status_after_metrics(self):
rows = [{'operation':'performance','frame_presents':12},
{'operation':'status','activity_level':1}]
def run(argv, **_):
if argv[-1] == '--stats':
return SimpleNamespace(returncode=0, stdout=json.dumps(rows))
return SimpleNamespace(returncode=1, stdout='')
for current, expected in (
(rows, 1),
([{'operation':'performance','frame_presents':12}], None),
(rows + [{'operation':'status','activity_level':3}], None),
):
with self.subTest(rows=current):
rows = current
out = io.StringIO()
with patch('subprocess.run', side_effect=run), contextlib.redirect_stdout(out):
exec(worn.PROBE, {})
result = json.loads(out.getvalue())
self.assertEqual(result['activity'], expected)
self.assertIsInstance(result['proximity'], list)
def test_probe_discards_nonfinite_proximity(self):
def glob(_path, pattern):
return [SimpleNamespace(read_text=lambda: 'nan')] if pattern == 'iio:device*/in_proximity_raw' else []
with patch('pathlib.Path.glob', autospec=True, side_effect=glob), \
patch('subprocess.run', return_value=SimpleNamespace(returncode=1, stdout='')), \
contextlib.redirect_stdout(io.StringIO()) as out:
exec(worn.PROBE, {})
self.assertEqual(json.loads(out.getvalue())['proximity'], [])
+7
View File
@@ -123,6 +123,12 @@ try:
next_binary.write_bytes(data)
os.replace(next_binary, binary)
binary.chmod(0o700)
# The Frame's user-installed tools (including its Tailscale CLI)
# live here, while the systemd user manager omits this directory.
# Keep standard system paths for plugins that invoke OS tools.
plugin_path = ':'.join((str(home / '.local/bin'), '/usr/local/sbin',
'/usr/local/bin', '/usr/sbin', '/usr/bin',
'/sbin', '/bin'))
text = ('[Unit]\nDescription=Decky Loader (Frame Control experimental user install)\n'
'After=default.target\n[Service]\nType=simple\nRestart=on-failure\n'
'RestartSec=5\nExecStart='+str(binary)+'\n'
@@ -130,6 +136,7 @@ try:
'Environment=PRIVILEGED_PATH='+str(base)+'\n'
'Environment=CHOWN_PLUGIN_PATH=0\nEnvironment=LIVE_RELOAD=0\n'
'Environment=USE_CEF_CLOSE_WORKAROUND=0\nEnvironment=KEEP_SYSTEMD_SERVICE=1\n'
'Environment=PATH='+plugin_path+'\n'
'[Install]\nWantedBy=default.target\n')
service.write_text(text)
receipt.write_text(json.dumps({'managedBy':'Frame Control','version':version,'sha256':digest})+'\n')
+25 -6
View File
@@ -86,7 +86,7 @@ def archive_info(data, expected_hash, expected_name):
folder = manifests[0].split('/')[0]
if not SAFE_FOLDER.fullmatch(folder):
raise ValueError('Invalid plugin folder')
seen = set()
seen = {}
for entry in files:
name = entry.filename
mode = entry.external_attr >> 16
@@ -95,11 +95,21 @@ def archive_info(data, expected_hash, expected_name):
if entry.is_dir() and parts[-1] == '':
parts = parts[:-1]
if ('\\' in name or name.startswith('/') or
any(part in ('', '.', '..') for part in parts) or name in seen or
any(part in ('', '.', '..') for part in parts) or
parts[0] != folder or entry.file_size > 30 * 1024 * 1024 or
kind not in (0, 0o040000, 0o100000)):
raise ValueError('Unsafe plugin archive member')
seen.add(name)
prior = seen.get(name)
if prior is not None:
# Some official ZIPs repeat unchanged files with execute-bit
# differences. Reject any content or other mode ambiguity.
old_mode = prior.external_attr >> 16
if (prior.file_size != entry.file_size or
((old_mode ^ mode) & ~0o111) or
archive.read(prior) != archive.read(entry)):
raise ValueError('Unsafe plugin archive member')
else:
seen[name] = entry
if entry.is_dir():
continue
with archive.open(entry) as source:
@@ -164,17 +174,26 @@ try:
files=z.infolist()
if not files or len(files)>3000 or sum(f.file_size for f in files)>150*1024*1024:
raise RuntimeError('Plugin archive too large')
seen=set()
seen={}
for f in files:
parts=f.filename.split('/')
if f.is_dir() and parts[-1]=='': parts=parts[:-1]
kind=f.external_attr>>16&0o170000
if (not parts or parts[0]!=folder or f.filename.startswith('/') or
any(part in ('','.','..') for part in parts) or '\\' in f.filename or
f.filename in seen or f.file_size>30*1024*1024 or
f.file_size>30*1024*1024 or
kind not in (0,0o040000,0o100000)):
raise RuntimeError('Unsafe plugin archive')
seen.add(f.filename)
prior=seen.get(f.filename)
if prior is not None:
# Match desktop validation before zipfile overwrites a
# repeated member during extraction.
old_mode=prior.external_attr>>16
if (prior.file_size!=f.file_size or
((old_mode^ (f.external_attr>>16)) & ~0o111) or
z.read(prior)!=z.read(f)):
raise RuntimeError('Unsafe plugin archive')
else: seen[f.filename]=f
stage.mkdir(mode=0o700)
z.extractall(stage)
# zipfile drops execute bits. Legacy plugins also publish shebang
+20 -11
View File
@@ -3185,12 +3185,13 @@
"https://github.com/tailscale-dev/deck-tailscale"
],
"status": "partial",
"reason": "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.",
"reason": "Settings render locally after the SuspensefulImage fallback. On the real Frame, exact official read-only backend status calls returned live state, exit-node and device entries when ~/.local/bin was in PATH; a transient user unit found the CLI with the proposed managed-service PATH. Live Framey Decky RPC, connection/exit-node changes and worn VR remain unverified; the bundled localhost QR asset is unavailable. No network settings were changed.",
"blockInstall": false,
"verification": [
"static archive",
"local browser",
"fake-Frame"
"fake-Frame",
"real Frame read-only backend"
],
"browser": {
"id": 43,
@@ -4650,13 +4651,15 @@
"https://github.com/jwhitlow45/free-loader",
"https://github.com/jwhitlow45/free-loader.git"
],
"status": "broken",
"reason": "Unsafe plugin archive member.",
"blockInstall": true,
"status": "partial",
"reason": "The exact archive's duplicate names contain identical bytes and differ only in execute bits; the guarded installer now accepts them, and a private local install/remove transaction passed. The current opaque Chromium host renders Free Games but cannot load deals with backend/network denied. Exact official Python settings and empty local-deals methods passed in a no-network container with a Decky stub. Real Decky, feed refresh, notifications and worn VR remain unverified.",
"blockInstall": false,
"verification": [
"static archive",
"local browser",
"fake-Frame"
"fake-Frame pinned install/remove",
"local exact-archive transaction",
"isolated Python backend"
],
"browser": {
"id": 63,
@@ -4673,8 +4676,8 @@
},
"archiveValidation": {
"id": 63,
"accepted": false,
"reason": "Unsafe plugin archive member"
"accepted": true,
"folder": "free-loader"
},
"backend": "Python",
"hardware": "No specific hardware requirement detected statically; runtime requirements unverified.",
@@ -4685,9 +4688,15 @@
"fakeFrame": {
"id": 63,
"sha256": "05208aee6f6ba824391d8ddc81b1168b2599a9711e7528cdaa789f6cc710b42a",
"archiveValidation": "Unsafe plugin archive member",
"result": "blocked",
"reason": "ValueError: Unsafe plugin archive member."
"archiveValidation": "accepted",
"result": "installed-removed",
"reason": "Targeted pinned official fake-Frame install/remove passed after the validator change; the earlier full 116-plugin audit blocked this digest."
},
"localFollowUp": {
"evidence": "docs/evidence/framey-free-loader-duplicates-2026-10-26.md",
"hostCommit": "d963305",
"frontendReady": true,
"backendScope": "isolated official Python settings/defaults and empty local-deals read with a Decky stub"
}
},
{