mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-28 19:03:56 +01:00
Compare commits
87
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1bc084400 | ||
|
|
978af2f287 | ||
|
|
fbeb24d754 | ||
|
|
dc139633a9 | ||
|
|
85c0bf542c | ||
|
|
0f421bfd2d | ||
|
|
27b5bf27f3 | ||
|
|
c33dc0ccb6 | ||
|
|
78634c09a9 | ||
|
|
5a83989138 | ||
|
|
b5bbcec1a0 | ||
|
|
bd36929053 | ||
|
|
ac04407bcc | ||
|
|
432b29f961 | ||
|
|
c88df07c81 | ||
|
|
423a86fefc | ||
|
|
5847a81066 | ||
|
|
3a8fee1d45 | ||
|
|
082af24115 | ||
|
|
3d06824f4d | ||
|
|
cf89bf1313 | ||
|
|
e93e75d849 | ||
|
|
a6869bbf93 | ||
|
|
a31223a90b | ||
|
|
4a95aeb5d0 | ||
|
|
9ae78aef60 | ||
|
|
821839baae | ||
|
|
d4c523efd9 | ||
|
|
13ed576abd | ||
|
|
21c609b4a2 | ||
|
|
b39905f7cf | ||
|
|
f1473fd610 | ||
|
|
bf56d21eca | ||
|
|
addd37467d | ||
|
|
9776cca4d3 | ||
|
|
fb8d939a28 | ||
|
|
59ad05b323 | ||
|
|
155a3199f8 | ||
|
|
41e5f0048b | ||
|
|
25d0cb41ab | ||
|
|
9e77a35e40 | ||
|
|
bc02974b49 | ||
|
|
5b448bc922 | ||
|
|
b345921385 | ||
|
|
1c60985c9f | ||
|
|
4da7220660 | ||
|
|
728a4dbe9b | ||
|
|
91cf774b21 | ||
|
|
560f3de563 | ||
|
|
022d407ff9 | ||
|
|
014f554012 | ||
|
|
c847b7a3ca | ||
|
|
ee13edabb8 | ||
|
|
fc030c6191 | ||
|
|
8990ddfb91 | ||
|
|
af8fc63f3b | ||
|
|
9c39300185 | ||
|
|
592988c212 | ||
|
|
89f5dc164e | ||
|
|
13c689926d | ||
|
|
1b143094d1 | ||
|
|
b2cc34576c | ||
|
|
2797212d8c | ||
|
|
dab13fa876 | ||
|
|
36cef1096a | ||
|
|
fcae5d66de | ||
|
|
a160043c63 | ||
|
|
64938443c8 | ||
|
|
1911923afa | ||
|
|
2419279a97 | ||
|
|
d958148c73 | ||
|
|
a27e09e72c | ||
|
|
b3618690c2 | ||
|
|
030873703e | ||
|
|
c7979a862d | ||
|
|
b963937227 | ||
|
|
7053c40334 | ||
|
|
f8aff5496b | ||
|
|
92a84ea683 | ||
|
|
4bc5e3cab2 | ||
|
|
86541446cf | ||
|
|
af81e46c35 | ||
|
|
39aa8d56fb | ||
|
|
f586214cb4 | ||
|
|
d20ff7f9aa | ||
|
|
5097a9f999 | ||
|
|
de3d7d4fbc |
No files matched your search
@@ -14,11 +14,11 @@ jobs:
|
||||
checks:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: "3.9" # the oldest python3 the Mac app may pick up (Xcode CLT)
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: "24"
|
||||
- name: Install zsh
|
||||
@@ -67,11 +67,11 @@ jobs:
|
||||
python: "3.13"
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: "24"
|
||||
- name: OBS lifecycle tests
|
||||
@@ -84,11 +84,11 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: "24"
|
||||
- name: Wireless doctor fixtures, UI and live read-only Windows inventory
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
ios:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Generate the project
|
||||
run: brew install xcodegen && cd ios && xcodegen generate
|
||||
- name: Build and test
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install zsh
|
||||
# The runner image's third-party apt sources (packages.microsoft.com) sometimes
|
||||
# fail to refresh; Ubuntu's own lists still update, and the install fails if not.
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Actual Decky 3.3.0 discovery and safe backend probes
|
||||
run: python3 scripts/framey-test-decky-runtime.py --output "$RUNNER_TEMP/decky-runtime.json"
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
os: [ubuntu-latest, ubuntu-24.04-arm]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Native libraries
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: UCRT64
|
||||
|
||||
@@ -81,8 +81,8 @@ jobs:
|
||||
files: app/dist/*.AppImage app/dist/*.deb
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version: "24"
|
||||
# PC in the headset (Experimental, Windows only): our capture adapter and
|
||||
@@ -113,6 +113,18 @@ jobs:
|
||||
run: npm ci && npm run ${{ matrix.script }}
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
- name: Check installer outputs before upload
|
||||
shell: bash
|
||||
run: |
|
||||
# Every platform must produce each advertised file type. An artifact
|
||||
# upload can otherwise succeed with just one file from the path list.
|
||||
read -r -a patterns <<< '${{ matrix.files }}'
|
||||
for pattern in "${patterns[@]}"; do
|
||||
if ! compgen -G "$pattern" >/dev/null; then
|
||||
echo "missing installer output matching $pattern" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
- name: Check the Mac bundle matches its runner
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
@@ -189,14 +201,14 @@ jobs:
|
||||
app/dist/*.zip
|
||||
app/dist/*.AppImage
|
||||
app/dist/*.deb
|
||||
if-no-files-found: ignore
|
||||
if-no-files-found: error
|
||||
|
||||
checksums:
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Attach SHA256SUMS after all installers upload
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
@@ -13,6 +13,19 @@ const path = require("path");
|
||||
|
||||
const REQUIRED = [
|
||||
"ui/server.py",
|
||||
"ui/vendor/deckyfileserver-1.2.0/aarch64.gz",
|
||||
"ui/vendor/deckyfileserver-1.2.0/x86_64.gz",
|
||||
"ui/vendor/deckyfileserver-1.2.0/source.tar.gz",
|
||||
"ui/vendor/deckyfileserver-1.2.0/LICENSE.txt",
|
||||
"ui/vendor/deckyfileserver-1.2.0/GO-LICENSE.txt",
|
||||
"ui/vendor/deckyfileserver-1.2.0/README.md",
|
||||
"scripts/build-framey-fileserver.py",
|
||||
"ui/vendor/localsend-0.4.5/aarch64.gz",
|
||||
"ui/vendor/localsend-0.4.5/source.tar.gz",
|
||||
"ui/vendor/localsend-0.4.5/LICENSE.txt",
|
||||
"ui/vendor/localsend-0.4.5/GO-LICENSE.txt",
|
||||
"ui/vendor/localsend-0.4.5/README.md",
|
||||
"scripts/build-framey-localsend.py",
|
||||
"ui/vendor/psutil-7.2.2/aarch64.whl",
|
||||
"ui/vendor/psutil-7.2.2/x86_64.whl",
|
||||
"ui/vendor/psutil-7.2.2/LICENSE.txt",
|
||||
|
||||
+2
-21
@@ -7,9 +7,9 @@
|
||||
// node build/fetch-deps.js mac arm64 x64 | win x64 | linux x64 arm64
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const https = require("https");
|
||||
const path = require("path");
|
||||
const { execFileSync } = require("child_process");
|
||||
const { get, fetchWithRetry } = require("./fetch-retry");
|
||||
|
||||
const PY = "3.12.14+20260924";
|
||||
const PY_URL = (triple) => "https://github.com/astral-sh/python-build-standalone/releases/download/"
|
||||
@@ -46,27 +46,8 @@ const PRUNE = [
|
||||
.flatMap((d) => [`lib/python3.12/${d}`, `Lib/${d}`]),
|
||||
];
|
||||
|
||||
function get(url, redirects = 5) {
|
||||
return new Promise((resolve, reject) => {
|
||||
https.get(url, { timeout: 60000 }, (res) => {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
res.resume();
|
||||
if (!redirects) return reject(new Error(`${url}: too many redirects`));
|
||||
let next;
|
||||
try { next = new URL(res.headers.location, url).href; }
|
||||
catch { return reject(new Error(`${url}: bad redirect ${res.headers.location}`)); }
|
||||
return resolve(get(next, redirects - 1));
|
||||
}
|
||||
if (res.statusCode !== 200) return reject(new Error(`${url}: HTTP ${res.statusCode}`));
|
||||
const chunks = [];
|
||||
res.on("data", (c) => chunks.push(c));
|
||||
res.on("end", () => resolve(Buffer.concat(chunks)));
|
||||
}).on("timeout", function () { this.destroy(new Error(`${url}: timed out`)); }).on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function download(url, sha256, file) {
|
||||
const data = await get(url);
|
||||
const data = await fetchWithRetry(get, url);
|
||||
const sum = crypto.createHash("sha256").update(data).digest("hex");
|
||||
if (sum !== sha256) throw new Error(`checksum mismatch for ${url}: ${sum}`);
|
||||
fs.writeFileSync(file, data);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
const https = require("https");
|
||||
|
||||
function get(url, redirects = 5, transport = https) {
|
||||
return new Promise((resolve, reject) => {
|
||||
transport.get(url, { timeout: 60000 }, (res) => {
|
||||
res.on("error", reject);
|
||||
res.on("aborted", () => reject(new Error(`${url}: response aborted`)));
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
res.resume();
|
||||
if (!redirects) return reject(new Error(`${url}: too many redirects`));
|
||||
let next;
|
||||
try { next = new URL(res.headers.location, url).href; }
|
||||
catch { return reject(new Error(`${url}: bad redirect ${res.headers.location}`)); }
|
||||
return resolve(get(next, redirects - 1, transport));
|
||||
}
|
||||
if (res.statusCode !== 200) {
|
||||
res.resume();
|
||||
const error = new Error(`${url}: HTTP ${res.statusCode}`);
|
||||
error.statusCode = res.statusCode;
|
||||
return reject(error);
|
||||
}
|
||||
const chunks = [];
|
||||
res.on("data", (chunk) => chunks.push(chunk));
|
||||
res.on("end", () => resolve(Buffer.concat(chunks)));
|
||||
}).on("timeout", function () { this.destroy(new Error(`${url}: timed out`)); }).on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Release assets are pinned by SHA-256 at the call site. Retry only transport
|
||||
// failures and transient HTTP responses; the caller still rejects bad bytes.
|
||||
async function fetchWithRetry(fetcher, url, pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try { return await fetcher(url); }
|
||||
catch (error) {
|
||||
const code = error.statusCode;
|
||||
if (code != null && code !== 408 && code !== 429 && code < 500) throw error;
|
||||
if (attempt === 2) throw error;
|
||||
await pause((attempt + 1) * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { get, fetchWithRetry };
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"version": "0.4.49",
|
||||
"version": "0.4.52",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frame-control",
|
||||
"version": "0.4.49",
|
||||
"version": "0.4.52",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^44.4.5",
|
||||
|
||||
+14
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"productName": "Frame Control",
|
||||
"version": "0.4.49",
|
||||
"version": "0.4.52",
|
||||
"description": "Desktop app for managing a Valve Steam Frame over SSH",
|
||||
"private": true,
|
||||
"main": "main.js",
|
||||
@@ -60,6 +60,16 @@
|
||||
"framey-compatibility.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "../ui/vendor/deckyfileserver-1.2.0",
|
||||
"to": "ui/vendor/deckyfileserver-1.2.0",
|
||||
"filter": ["*.gz", "*.txt", "README.md"]
|
||||
},
|
||||
{
|
||||
"from": "../ui/vendor/localsend-0.4.5",
|
||||
"to": "ui/vendor/localsend-0.4.5",
|
||||
"filter": ["*.gz", "*.txt", "README.md"]
|
||||
},
|
||||
{
|
||||
"from": "../ui/vendor/psutil-7.2.2",
|
||||
"to": "ui/vendor/psutil-7.2.2",
|
||||
@@ -80,7 +90,9 @@
|
||||
"*.sh",
|
||||
"obs-headset-source.py",
|
||||
"frame_power_sample.py",
|
||||
"frame_power_report.py"
|
||||
"frame_power_report.py",
|
||||
"build-framey-fileserver.py",
|
||||
"build-framey-localsend.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { EventEmitter } = require('node:events');
|
||||
const test = require('node:test');
|
||||
const { get, fetchWithRetry } = require('../build/fetch-retry');
|
||||
|
||||
test('installer fetch recovers a dropped connection without hiding final failure', async () => {
|
||||
let calls = 0;
|
||||
const delays = [];
|
||||
const bytes = await fetchWithRetry(async () => {
|
||||
if (++calls < 3) throw new Error('socket closed');
|
||||
return Buffer.from('pinned bytes');
|
||||
}, 'https://example.invalid/package', async (ms) => delays.push(ms));
|
||||
assert.equal(bytes.toString(), 'pinned bytes');
|
||||
assert.equal(calls, 3);
|
||||
assert.deepEqual(delays, [1000, 2000]);
|
||||
|
||||
calls = 0;
|
||||
await assert.rejects(fetchWithRetry(async () => {
|
||||
calls++;
|
||||
throw new Error('socket closed');
|
||||
}, 'https://example.invalid/package', async () => {}), /socket closed/);
|
||||
assert.equal(calls, 3);
|
||||
});
|
||||
|
||||
test('installer fetch rejects permanent HTTP errors on the first attempt', async () => {
|
||||
let calls = 0;
|
||||
const missing = new Error('HTTP 404');
|
||||
missing.statusCode = 404;
|
||||
await assert.rejects(fetchWithRetry(async () => {
|
||||
calls++;
|
||||
throw missing;
|
||||
}, 'https://example.invalid/missing', async () => {}), /HTTP 404/);
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
|
||||
test('installer fetch retries a transient HTTP response', async () => {
|
||||
let calls = 0;
|
||||
const busy = new Error('HTTP 503');
|
||||
busy.statusCode = 503;
|
||||
const bytes = await fetchWithRetry(async () => {
|
||||
if (++calls === 1) throw busy;
|
||||
return Buffer.from('pinned bytes');
|
||||
}, 'https://example.invalid/package', async () => {});
|
||||
assert.equal(bytes.toString(), 'pinned bytes');
|
||||
assert.equal(calls, 2);
|
||||
});
|
||||
|
||||
test('installer fetch retries a response aborted after headers and partial bytes', async () => {
|
||||
let calls = 0;
|
||||
const transport = {
|
||||
get(_url, _options, callback) {
|
||||
const request = new EventEmitter();
|
||||
queueMicrotask(() => {
|
||||
const response = new EventEmitter();
|
||||
response.statusCode = 200;
|
||||
response.headers = {};
|
||||
callback(response);
|
||||
if (++calls === 1) {
|
||||
response.emit('data', Buffer.from('partial'));
|
||||
response.emit('aborted');
|
||||
} else {
|
||||
response.emit('data', Buffer.from('complete'));
|
||||
response.emit('end');
|
||||
}
|
||||
});
|
||||
return request;
|
||||
},
|
||||
};
|
||||
const bytes = await fetchWithRetry(
|
||||
(url) => get(url, 5, transport),
|
||||
'https://example.invalid/package',
|
||||
async () => {},
|
||||
);
|
||||
assert.equal(bytes.toString(), 'complete');
|
||||
assert.equal(calls, 2);
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
# What we have tested
|
||||
|
||||
Release baseline: Frame Control **v0.4.41**, 26 October 2026. Rows also identify verified post-release main changes explicitly. “Real Frame + physical PC” means an actual headset and a non-virtual computer. “VM only” means a Windows virtual machine, sometimes connected to the real Frame. A CI test or a simulated device is **not** a physical hardware test. The date/version column is the last relevant check, not a promise that every new release was retested on that platform. See each link for the exact procedure and limits.
|
||||
Current published release: Frame Control **v0.4.52**, 28 October 2026. Rows identify the last direct check of each feature; an older row does not mean the latest installer was retested on that hardware. “Real Frame + physical PC” means an actual headset and a non-virtual computer. “VM only” means a Windows virtual machine, sometimes connected to the real Frame. A CI test or a simulated device is **not** a physical hardware test. See each link for the exact procedure and limits.
|
||||
|
||||
| Feature | Real Frame + physical PC tested | VM only | Untested or limited | Last check and proof |
|
||||
|---|---|---|---|---|
|
||||
| Mac desktop release — tested on a real Mac | Physical MacBook Air (MacBookAir9,1), Intel Core i3, macOS 15.7.7 (24G720): official v0.4.39 x64 DMG and ZIP hashes/architectures verified; clean DMG app installed and launched. Native UI, preferences across restart, update discovery of 0.4.40, offline OBS Start/Stop and 116-entry Decky store checked. Actual Open at Login menu toggled on, persisted across Quit/relaunch, then restored off. A 54-byte relay upload/readback/delete passed under coordinator/headset locks | No | Ordinary browser quarantine, right-click Open and Open Anyway remain unverified; the synthetic quarantine probe was inconclusive. Actual login startup untested. Direct frame.local had no route and app SSH trust failed; relay transfer is not native Mac transfer acceptance. Real OBS/Framey VR unverified. Earlier shutdown race fix #284 shipped in v0.4.40, whose Intel installer was not tested here | 26 Oct, official v0.4.39 tag `fd0b0a9`: [repeat Intel acceptance evidence](evidence/intel-mac-v0.4.39-2026-10-26.md); [earlier release test](evidence/mac-v0439-2026-10-26.md) |
|
||||
| Core title smoke test | Linux lxso2 ran the exact v0.4.41 tagged source against the real Frame; 13/13 reserved title steps passed and cleanup completed | No | The headset was unworn; visible rendering and controller input were not checked. Optional Steam Linux Runtime 4 was absent, so the x86_64 test executable did not actually launch. Other feature paths retain their separate limits below | 26 Oct, exact v0.4.41 tag `b5d50a8`, Frame OS 0.5.6 build `20261022.6153909`: [tagged release evidence](evidence/v0441-release-2026-10-26.md); [earlier candidate smoke](evidence/v0441-candidate-frame-2026-10-26.md) |
|
||||
| Core title smoke test | A Linux host ran the v0.4.52 versioned source candidate, whose tree matches the tagged release commit, against the real Frame under a unique lock: 12 steps passed, one optional x86_64 launch was skipped, zero failed; a separate locked cleanup audit passed | No | The headset was in standby and unworn. Steam Linux Runtime 4 was absent, so the x86_64 test executable did not launch. Worn rendering and physical-controller input were not checked | 28 Oct, v0.4.52 tag `v0.4.52`, Frame OS 0.5.6 build `20261022.6153909`: [release verification](evidence/v0452-release-2026-10-28.md), [candidate smoke](evidence/v0452-candidate-2026-10-28.md) |
|
||||
| Windows desktop in the headset | No physical Windows PC test | Windows 11 VM sent a whole desktop to a real Frame; this used an intermediate fix, not the final native DLL | Physical GPU, capture and controller input; individual windows unavailable in bundled DXGI build | 24 Oct, v0.4.10 installer + candidate fix: [VM log](evidence/windows-vm-2026-10-24.md), [PC guide](pc-in-headset.md) |
|
||||
| Linux desktop in the headset | No | No | Native Linux PC capture is disabled; lxso2 lacked a desktop portal and PipeWire session. The separate Steam `-pipewire` helper is not Frame Control's PC capture | 24 Oct, v0.4.17: [host audit](evidence/linux-pc-host-lxso2-2026-10-24.md), [Steam helper PR #163](https://github.com/saphid/frame-control/pull/163) |
|
||||
| Mac desktop in the headset | A physical Mac sent its screen/window to a real Frame | No | Current v0.4.26 Mac installer and worn-headset controller input have not been repeated | 28 Sep, pre-v0.4 / Frame OS 0.4.1: [Mac guide and measurement](streaming.md#b-show-the-macs-desktop-inside-the-frame) |
|
||||
@@ -24,7 +24,7 @@ Release baseline: Frame Control **v0.4.41**, 26 October 2026. Rows also identify
|
||||
| Open at login | No Frame needed; official v0.4.39 on the physical Intel Air toggled the actual native menu on, retained its checkmark after Quit/relaunch and restored it off. Linux autostart file previously toggled on a physical Linux host with an isolated config directory | No | A full OS login session and physical Windows host login remain untested | 26 Oct, v0.4.39: [native Intel toggle evidence](evidence/intel-mac-v0.4.39-2026-10-26.md); Linux v0.4.26: [PR #189](https://github.com/saphid/frame-control/pull/189) |
|
||||
| Local Frame Control extensions | Example panel displayed live Frame status in a browser preview; the real Frame served a read-only example in the experimental headset panel | No | No wearer-visible extension view; third-party extensions and physical Windows/macOS extension folders untested | 25 Oct, v0.4.29: [PR #194](https://github.com/saphid/frame-control/pull/194), [PR #201](https://github.com/saphid/frame-control/pull/201), [extension guide](extensions.md) |
|
||||
| Experimental Frame Control SteamVR panel | Frame OS 0.5.6 registered and served the opt-in overlay. A later unattended check safely woke SteamVR and found the panel in stereo compositor and mirror captures; it removed its temporary service and restored power settings | No | No person wore the headset for that check. Physical controller input, scrolling, VR keyboard, comfort and legibility remain unverified. The large status view has browser and packaging checks, not a wearer review | 26 Oct, v0.4.29–0.4.40: [panel PR #201](https://github.com/saphid/frame-control/pull/201), [unattended evidence](evidence/framey-vr-2026-10-25/README.md), [post-#292 unworn sample exercise](evidence/v0440-release-2026-10-26.md#post-tag-292-panel-regression), [earlier standby-gated exercise](evidence/framey-vr-standby-gate-2026-10-26.md), [worn checklist](framey-vr-check.md) |
|
||||
| Experimental Decky Loader manager | Frame OS 0.5.6 ARM64 user-service install/status/update/remove and managed ProtonDB install/update/remove passed earlier. A private Decky 3.3.0 service returned read-only ProtonDB and Steam Status setting RPCs; a later locked check verified the official Notebook default drawing/save/reopen flow and Simple Timer backend events in local Chromium | No | The loader remains opt-in. Installs/updates reject an archive whose Decky name collides with another installed plugin before download and under the headset transaction lock; this safety path has host/fake Frame tests, not a new real-headset collision test. v0.4.41 classifies the 116 exact-version plugins as 3 works, 48 partial, 54 broken and 11 not applicable. Notebook works for its default flow; Volume Mixer and Mixer work for tested per-app silent-stream volume control only. TabMaster is partial. Deck Shelves moved to partial after its exact chunked frontend rendered settings in local Chromium; its backend and Steam Home shelves remain unverified. Steamback, PlayTime and Deck Settings became partial after detached read-only library/user snapshots and isolated amd64 backend checks; their official ARM64 backends, automatic behavior and VR panels remain unverified. A read-only Steam bridge returned the Frame country, install folders and system information; IsThereAnyDeal selected that country in local Chromium, but its deployed HTTP endpoint and deal lookup remain unverified. An isolated ARM64 LocalSend core answered its info endpoint, but the official x86-only plugin archive and file transfers remain blocked. Volume Mixer and Mixer controlled a named silent stream through real ARM64 Decky with independent Pulse readbacks; Volume Boost default-sink reads matched, but real writes were denied. Audible output, Bluetooth playback, internal-node controls and worn VR remain unverified. Most official backends and VR controls remain unverified. Physical controller use and wearer-visible official panels remain unverified | 26 Oct, v0.4.31–0.4.41: [managed store PR #208](https://github.com/saphid/frame-control/pull/208), [name-collision PR #269](https://github.com/saphid/frame-control/pull/269), [catalog PR #266](https://github.com/saphid/frame-control/pull/266), [backend event evidence](evidence/framey-events-backend-2026-10-26.json), [Steam read PR #274](https://github.com/saphid/frame-control/pull/274), [LocalSend evidence PR #275](https://github.com/saphid/frame-control/pull/275), [local audio evidence](evidence/framey-audio-backends-2026-10-26.md), [ARM64 audio evidence](evidence/framey-audio-arm64-2026-10-26.md), [post-release library evidence](evidence/framey-library-context-2026-10-26.md), [compatibility table](framey-compatibility.md) |
|
||||
| Experimental Framey VR plugin panels and store | Unattended Frame OS 0.5.6 runs showed the Framey sample panel in stereo compositor and mirror captures. The latest standby-gated exercise restored the original power settings and released its shared lock. Synthetic OpenVR clicks changed a live sample setting, read its Decky backend file and confirmed it after a fresh iframe reopen | No | This was the repository sample, not an official store plugin. Physical controllers, native scrolling, VR keyboard, wearer comfort and legibility remain unverified. CSS Loader and SteamGridDB exact archives passed fake Frame install/remove and panel packing; this does not establish theme or artwork controls in VR. Panel-local backend events and React toasts have local/real-backend checks, but no wearer review. Pause Games and Tailscale Control settings render in local Chromium; their live actions are unverified. The 116-plugin catalog had 60 fake installs/removals and 56 expected blocks; v0.4.40 recorded 101 local Chromium initializers ready and 15 failed, up from 98/18 in v0.4.39. Notebook is marked works only for its default drawing/save/reopen flow | 26 Oct, v0.4.38–0.4.40: [post-#294 unworn sample and scroll evidence](evidence/framey-scroll-probe-2026-10-26.md#post-294-exact-main-sample-check), [backend event PR #270](https://github.com/saphid/frame-control/pull/270), [settings/cache PR #271](https://github.com/saphid/frame-control/pull/271), [post-release sample exercise](evidence/v0439-release-2026-10-26.md#post-release-framey-library-change-282), [worn checklist](framey-vr-check.md) |
|
||||
| Experimental Decky Loader manager | Earlier real Frame checks passed opt-in ARM64 user-service install/status/update/remove and managed ProtonDB install/update/remove. The current exact-version catalog records six scoped works: Notebook default drawing, Volume Mixer and Mixer silent per-app volume, Volume Boost muted sink controls, DeckySpy metrics, and Simple Timer silent in-panel timers | No | v0.4.52 classifies 116 exact-version plugins as **6 works / 54 partial / 45 broken / 11 N/A**. “Works” applies only to each documented behavior, never worn VR. Network Sentinel and Battery Tracker are partial; their managed real-Frame ARM64 behavior is unverified. Most other backends, audible output, physical-controller use and wearer-visible official panels remain unverified | 28 Oct, v0.4.52: [exact-version table](framey-compatibility.md), [release verification](evidence/v0452-release-2026-10-28.md), [DeckySpy evidence](evidence/framey-deckyspy-2026-10-26.md), [Simple Timer evidence](evidence/framey-simple-timer-2026-10-26.md), [audio evidence](evidence/framey-audio-arm64-2026-10-26.md) |
|
||||
| Experimental Framey VR plugin panels and store | An unattended Frame OS 0.5.6 exercise showed the repository sample panel in stereo compositor and mirror captures. Synthetic OpenVR clicks changed a live sample setting, read its Decky backend file, and confirmed it after a fresh iframe reopen; temporary services were removed and original power settings restored | No | This was the repository sample, not an official store plugin. Physical controllers, native scrolling, VR keyboard, wearer comfort and legibility remain unverified. Fake-Frame install/remove and local Chromium plugin initialization do not prove an official plugin is visible or usable in worn VR | 28 Oct, v0.4.52 catalog with earlier 25–26 Oct VR evidence: [unattended sample evidence](evidence/framey-vr-2026-10-25/README.md), [compatibility table](framey-compatibility.md), [worn checklist](framey-vr-check.md) |
|
||||
|
||||
Release notes must preserve these limits until a newer recorded check changes them. When a feature is retested, add its date, exact app/Frame version, host type and proof here. The older [v0.3.1 matrix](support-matrix.html) is a historical snapshot and does not describe current coverage.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Large Send to Frame files: source and offline verification
|
||||
|
||||
[Issue #373](https://github.com/saphid/frame-control/issues/373) reports that
|
||||
a 26.5 GB file selected from external USB media failed with “empty or
|
||||
too-large upload” in Frame Control 0.4.8. Current main still imposed an
|
||||
8 GiB `POST /api/upload` body cap for **Send to Frame**, even in the desktop
|
||||
app. The desktop already knows the selected local file path, but previously
|
||||
sent the whole file through HTTP into another temporary copy before SSH.
|
||||
|
||||
The desktop page now passes that path to `POST /api/transfer`, which is
|
||||
enabled in the app-started server. It checks for an absolute, nonempty
|
||||
regular file and copies it directly by the
|
||||
existing rsync/scp route to `~/Downloads`. This avoids both the 8 GiB HTTP
|
||||
cap and a duplicate temporary file on the computer. The endpoint preserves
|
||||
the selected-headset work guard. Large copies have a finite timeout that
|
||||
scales with source size. Plain browser uploads still have an 8 GiB cap and
|
||||
now state it before sending bytes; the page says which route applies.
|
||||
|
||||
Offline checks use a mocked 27 GiB file size without allocating that space.
|
||||
They verify the direct server copy path, a timeout longer than one hour, the
|
||||
page's desktop-versus-browser choice, and early rejection of an oversized
|
||||
browser upload. Loopback tests confirm the browser cannot call the direct
|
||||
route, reject a mismatched selected headset with HTTP 409, and accept the
|
||||
matching desktop request with HTTP 200. Unit tests reject missing, relative,
|
||||
directory, hidden and empty source paths before any SSH. The full Python
|
||||
suite on the final branch passed **1,690 tests, 16 skipped**. Hosted CI is a
|
||||
separate merge gate.
|
||||
|
||||
An actual 26.5 GB transfer from USB media to a Frame, end-to-end throughput,
|
||||
resume after interruption and free-space behavior remain unverified. No real
|
||||
headset or user file was touched for this change.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Battery Tracker 0.2.0 user-service adaptation
|
||||
|
||||
Official store ID 67, ZIP SHA-256 `91c388d3c1b52079314045b79671ad55c767510658b6193f6936ed6cad383b94`; the exact `main.py` SHA-256 is `2303ec70c262f1cbfe1c8c138770c6e7c3496827e748e679c94ca43ff9f28725`. The BSD-licensed archive requests `_root`, hard-codes Steam Deck `BAT1`, raises on empty history, and drops the final sample in per-app grouping. The backend records local battery readings in plugin runtime SQLite; it does not need privileged writes for the verified path.
|
||||
|
||||
The digest-scoped managed install removes `_root`, discovers a readable supply whose `type` is `Battery` (preferring `BAT1` on Deck), fixes empty and single-sample history, retains pending samples while the panel is open, and flushes them on unload. The original ZIP and license remain pinned in the test fixture. Changed archive bytes still fail the root gate; other root plugins remain blocked.
|
||||
|
||||
Under a unique owner token on `/tmp/frame-test.lock`, a **read-only** real Frame probe found SteamVR activity `3` (standby), proximity raw `2.25`, and one Battery-type supply named `max1720x_bat_7-36`. It exposed readable `capacity` (89%), `status` (Charging), `voltage_now`, and `current_now`; `power_now` was absent. The owned lock was released. No wake, input, normal service, power setting, or plugin install was touched.
|
||||
|
||||
In network/device-isolated, unprivileged **actual Decky 3.3.0 amd64**, the exact adapted archive was discovered. A private Frame-shaped supply fixture produced history and per-app power through public legacy RPC. The complete native regression runner, including existing negative controls, passed. Local managed install/remove tests and the exact-source/manifest guard passed. CI's native ARM64 and fake Frame e2e jobs remain required before merge.
|
||||
|
||||
This supports **partial** compatibility. The official frontend initialized in the earlier opaque Chromium audit with backend calls denied; it has not been tested with these live samples. Actual managed recording on Frame ARM64, long-term history, foreground-app attribution, panel controls, and worn VR remain unverified. Existing copies need update or reinstall.
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"plugin": {
|
||||
"id": 60,
|
||||
"name": "DeckyFileServer",
|
||||
"version": "1.2.0",
|
||||
"sha256": "7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0"
|
||||
},
|
||||
"status": "partial",
|
||||
"runtime": {
|
||||
"decky": "v3.3.0",
|
||||
"amd64LoaderSha256": "e80d03e641d1766a33a817cb27ab8d2e0a4633685e5d27e4de948a6687526181",
|
||||
"amd64Probes": [
|
||||
{
|
||||
"id": 60,
|
||||
"result": "discovery, API1 settings, native HTTPS listing/download, thumbnail policy and stop passed; uploads disabled",
|
||||
"discovered": [
|
||||
{
|
||||
"name": "DeckyFileServer",
|
||||
"version": "1.2.0",
|
||||
"load_type": 1,
|
||||
"disabled": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"result": "original empty argument bug reproduced: thumbnail policy ignored",
|
||||
"discovered": [
|
||||
{
|
||||
"name": "DeckyFileServer",
|
||||
"version": "1.2.0",
|
||||
"load_type": 1,
|
||||
"disabled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"realFrame": {
|
||||
"timeUtc": "2026-10-27T00:21:17.975213+00:00",
|
||||
"arch": "aarch64",
|
||||
"uid": 1000,
|
||||
"runtime": {
|
||||
"version": "v3.3.0",
|
||||
"sha256": "8ef5dcf36cbbbb6e97201a6baed5d79c4cc44ec27789fc9af1c5b6ae8f0b25b6"
|
||||
},
|
||||
"publicFixtureOnly": true,
|
||||
"uploadsEnabled": false,
|
||||
"actions": {
|
||||
"discoveryAndApi1Rpc": true,
|
||||
"settingsInitialized": true,
|
||||
"nativeHttpsListing": true,
|
||||
"nativeHttpsDownload": true,
|
||||
"uploadsDenied401": true,
|
||||
"disabledThumbnailsRespected": true,
|
||||
"serverStop": true
|
||||
},
|
||||
"normalServicesBefore": {
|
||||
"decky": "failed",
|
||||
"panel": "inactive"
|
||||
},
|
||||
"normalServicesAfter": {
|
||||
"decky": "failed",
|
||||
"panel": "inactive"
|
||||
},
|
||||
"batteryBefore": [
|
||||
99
|
||||
],
|
||||
"batteryAfter": [
|
||||
100
|
||||
],
|
||||
"cleanup": {
|
||||
"baseAbsent": true,
|
||||
"unitAbsent": true,
|
||||
"archiveAbsent": true
|
||||
},
|
||||
"lockReleased": true,
|
||||
"independentAudit": {
|
||||
"baseAbsent": true,
|
||||
"unitAbsent": true,
|
||||
"process": "MainPID=0\nLoadState=not-found\n",
|
||||
"lockAbsent": true
|
||||
}
|
||||
},
|
||||
"localChromium": {
|
||||
"opaqueOrigin": "null",
|
||||
"warningVisible": true,
|
||||
"uiStart": true,
|
||||
"separateLanHostDownload": true,
|
||||
"uiStop": true,
|
||||
"settingsModal": true,
|
||||
"reopenPersistedDirectory": true,
|
||||
"passed": true
|
||||
},
|
||||
"fakeFrame": "Pinned official archive install/remove; native helper hash, manifest flags and shim receipt asserted; plugin code not executed by fake Frame.",
|
||||
"limitations": [
|
||||
"Frontend prints HTTP and steamdeck URLs although helper uses HTTPS.",
|
||||
"Steam directory picker unavailable; a preconfigured directory was used.",
|
||||
"Settings thumbnail toggle visually showed unchecked despite backend true; no settings save asserted.",
|
||||
"Uploads and generated thumbnails unverified; all test requests read-only.",
|
||||
"Timeout expiry, protected-file permissions and worn VR unverified."
|
||||
],
|
||||
"builds": [
|
||||
{
|
||||
"arch": "x86_64",
|
||||
"sha256": "5e90efd1bf5163bef1686045f4714ad0e4fd0f54edb037fce8cdb06c58177bcb",
|
||||
"gzipSha256": "84e49f049bbc475bc553ef52adc05dcd323e36c5f5ec75008558ae87f3a0e6d7",
|
||||
"bytes": 14815380
|
||||
},
|
||||
{
|
||||
"arch": "aarch64",
|
||||
"sha256": "86b80021ebd62ed619eba065757c1122defa5a74b2c045aa44a0c6b4403a508a",
|
||||
"gzipSha256": "7ca1b8aebd4e8d2c8ce382ca561e1413ad8e581f85f419a1da777c87d86c4a19",
|
||||
"bytes": 13893780
|
||||
}
|
||||
],
|
||||
"startupRetries": "Two private test-adapter failures (CEF reload, script syntax) and one overly early LAN readiness assertion preceded the successful run. Each run removed its private service/base/upload and released the lock; battery stayed 100.",
|
||||
"rebuildMatchedBothAssets": true,
|
||||
"automatedChecks": {
|
||||
"pythonTests": 1662,
|
||||
"pythonSkipped": 16,
|
||||
"nodeAppTests": 46,
|
||||
"publishedDecky330NativeAmd64Probes": 9,
|
||||
"fakeFramePinnedInstallRemove": "passed",
|
||||
"combinedDesktopResources": "passed",
|
||||
"combinedHeadsetBundleBytes": 12485251
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
# DeckyFileServer ARM64 backend — 27 October 2026
|
||||
|
||||
DeckyFileServer **1.2.0**, official store ID **60**, moves **broken → partial**
|
||||
only for the exact managed archive
|
||||
`7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0`.
|
||||
[Redacted observations](framey-fileserver-arm64-2026-10-27.json).
|
||||
|
||||
## Confirmed blockers and adaptation
|
||||
|
||||
The published ZIP contains a 19,779,282-byte x86_64 Go helper and requests `_root`.
|
||||
Its actual operations use ordinary file permissions and an unprivileged port.
|
||||
The managed installer replaces only this digest's original helper, checks the
|
||||
replacement hash and ELF machine on the Frame, and removes only this manifest's
|
||||
root request. Unknown versions, other binaries and root plugins remain blocked;
|
||||
the Frame repeats archive/root/binary checks before extraction.
|
||||
|
||||
Pinned upstream commit `dcf3f3ce5104d05811fa8c156c4f58632b84b313` has Python,
|
||||
manifest and package metadata byte-identical to the store archive. The published
|
||||
Go executable lacks a VCS revision, so that binary's source revision cannot be
|
||||
independently established. The rebuilt static Linux amd64/ARM64 helpers use
|
||||
Go 1.25.7 and vendored dependencies. Complete corresponding GPL source, dependency
|
||||
licenses, build recipe and hash pins ship with both desktop and headset bundles:
|
||||
[provenance](../../ui/vendor/deckyfileserver-1.2.0/README.md).
|
||||
The sole Go change sends its log to Decky's per-plugin log directory instead of
|
||||
one global `/tmp` file. No compiler, pip, root service or runtime download is used.
|
||||
|
||||
Actual published Decky 3.3.0 discovery/public API1 RPC reproduced a hostname-DNS
|
||||
failure in `get_status`. A local-address fallback selects the kernel source IP
|
||||
without sending a packet, or returns loopback when no route exists.
|
||||
The original command array includes empty strings for omitted flags; Go stops
|
||||
parsing there and silently ignores the following `-disablethumbnails` flag.
|
||||
The shim filters empty arguments and applies the thumbnail setting before start.
|
||||
A negative runtime case restores the original empty arguments and proves that
|
||||
thumbnails remain enabled despite the setting. **Decky 3.3.0 does expose
|
||||
`decky.HOME`**; an initial assumption that it was removed was disproved by the
|
||||
actual runtime. That API call and the plugin's API1 instance convention stay intact.
|
||||
|
||||
## Verification
|
||||
|
||||
- **Published Decky 3.3.0 amd64:** actual discovery, default settings, public
|
||||
API1 calls, native HTTPS directory listing and marker download, disabled
|
||||
thumbnails, upload endpoint returning 401, history and server stop. Execution
|
||||
used an unprivileged, network-isolated, read-only disposable container.
|
||||
- **Real Frame ARM64, Frame OS 0.5.6:** the same useful backend actions passed
|
||||
through Steam CEF's existing Decky websocket, using a bounded private user
|
||||
service and a directory containing only public test markers. No home directory
|
||||
was shared and uploads stayed off.
|
||||
- **Local Chromium opaque iframe with the actual Frame backend:** official
|
||||
warning, Enable Server, stop, settings modal and panel reopen with persisted
|
||||
private directory passed. A separate LAN host downloaded exactly the public
|
||||
marker over HTTPS after the panel started the server. The relay allowed only
|
||||
safe settings on that private directory; it denied uploads and other methods.
|
||||
- **Fake Frame:** pinned archive install/remove, original frontend bytes,
|
||||
replaced native helper hash, root-free manifest and managed shim receipt
|
||||
passed through HTTP and SSH. Fake Frame does not execute plugin backends.
|
||||
|
||||
The shared `/tmp/frame-test.lock` was acquired normally and released after each
|
||||
run. Two private test-adapter errors and an early LAN-readiness assertion were
|
||||
corrected before the successful run; all attempts cleaned their private services
|
||||
and artifacts. Normal managed Decky remained failed and the panel inactive,
|
||||
matching their original states. Battery stayed **100 → 100** and no power setting
|
||||
was changed. An independent SSH audit confirms base/unit/upload absent,
|
||||
`MainPID=0`, `LoadState=not-found` and lock absent. Chromium and the local relay
|
||||
were closed. No worn-VR claim is made.
|
||||
|
||||
The final branch, including merged DeckySpy and Simple Timer, passes **1,662
|
||||
Python tests (16 skipped)**, all **9** published Decky 3.3.0 amd64 probes, **46**
|
||||
app tests and pinned fake-Frame install/remove. Both helper rebuilds match byte
|
||||
for byte. The combined headset bundle is 12,485,251 bytes, below its existing
|
||||
32 MiB limit, and includes both native dependencies plus GPL source/licenses.
|
||||
|
||||
## Remaining limitations
|
||||
|
||||
The official frontend advertises **HTTP** and a **steamdeck** hostname even
|
||||
though its helper always serves HTTPS. Use `https://<Frame address>:<port>`;
|
||||
the upstream public self-signed certificate produces a browser warning.
|
||||
Its Steam folder picker is unavailable in Framey; tests preconfigured only the
|
||||
private fixture directory. The thumbnail toggle also appeared unchecked in the
|
||||
settings modal while backend state was true; saving that modal was not tested.
|
||||
These are concrete frontend follow-ups, so the plugin remains **partial**.
|
||||
Uploads, thumbnail generation, inactivity shutdown, protected-file permissions
|
||||
and worn VR remain unverified. The server exposes its selected directory on all
|
||||
interfaces without authentication; select only files intended for sharing on a
|
||||
trusted network. Existing unadapted copies require managed update/reinstall.
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"helperSha256": "e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae",
|
||||
"lockAcquired": true,
|
||||
"action": "private_network_marker_only",
|
||||
"before": {
|
||||
"activity": 3,
|
||||
"proximity": [
|
||||
4.6875
|
||||
],
|
||||
"battery": [
|
||||
72
|
||||
],
|
||||
"services": {
|
||||
"frame-control-decky.service": "inactive",
|
||||
"frame-control-headset.service": "inactive"
|
||||
}
|
||||
},
|
||||
"transfer": {
|
||||
"result": "private_arm64_transfer_passed",
|
||||
"protocolVersion": "2.0",
|
||||
"markerBytes": 38,
|
||||
"markerSha256": "3e724efac20a4a600a93f7bdddd469b8287a6aaa2b7b433d838d8ef23e1a9c2c",
|
||||
"network": "private-veth-no-default-route",
|
||||
"helperSha256": "e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae"
|
||||
},
|
||||
"after": {
|
||||
"activity": 3,
|
||||
"proximity": [
|
||||
4.6875
|
||||
],
|
||||
"battery": [
|
||||
72
|
||||
],
|
||||
"services": {
|
||||
"frame-control-decky.service": "inactive",
|
||||
"frame-control-headset.service": "inactive"
|
||||
}
|
||||
},
|
||||
"result": "private_arm64_transfer_passed",
|
||||
"ownedDirectoryAbsent": true,
|
||||
"ownLockReleased": true
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
# Decky Localsend managed ARM64 candidate, 27 October 2026
|
||||
|
||||
Official Decky Localsend **0.4.5**, store ID **125**, archive SHA-256
|
||||
`e271ccb55eb23f918b99a69f79aec905df4c9b2ef7b23bbf4ec1c2d1a6f0c6b7`,
|
||||
contains an x86_64 helper. Frame Control's proposed managed adaptation accepts
|
||||
only that exact archive. The original x86 helper and Python backend hashes are
|
||||
checked before a pinned static ARM64 helper replaces the binary. The headset
|
||||
transaction checks the ARM64 SHA-256 and ELF machine again. It leaves the
|
||||
official Python, frontend, manifest and BSD license unchanged, retains
|
||||
managed update/remove receipts, and rejects changed archive digests.
|
||||
|
||||
[Source correspondence and local upstream transfer](framey-localsend-source-match-2026-10-27.md)
|
||||
records the exact upstream plugin tag and Go commit. The native asset and
|
||||
build recipe are [bundled](../../ui/vendor/localsend-0.4.5/README.md).
|
||||
Go dependencies are checksum-pinned but not vendored. The Frame installer
|
||||
does not download modules or run a compiler.
|
||||
|
||||
## Evidence so far
|
||||
|
||||
- The host transaction tests installed and removed the exact archive,
|
||||
preserved its Python/LICENSE bytes, checked the managed receipt and
|
||||
rejected a changed archive. The ARM64 branch independently wrote and
|
||||
hashed the AArch64 ELF on x86 without executing it.
|
||||
- The fake Frame managed install/remove flow passed with the exact official
|
||||
archive and checked the installed helper digest. Its project containers,
|
||||
volumes and network were absent after the run.
|
||||
- The full local Python suite passed **1,669 tests with 16 skipped** after
|
||||
rerunning with loopback socket access. The first attempt ran inside a
|
||||
workspace sandbox that denied local test sockets across unrelated server
|
||||
tests; it did not identify a LocalSend regression.
|
||||
- A fresh local Chromium opaque iframe initialized the exact official frontend
|
||||
with backend and network effects denied. Its visible text included Backend is
|
||||
stopped, Device Name, Available Devices, File Upload, Receive History and
|
||||
Configuration. The initializer reported ready and called only
|
||||
`get_receive_history`, `get_backend_status` and `get_backend_config`.
|
||||
This proves a visible initial panel with safe fallbacks, not working send,
|
||||
receive or settings controls.
|
||||
- The original x86 backend and helper completed one private no-route marker
|
||||
transfer outside Decky. This does not prove the managed ARM64 binary or
|
||||
actual Decky service.
|
||||
- The published Decky 3.3.0 AMD64 and ARM64 runtime gate discovered the
|
||||
adapted plugin and returned its stopped status and safe configuration through
|
||||
public RPCs. Its helper remained stopped during that gate.
|
||||
- A locked real Frame probe ran the rebuilt ARM64 helper with SHA-256
|
||||
`e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae`
|
||||
in a private network namespace with no default route. Its LocalSend 2.0
|
||||
endpoint accepted and saved one 38-byte marker, and the saved bytes matched
|
||||
the sent marker. The headset was in standby (SteamVR activity 3, proximity
|
||||
raw 4.6875) with 72% battery before and after. Both normal Frame Control
|
||||
services stayed inactive. The owned helper/process/files and shared lock
|
||||
were removed. [Machine-readable result](framey-localsend-arm64-transfer-2026-10-27.json).
|
||||
|
||||
A useful frontend workflow still needs verification. The ARM64 marker test
|
||||
exercised the helper in isolation, not the actual Decky plugin service, local
|
||||
discovery, user-facing transfer, or installation on the headset. No worn
|
||||
visibility or physical-controller claim follows from this candidate.
|
||||
|
||||
The proposed catalog status is **partial** for exact managed archive
|
||||
installation and settings/status. Steam routes, local discovery, user-facing
|
||||
transfer, clipboard/screenshot sharing and worn VR remain unverified. The
|
||||
historical raw-archive audit fields still describe the earlier unadapted x86
|
||||
ZIP; `managedNative` records the current digest-scoped path separately.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Decky Localsend 0.4.5 source match and ARM64 build, 27 October 2026
|
||||
|
||||
The exact official store archive (ID 125, SHA-256
|
||||
`e271ccb55eb23f918b99a69f79aec905df4c9b2ef7b23bbf4ec1c2d1a6f0c6b7`)
|
||||
remains **blocked** on Frame: it carries an x86_64 `bin/localsend-core`.
|
||||
No managed plugin install, transfer, or status promotion was performed.
|
||||
|
||||
The archive's `main.py` has SHA-256
|
||||
`db03f9201fe50b75dea99a7a05b54ec3f352d1bdfe174115b0e569154198d8ad`,
|
||||
identical byte for byte to `main.py` at the upstream Decky plugin tag `v0.45`
|
||||
(`7c166bdc33e2a667527ffd511eb5b90d3ce0bd67`). That tag pins its Go
|
||||
submodule to `ad28b8e9440e17e44b4735a6c4aa0512009cf333`. All 42 dependency
|
||||
versions embedded in the official helper match that source's `go.mod`, which
|
||||
declares Go 1.25.7.
|
||||
|
||||
Building that pinned Go commit with `CGO_ENABLED=0`, `GOOS=linux`,
|
||||
`GOARCH=amd64`, and the flags `-trimpath -buildvcs=false -ldflags='-s -w'`
|
||||
produced **byte-identical** official helper bytes: 15,241,400 bytes,
|
||||
SHA-256 `d448d03861d578685e6c70ea3ace25eb34fca99cb9d4f98bf51ba23aa5cae843`.
|
||||
This resolves the source correspondence gap left by the earlier build from
|
||||
the later `66edc509` commit. The same pinned source and flags with
|
||||
`GOARCH=arm64` produced a static AArch64 ELF of 11,731,128 bytes, SHA-256
|
||||
`e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae`.
|
||||
Two ARM64 builds matched byte for byte. The Go core is MIT licensed; the
|
||||
separate Decky Python plugin is BSD-3-Clause.
|
||||
|
||||
A unique-owner atomic `/tmp/frame-test.lock` preflight on the real Frame read
|
||||
SteamVR activity **1**, proximity raw **88.0625**, and battery **35%**.
|
||||
Because the headset was active or wearer status was unknown, the probe did
|
||||
not upload or execute the ARM64 helper, start a LocalSend service, or send
|
||||
input. The task-owned directory was absent and only the owned lock was
|
||||
released. This is **not** a worn visual or controller result.
|
||||
|
||||
The unmodified plugin backend's `_main` starts a fixed-path Unix notification
|
||||
socket but does not start the Go network service. Its explicit `start_backend`
|
||||
method starts the helper with a default all-interface setting, HTTPS and
|
||||
auto-save disabled. A local follow-up ran that exact Python backend with the
|
||||
official x86_64 helper in a disposable user/network namespace. A veth pair
|
||||
provided only documentation-range private addresses and there was no default
|
||||
route. A private Decky module stub supplied task-owned settings, runtime,
|
||||
plugin and log directories; its notification socket was moved into that
|
||||
directory. The backend's `start_backend`, `proxy_get` info request and
|
||||
`proxy_post` prepare/upload methods passed. One 42-byte public marker
|
||||
arrived in the private upload directory with independent SHA-256
|
||||
`346373ca56ec52c3f47b8d41de2a6a8479d8ad10d2891ff3b0dc7ea9c3892a39`.
|
||||
The backend stopped, its process and private directory were absent, and the
|
||||
global notification socket was untouched. This verifies the upstream x86
|
||||
backend/proxy/transfer contract locally, **not** Decky 3.3.0 discovery, ARM64
|
||||
transfer, an installed Frame plugin or real-network transfer.
|
||||
|
||||
Before a managed ARM64 adaptation, verify actual Decky backend discovery/RPC,
|
||||
the frontend and a private transfer with the exact-source ARM64 helper;
|
||||
package the source, license and exact helper digest. The official x86_64
|
||||
archive remains blocked until that work passes. Worn VR and physical
|
||||
controller use remain unverified.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Network Sentinel 1.0.4 managed user-service check
|
||||
|
||||
Official store ID 114, ZIP SHA-256 `d3e07f40a0c800e72d15594e03c8c61e9bf3092524a578a0260520e735b971a5`.
|
||||
The exact archive's `main.py` SHA-256 is `2baa390cb66dc267e8f04e61ba1edc43af75115ad5e453d308f6bf26ed70b6b9`.
|
||||
Its backend imports `psutil`, but the ZIP contains no wheel. The manifest requests `_root` although the inspected backend uses user-readable interface counters, subprocess ping and DNS, settings, and optional `nmcli` reads. Loading the backend does not start monitoring.
|
||||
|
||||
The managed install checks the complete archive digest, manifest and Python source before removing `_root`. It adds the existing pinned BSD psutil 7.2.2 wheel for the native architecture to this plugin's `py_modules`, uses Decky's plugin-scoped settings directory, leaves legacy path migration inactive in the user service, and initializes ping jitter for failed/offline calls. Other root-requesting archives still fail the root gate. The receipt records `network-sentinel-user-psutil`; existing copies need update or reinstall.
|
||||
|
||||
Local transaction tests installed and removed the exact archive, checked the native wheel and unmodified license, and rejected an altered archive. In network/device-isolated, unprivileged **actual Decky 3.3.0 amd64**, the managed plugin was discovered, imported native psutil, returned default and updated settings, reported empty history and bandwidth, started its monitoring loop, produced a history sample with quality and bandwidth fields, then stopped. The full native regression runner also passed its other cases, including FileServer and negative controls. The first run's existing large FileServer install timed out at 15 seconds under host load; its bounded timeout is now 30 seconds and the complete rerun passed. No external network call succeeded in the isolated container. Native ARM64 and fake Frame install/remove are CI gates for this change; neither is claimed by this local evidence.
|
||||
|
||||
The preexisting local Chromium audit initialized the exact frontend with backend calls denied. It did not prove a functioning panel. External latency/DNS, Wi-Fi scans, actual Frame ARM64 runtime, frontend controls, and worn VR remain unverified. This is a **partial** compatibility result, not a full monitoring or headset-usability claim. No real Frame, normal service, power value, or physical network setting was touched in this check.
|
||||
@@ -0,0 +1,35 @@
|
||||
# AutoSuspend and CheatDeck static action limits, 28 October 2026
|
||||
|
||||
This is a read-only follow-up to the exact official Decky compatibility
|
||||
catalog. The two archives were downloaded from the official version CDN and
|
||||
SHA-256 checked before inspection. Neither plugin was imported, installed,
|
||||
opened in a live panel, or run on the Frame.
|
||||
|
||||
## AutoSuspend 2.2.0, store ID 27
|
||||
|
||||
Archive SHA-256:
|
||||
`20ceb9aa5327226326531d947e4a7a5318ba78b73f503266996a81877cffa536`.
|
||||
The Python backend exposes settings reads and writes. The frontend contains a
|
||||
`SteamClient.System.ShutdownPC()` call and Steam suspend callbacks. Its existing
|
||||
opaque Chromium audit fails during initialization with `Cannot read properties
|
||||
of null (reading '6')`. Static presence of those actions does not prove that
|
||||
opening the panel would call them, but a settings-only initialization check
|
||||
would not establish safe threshold or suspend behavior on Frame. The plugin
|
||||
remains **broken**; no suspend, shutdown, notification, or live setting was
|
||||
exercised.
|
||||
|
||||
## CheatDeck 1.1.6, store ID 76
|
||||
|
||||
Archive SHA-256:
|
||||
`6dfe1f821b0cfe2a61c95e49efd6cb2587d79ff3d1e101e14fa80c12c7f6d89f`.
|
||||
The published README directs users to a Steam game context-menu item to choose
|
||||
a trainer and save launch options. The frontend references
|
||||
`SteamClient.Apps.SetAppLaunchOptions`; the Python backend provides settings
|
||||
RPCs. The existing opaque Chromium audit fails with `Cannot convert undefined
|
||||
or null to object`. The isolated Framey panel has no Steam game context-menu
|
||||
target, so repairing that initializer alone would not verify the advertised
|
||||
trainer workflow. The plugin remains **broken**; no launch-option write, game,
|
||||
trainer, real headset, or worn-VR action was exercised.
|
||||
|
||||
These observations only refine the reasons for the two existing catalog rows.
|
||||
The exact-version totals stay **6 works / 52 partial / 47 broken / 11 N/A**.
|
||||
@@ -0,0 +1,308 @@
|
||||
{
|
||||
"date": "2026-10-27",
|
||||
"scope": "Shared standalone Toggle value prop; no plugin-wide status promotion",
|
||||
"affected": [
|
||||
{
|
||||
"id": 7,
|
||||
"name": "CSS Loader",
|
||||
"version": "2.1.3",
|
||||
"sha256": "99beaf1374af82408e6d056e0b0147e5fca544e1035fa7ab88419231697bc231",
|
||||
"status": "partial"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "Pause Games",
|
||||
"version": "1.0.3",
|
||||
"sha256": "4aefdfb0ef25dea9c6d9fd037731c8b99ee8a1be966c2f7d5b0602112ef29f62",
|
||||
"status": "partial"
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"name": "AutoFlatpaks",
|
||||
"version": "1.6.8",
|
||||
"sha256": "344fce82cfb4800caccb4d6c0ec46e1ad6c9594a622d5baa0bb31286baf2d1a5",
|
||||
"status": "broken"
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"name": "DeckyFileServer",
|
||||
"version": "1.2.0",
|
||||
"sha256": "7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0",
|
||||
"status": "broken"
|
||||
},
|
||||
{
|
||||
"id": 64,
|
||||
"name": "Syncthing",
|
||||
"version": "0.2.7",
|
||||
"sha256": "27e47379c280c92253d5533435f57c33b332b215f866d2ac4493daf18ec0d1c4",
|
||||
"status": "broken"
|
||||
},
|
||||
{
|
||||
"id": 113,
|
||||
"name": "Decky LSFG-VK",
|
||||
"version": "0.12.2",
|
||||
"sha256": "ec59fc8aceb4e1abe2066b3a0707ed6313cabc4421b7c17765397903e2f8fd26",
|
||||
"status": "partial"
|
||||
},
|
||||
{
|
||||
"id": 116,
|
||||
"name": "Muon",
|
||||
"version": "0.2.1",
|
||||
"sha256": "a7659c79b74110476cc4458cb10530cdcb2539fa11bc8c759038a080eaa81049",
|
||||
"status": "broken"
|
||||
},
|
||||
{
|
||||
"id": 126,
|
||||
"name": "Launch Options",
|
||||
"version": "1.9.2",
|
||||
"sha256": "381ac8b915402176cf768d1ccc1bc29a6b82ea9255675f0a30c0c81ea5427225",
|
||||
"status": "broken"
|
||||
},
|
||||
{
|
||||
"id": 129,
|
||||
"name": "ShadowDeck",
|
||||
"version": "1.1.11",
|
||||
"sha256": "2b97b769a49ac2a45f546090fc96d07e09e146020104d708a0efdad990687f8b",
|
||||
"status": "broken"
|
||||
}
|
||||
],
|
||||
"browser": {
|
||||
"passed": true,
|
||||
"opaqueOrigin": "null",
|
||||
"checks": [
|
||||
"saved true renders checked",
|
||||
"standalone Toggle has no empty field wrapper",
|
||||
"native click writes boolean false and rerenders",
|
||||
"second click writes boolean true",
|
||||
"external false update renders unchecked",
|
||||
"external true update renders checked",
|
||||
"disabled Toggle stays inert",
|
||||
"ToggleField checked contract preserved"
|
||||
]
|
||||
},
|
||||
"publishedDeckyRuntime": {
|
||||
"version": "v3.3.0",
|
||||
"architecture": "x86_64",
|
||||
"loaderSha256": "e80d03e641d1766a33a817cb27ab8d2e0a4633685e5d27e4de948a6687526181",
|
||||
"totalProbes": 8,
|
||||
"pauseGames": {
|
||||
"id": 13,
|
||||
"name": "Pause Games",
|
||||
"version": "1.0.3",
|
||||
"sha256": "4aefdfb0ef25dea9c6d9fd037731c8b99ee8a1be966c2f7d5b0602112ef29f62",
|
||||
"shim": null,
|
||||
"calls": [
|
||||
{
|
||||
"method": "load_settings",
|
||||
"result": {}
|
||||
},
|
||||
{
|
||||
"method": "save_setting",
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"method": "load_settings",
|
||||
"result": {
|
||||
"autoPause": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "save_setting",
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"method": "load_settings",
|
||||
"result": {
|
||||
"autoPause": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "save_setting",
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"method": "load_settings",
|
||||
"result": {
|
||||
"autoPause": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "add_no_auto_pause_set",
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"method": "load_settings",
|
||||
"result": {
|
||||
"autoPause": true,
|
||||
"noAutoPauseSet": [
|
||||
424242
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "remove_no_auto_pause_set",
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"method": "load_settings",
|
||||
"result": {
|
||||
"autoPause": true,
|
||||
"noAutoPauseSet": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"discovered": [
|
||||
{
|
||||
"name": "Pause Games",
|
||||
"version": "1.0.3",
|
||||
"load_type": 1,
|
||||
"disabled": false
|
||||
}
|
||||
],
|
||||
"result": "discovery and Toggle-backed private settings persistence passed; no process actions"
|
||||
}
|
||||
},
|
||||
"realFrame": {
|
||||
"timeUtc": "2026-10-27T01:10:45.197435+00:00",
|
||||
"architecture": "aarch64",
|
||||
"runtime": {
|
||||
"version": "v3.3.0",
|
||||
"sha256": "8ef5dcf36cbbbb6e97201a6baed5d79c4cc44ec27789fc9af1c5b6ae8f0b25b6"
|
||||
},
|
||||
"backend": {
|
||||
"passed": true,
|
||||
"discovered": [
|
||||
{
|
||||
"name": "Pause Games",
|
||||
"version": "1.0.3",
|
||||
"load_type": 1,
|
||||
"disabled": false
|
||||
}
|
||||
],
|
||||
"privateSettingsPersisted": true,
|
||||
"calls": [
|
||||
{
|
||||
"method": "load_settings",
|
||||
"result": {}
|
||||
},
|
||||
{
|
||||
"method": "save_setting",
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"method": "load_settings",
|
||||
"result": {
|
||||
"autoPause": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "save_setting",
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"method": "load_settings",
|
||||
"result": {
|
||||
"autoPause": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "save_setting",
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"method": "load_settings",
|
||||
"result": {
|
||||
"autoPause": true,
|
||||
"pauseBeforeSuspend": false,
|
||||
"overlayPause": false,
|
||||
"noAutoPauseSet": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "add_no_auto_pause_set",
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"method": "load_settings",
|
||||
"result": {
|
||||
"autoPause": true,
|
||||
"pauseBeforeSuspend": false,
|
||||
"overlayPause": false,
|
||||
"noAutoPauseSet": [
|
||||
424242
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "remove_no_auto_pause_set",
|
||||
"result": null
|
||||
},
|
||||
{
|
||||
"method": "load_settings",
|
||||
"result": {
|
||||
"autoPause": true,
|
||||
"pauseBeforeSuspend": false,
|
||||
"overlayPause": false,
|
||||
"noAutoPauseSet": []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"batteryBefore": [
|
||||
100
|
||||
],
|
||||
"batteryAfter": [
|
||||
100
|
||||
],
|
||||
"originalServiceStatesPreserved": true,
|
||||
"cleanup": {
|
||||
"baseAbsent": true,
|
||||
"unitAbsent": true,
|
||||
"archiveAbsent": true
|
||||
},
|
||||
"lockReleased": true,
|
||||
"independentCleanupAudit": {
|
||||
"passed": true,
|
||||
"privateBasesUnitsUploadsAbsent": true,
|
||||
"privateUnitsNotLoaded": true
|
||||
}
|
||||
},
|
||||
"fakeFrame": {
|
||||
"blocked": 55,
|
||||
"installed-removed": 61
|
||||
},
|
||||
"python": {
|
||||
"ran": 1659,
|
||||
"skipped": 16,
|
||||
"result": "OK"
|
||||
},
|
||||
"unverified": [
|
||||
"Other importer plugin backends",
|
||||
"Process pause/resume",
|
||||
"Automatic pausing/suspend",
|
||||
"Physical controller/keyboard",
|
||||
"Worn VR"
|
||||
],
|
||||
"affectedSnapshot": "Initial independent verification before merging main b963937; DeckyFileServer is partial in the merged catalog.",
|
||||
"mergeValidation": {
|
||||
"mainCommit": "b963937",
|
||||
"catalog": {
|
||||
"works": 6,
|
||||
"partial": 51,
|
||||
"broken": 48,
|
||||
"na": 11
|
||||
},
|
||||
"python": {
|
||||
"ran": 1663,
|
||||
"skipped": 16,
|
||||
"result": "OK"
|
||||
},
|
||||
"publishedDeckyRuntime": {
|
||||
"version": "v3.3.0",
|
||||
"architecture": "x86_64",
|
||||
"probes": 10,
|
||||
"result": "All passed, including Pause Games and DeckyFileServer positive/negative cases"
|
||||
},
|
||||
"hardwareRerun": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# Shared standalone Toggle contract — 2026-10-27
|
||||
|
||||
Framey previously exported `DFL.Toggle` as `ToggleField`. Official standalone
|
||||
toggles pass `value`, while ToggleField takes `checked`. A saved true setting
|
||||
therefore appeared unchecked, and clicking it could send true again instead of
|
||||
turning it off. Toggle now renders a standalone controlled checkbox from
|
||||
`value`; ToggleField retains its `checked` contract. Both emit booleans and keep
|
||||
disabled controls inert. No official archive or manifest is rewritten.
|
||||
|
||||
Nine exact catalog archives use this standalone contract: CSS Loader (7),
|
||||
Pause Games (13), AutoFlatpaks (32), DeckyFileServer (60), Syncthing (64),
|
||||
Decky LSFG-VK (113), Muon (116), Launch Options (126), and ShadowDeck (129).
|
||||
These are affected API consumers, **not nine verified working plugins**.
|
||||
Their existing root, native, Steam UI and runtime blockers still apply.
|
||||
The catalog records this repair without promoting any status.
|
||||
|
||||
## Verification
|
||||
|
||||
- The Node regression fails against the old alias: true renders false. It
|
||||
passes with the fix and checks true/false callbacks, disabled behavior and
|
||||
the unchanged ToggleField contract.
|
||||
- The pinned, unmodified Pause Games 1.0.3 frontend component renders both
|
||||
persisted exclusion-list states and dispatches only its expected add/remove
|
||||
settings callbacks. This VM fixture does not run effects or process actions.
|
||||
- Local Chromium, React 18.3.1 and the production host in an opaque sandboxed
|
||||
iframe pass eight DOM checks: saved true, standalone shape, false/true native
|
||||
clicks and rerenders, external false/true updates, disabled input, and
|
||||
ToggleField. This is a control fixture, not nine end-to-end plugin tests.
|
||||
- Actual published Decky 3.3.0 on native amd64 discovers the exact Pause Games
|
||||
archive and uses public API1 instance RPC to save/read true/false settings,
|
||||
add/remove a synthetic exclusion and independently read its private settings
|
||||
file. This is a permanent runtime probe alongside existing backend shims;
|
||||
all eight probes, including historical negative cases, pass. Hosted CI also
|
||||
runs this gate on native ARM64; local ARM64 evidence below is from the Frame.
|
||||
- Under the shared headset lock, a bounded, removable private Decky 3.3.0 ARM64
|
||||
user service repeats discovery and those safe settings operations through
|
||||
the normal Steam CEF DeckyBackend bridge. The private file readback matches.
|
||||
The harness never requests pause/resume/kill or suspend operations. Steam's
|
||||
Decky frontend also populated its normal default setting keys during this
|
||||
run; no running-game behavior is claimed.
|
||||
- Fake-Frame audit: all 116 archives attempted, 61 installed/removed and 55
|
||||
blocked by existing policy. Full Python suite: 1,659 tests, 16 skipped, OK.
|
||||
|
||||
The exact Pause Games archive SHA-256 is
|
||||
`4aefdfb0ef25dea9c6d9fd037731c8b99ee8a1be966c2f7d5b0602112ef29f62`.
|
||||
The official store refresh still contains 116 entries; all pinned IDs, versions
|
||||
and digests are unchanged.
|
||||
[Structured results](framey-toggle-2026-10-27.json) include scoped readbacks and
|
||||
the nine version/digest pairs.
|
||||
|
||||
## Probe retries and cleanup
|
||||
|
||||
The first hardware adapter connected directly to Decky's single WebSocket and
|
||||
reused a request ID also used by Steam CEF; it received an unrelated cached
|
||||
reply. A second adapter retained a CEF connection across Decky's startup reload
|
||||
and disconnected. Neither is a successful plugin verification. Both private
|
||||
services and files were removed. The successful adapter waits for startup and
|
||||
reattaches CEF during discovery, then serializes settings calls through the
|
||||
normal DeckyBackend bridge.
|
||||
|
||||
All private services, bases and uploaded archives were removed, and the shared
|
||||
lock was released. An independent locked audit confirms no matching unit is
|
||||
loaded and no private base, unit file or upload remains. Original managed Decky
|
||||
and panel states were preserved. Battery stayed 100 → 100 during the successful
|
||||
run; no power setting changed.
|
||||
|
||||
No plugin gains a works claim. Real process control, other importer backends,
|
||||
physical input and worn VR remain unverified. The original independent branch
|
||||
had **6 works / 50 partial / 49 broken / 11 N/A**. After merging main's landed
|
||||
DeckyFileServer ARM64 support, current totals are **6 works / 51 partial /
|
||||
48 broken / 11 N/A**. Its managed-native receipt guard and both runtime probes
|
||||
are preserved; its former Toggle display limitation is removed from the current
|
||||
catalog reason. The initial verification counts above describe the original
|
||||
branch; combined validation is recorded below.
|
||||
|
||||
Combined merge validation against main `b963937`: full Python suite passes
|
||||
1,663 tests with 16 skips; all ten actual published Decky 3.3.0 native amd64
|
||||
probes pass, including Pause Games settings and DeckyFileServer positive/negative
|
||||
cases. Generated documentation matches the merged catalog. No hardware rerun
|
||||
was needed for these fixture/catalog conflicts.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Installer dependency fetch retry, 28 October 2026
|
||||
|
||||
The desktop installer build fetches pinned Python, platform-tools, CA and KDE
|
||||
Connect packages through `app/build/fetch-deps.js`. Before this change, one
|
||||
temporary connection drop or HTTP 503 ended the build. The iOS bundle's
|
||||
separate Python fetch hit a real remote disconnect in main CI run
|
||||
[40698195628](https://github.com/saphid/frame-control/actions/runs/40698195628);
|
||||
that path was repaired separately in PR #372.
|
||||
|
||||
The desktop fetch now retries transient transport errors and HTTP 408, 429 or
|
||||
5xx responses at most twice, with one- and two-second waits. HTTP 4xx errors
|
||||
other than 408/429 fail immediately. The existing pinned SHA-256 comparison
|
||||
still happens after download and before writing package bytes, so altered
|
||||
content fails without retry or installation.
|
||||
|
||||
Local Node 24 checks passed: retry recovery, exhausted retries, permanent HTTP
|
||||
error, HTTP 503 and a response aborted after partial bytes; all six app test
|
||||
files, script syntax and
|
||||
`git diff --check`. Hosted exact-head CI and installer preview remain required
|
||||
before merge/release. No installer was published, and this test makes no
|
||||
claim about a physical Mac or Windows install.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Main Windows test gate, 28 October 2026
|
||||
|
||||
Exact main commit `b5bbcec1a0cc3f8b3d25ef2686fd8315e209f167`
|
||||
([checks run 40723931638](https://github.com/saphid/frame-control/actions/runs/40723931638))
|
||||
failed in `server-tests (windows-latest, 3.12)`. Its server test step had one
|
||||
error: `test_settings_need_and_accept_the_ui_key` received Windows
|
||||
`ConnectionAbortedError [WinError 10053]` while reading an unauthorized
|
||||
POST response. The test sent a JSON body, but the missing-key guard rejects
|
||||
the request before reading it. Closing a connection with an unread body can
|
||||
reset it on Windows; that is the likely cause, rather than a settings write
|
||||
failure. The other Windows tests completed, and the exact-main native Decky
|
||||
runtime run passed.
|
||||
|
||||
The test now sends an empty POST when checking that a missing key is refused.
|
||||
Its subsequent authorized POST still sends a settings body and verifies the
|
||||
saved value. The local `tests/test_server.py` module passed 45 tests after
|
||||
this change. Hosted Windows CI on the repair commit and green exact-main CI
|
||||
remain required before further merges.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Published release manifest guard
|
||||
|
||||
The publisher previously accepted an already published release, deleted its existing `update.json`, then attempted to upload a replacement. If that upload failed, running copies would lose the live update manifest. The publisher now refuses a non-draft release before any asset deletion or upload.
|
||||
|
||||
The stand-in GitHub API regression provides a published release with an existing `update.json` and verifies that publication fails without any mutating API call. On 2026-10-28, `python3 -m unittest discover -s tests -p test_publish_release.py -q` passed all 12 tests; `git diff --check` passed. Hosted CI remains the merge gate. No GitHub release or update channel was changed by this test.
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"tag": "v0.4.50",
|
||||
"releaseId": 424993367,
|
||||
"tagNameAtAudit": "v0.4.50",
|
||||
"draftAtAudit": true,
|
||||
"sha256sumsSha256": "85383104a8f08f49ffde24ac867f9c2ead41951e602600a9d22d58bc19bf06f5",
|
||||
"assets": [
|
||||
{
|
||||
"name": "Frame-Control-mac-arm64.dmg",
|
||||
"size": 173090558,
|
||||
"sha256": "d402924394725bac2b28bef17c7945e2f7e4c3093227eace78a42ed80e864be9",
|
||||
"udifTrailer": "ok"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-mac-arm64.zip",
|
||||
"size": 172911785,
|
||||
"sha256": "2583937fefdc315706018c1bf794b916726ee55b461e321c3334df21245a110d",
|
||||
"zipCrc": "ok",
|
||||
"members": 1666,
|
||||
"bundleVersion": "0.4.50"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-mac-x64.dmg",
|
||||
"size": 180139223,
|
||||
"sha256": "2128c14b15dd1a2a59790c43d42640a25185562056796a3437910d6f88f996c8",
|
||||
"udifTrailer": "ok"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-mac-x64.zip",
|
||||
"size": 179998207,
|
||||
"sha256": "ed684023ff087d6741e88d4ca4f8a47e67bec07255fc73fbd18a6bcaa42d14fe",
|
||||
"zipCrc": "ok",
|
||||
"members": 1662,
|
||||
"bundleVersion": "0.4.50"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-Setup-x64.exe",
|
||||
"size": 147348168,
|
||||
"sha256": "7c46fcc759cefb9060e1dc2fce39682f6dad306f3666805f77ce707e16ef3e2c",
|
||||
"peMachine": 332
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-win-x64.zip",
|
||||
"size": 194302082,
|
||||
"sha256": "57e71304bae96dc8aab6b845a64c27ac428931d02bb41ae60b33b19fcd11193f",
|
||||
"zipCrc": "ok",
|
||||
"members": 1510
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-linux-x86_64.AppImage",
|
||||
"size": 175453095,
|
||||
"sha256": "d2196b9a50d0777203e60721dd19c6edbb5c18a7639148f0a496d420e231b053",
|
||||
"elfMachine": 62
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-linux-arm64.AppImage",
|
||||
"size": 167036625,
|
||||
"sha256": "49370f9c27931bf6c28676d627dc34553550b544ab38b7f4be8e8f6dbc9b7c4b",
|
||||
"elfMachine": 183
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-linux-amd64.deb",
|
||||
"size": 142209572,
|
||||
"sha256": "430d057f234557f5f9dbb3e2cff3469829d041d0d7be191e70e6ef7de6663256",
|
||||
"debVersion": "0.4.50",
|
||||
"architecture": "amd64"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-linux-arm64.deb",
|
||||
"size": 129291236,
|
||||
"sha256": "6403f8caafcdd5832415547e146ce3886cf7358e0c3a8898f4ee6e9a0a7b05ea",
|
||||
"debVersion": "0.4.50",
|
||||
"architecture": "arm64"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
# v0.4.50 candidate gate, 27 October 2026
|
||||
|
||||
The version bump at `a160043` follows the merged Windows panel-response repair
|
||||
[#356](https://github.com/saphid/frame-control/pull/356) and the Framey changes
|
||||
[#353](https://github.com/saphid/frame-control/pull/353),
|
||||
[#354](https://github.com/saphid/frame-control/pull/354),
|
||||
[#355](https://github.com/saphid/frame-control/pull/355),
|
||||
[#357](https://github.com/saphid/frame-control/pull/357),
|
||||
[#358](https://github.com/saphid/frame-control/pull/358), and
|
||||
[#359](https://github.com/saphid/frame-control/pull/359).
|
||||
The exact DeckyFileServer catalog entry is **partial**; the shared Toggle fix
|
||||
does not promote any plugin status. Worn VR and physical controller behavior
|
||||
remain unverified.
|
||||
|
||||
## Locked real Frame smoke
|
||||
|
||||
Early `scripts/frame-smoke.sh` attempts exited before any headset read or test
|
||||
action because lxso2's Tailnet route was down and lxso1 lacked the authorized
|
||||
SSH key and pinned host record. No lock was taken by those attempts. The final
|
||||
run kept lxso2's existing private key and pinned Frame host key end to end,
|
||||
using lxso1 only as a TCP relay. A task-owned localhost tunnel reached the
|
||||
devkit endpoint after the runner acquired `/tmp/frame-test.lock`; no global
|
||||
SSH, network or security setting was changed.
|
||||
|
||||
At **02:25:14 UTC**, `scripts/frame-smoke.sh` ran from candidate commit
|
||||
`fcae5d6` on Frame OS **0.5.6**, build `20261022.6153909`. Its locked
|
||||
read-only preflight found SteamVR activity **3 (standby)**, proximity raw
|
||||
**3.25** and battery **100%**. The result was **12 passed / one optional
|
||||
x86_64 launch skipped / zero failed**. ARM64 and Windows/Proton test titles
|
||||
installed, launched and were removed. The x86_64 title installed and was
|
||||
removed, but Steam Linux Runtime 4 was absent, so no x86_64 launch is claimed.
|
||||
Both the initial leftover sweep and final owned cleanup passed; the runner
|
||||
released only its lock. The private result is
|
||||
`/tmp/frame-control-v0450-smoke-results/2026-10-27T022514Z-20261022.6153909.json`
|
||||
(SHA-256 `7b8d88aafb4459bf882edeb10f65c021c5aa25ad9ed2755faf04e3b4a95c1632`).
|
||||
|
||||
A separate read-only audit acquired a fresh unique lock after the smoke. It
|
||||
found activity **3**, proximity raw **3.25**, battery **100%**, no exact reserved
|
||||
`fc_smoke_*` or `fc-smoke-*` title directories or metadata, and no processes
|
||||
owned by those paths. That audit released its own lock. The local tunnel was
|
||||
stopped; the isolated lxso1 checkout and temporary SSH files were removed.
|
||||
There was no wearer confirmation, wake command or worn interaction test.
|
||||
|
||||
## Remaining release gates
|
||||
|
||||
The candidate needs green exact-head PR CI, e2e, native amd64/ARM64 Decky
|
||||
runtime checks and all four installer previews. After the candidate merges,
|
||||
its exact main commit needs green CI and e2e. The tagged release then needs
|
||||
every installer build, ten assets, matching checksums, release verification,
|
||||
independent package checks and publisher dry run. A failed, cancelled, skipped,
|
||||
pending or unverified release gate holds publication. The current published
|
||||
release remains v0.4.49.
|
||||
@@ -0,0 +1,75 @@
|
||||
# v0.4.50 tagged release verification, 27 October 2026
|
||||
|
||||
The annotated `v0.4.50` tag dereferences to merged main commit
|
||||
`2797212d8c95311deb8ba73043d0f90ba10bc962`. Its tree exactly matched
|
||||
the reviewed [candidate PR #360](https://github.com/saphid/frame-control/pull/360)
|
||||
head `dab13fa8763b4f836cd49d398d152124655ea8dc`. The candidate includes
|
||||
the Framey and Windows changes listed in the public release notes and the
|
||||
publisher gate that checks every workflow run attempt.
|
||||
|
||||
## Exact commit and tagged build gates
|
||||
|
||||
- [Main checks](https://github.com/saphid/frame-control/actions/runs/40440862876)
|
||||
completed successfully on the tag commit: seven of seven jobs, including
|
||||
e2e, iOS, Windows, macOS, Linux and the aggregate gate.
|
||||
- [Native Decky runtime](https://github.com/saphid/frame-control/actions/runs/40440862867)
|
||||
completed successfully on that commit: AMD64 and ARM64 actual Decky 3.3.0
|
||||
discovery and safe probes.
|
||||
- An exact-main [installer preview](https://github.com/saphid/frame-control/actions/runs/40441532929)
|
||||
built Windows, Linux, Apple Silicon Mac and Intel Mac installers successfully
|
||||
before tagging. Its draft and checksum jobs were inapplicable to the manual
|
||||
preview and skipped by their tag-only conditions; they were not release gates.
|
||||
- The [exact-tag release workflow](https://github.com/saphid/frame-control/actions/runs/40442228066)
|
||||
then passed all six jobs on its first attempt: draft, four platform builds,
|
||||
and checksums. No tagged job failed, was cancelled, skipped or overridden.
|
||||
|
||||
The draft contained exactly ten installer assets and `SHA256SUMS` before
|
||||
publication. Every installer was independently downloaded by GitHub asset ID,
|
||||
one file at a time. Its downloaded size and SHA-256 matched GitHub's asset
|
||||
metadata and the downloaded `SHA256SUMS`; the latter's SHA-256 was
|
||||
`85383104a8f08f49ffde24ac867f9c2ead41951e602600a9d22d58bc19bf06f5`.
|
||||
The [sanitized audit result](v0450-assets-2026-10-27.json) records all ten
|
||||
names, sizes, hashes and package checks. The audit also checked both Mac ZIP
|
||||
CRCs and bundle version `0.4.50`, both DMG UDIF trailers, the Windows ZIP
|
||||
CRC and PE installer header, both AppImage ELF architectures, and both DEB
|
||||
version and architecture fields. The Windows NSIS installer itself has a
|
||||
32-bit PE launcher; tagged Windows CI installed its x64 payload and checked
|
||||
packaged resources and Python libraries.
|
||||
|
||||
The independently downloaded Linux x86_64 AppImage had SHA-256
|
||||
`d2196b9a50d0777203e60721dd19c6edbb5c18a7639148f0a496d420e231b053`.
|
||||
Its first local launch exited inside the workspace sandbox before the server
|
||||
answered. The same bytes then passed outside that sandbox under isolated Xvfb:
|
||||
HTTP 200 with `FrameControl/1`, followed by a stopped server. The launcher
|
||||
used a temporary profile and disabled telemetry and update checks. Tagged
|
||||
Linux CI also launched its own built AppImage. No physical Mac or Windows
|
||||
launch of these exact installers was performed.
|
||||
|
||||
## Real Frame and publication
|
||||
|
||||
The [candidate evidence](v0450-candidate-2026-10-27.md) records a unique-lock
|
||||
real Frame smoke on the version-bumped source: **12 passed, one optional
|
||||
x86_64 launch skipped, zero failed**. Steam Linux Runtime 4 was absent, so no
|
||||
x86_64 launch is claimed. A separate fresh-lock cleanup audit found no owned
|
||||
test title directories, metadata or processes; both locks were released.
|
||||
SteamVR activity was 3 (standby), proximity raw 3.25 and battery 100%.
|
||||
There was no wearer confirmation, wake command or worn interaction test.
|
||||
The private smoke result's SHA-256 is
|
||||
`7b8d88aafb4459bf882edeb10f65c021c5aa25ad9ed2755faf04e3b4a95c1632`.
|
||||
|
||||
`scripts/publish-release.sh --dry-run v0.4.50` passed the exact main,
|
||||
native runtime and tagged release jobs, all ten installer hashes, regenerated
|
||||
`SHA256SUMS`, draft notes and update-manifest checks. The real publisher
|
||||
repeated those gates, attached `update.json`, and published
|
||||
[v0.4.50](https://github.com/saphid/frame-control/releases/tag/v0.4.50) at
|
||||
**03:19:21 UTC**. GitHub's latest-release API returned `draft=false`,
|
||||
`prerelease=false`, ten installers, `SHA256SUMS` and `update.json`. A fresh
|
||||
public latest-download of `update.json` reported version `0.4.50`, the
|
||||
correct release page, ten hashes matching the independent downloads, and the
|
||||
worn-headset limit in its notes.
|
||||
|
||||
The exact-version official Decky catalog is **6 works / 51 partial /
|
||||
48 broken / 11 N/A**. These statuses have the scoped evidence in
|
||||
[the compatibility table](../framey-compatibility.md). Worn headset
|
||||
visibility, physical controller input, VR keyboard and optical comfort remain
|
||||
unverified. The hourly shared-lock worn check remains enabled.
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"tag": "v0.4.51",
|
||||
"releaseId": 425049939,
|
||||
"tagNameAtAudit": "v0.4.51",
|
||||
"draftAtAudit": true,
|
||||
"sha256sumsSha256": "9d28eae8610356cba56310c892b6dd16dfc55c0507c9089600ab86e9697a04f8",
|
||||
"assets": [
|
||||
{
|
||||
"name": "Frame-Control-mac-arm64.dmg",
|
||||
"size": 177621496,
|
||||
"sha256": "8c84ef490402c202ad694f9d363d792c5dbc9b046e40d8d834bcb6d7752ab6e8",
|
||||
"udifTrailer": "ok"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-mac-arm64.zip",
|
||||
"size": 177449210,
|
||||
"sha256": "3541b67985e3931b9469af9249df3f71133589d726ffbd7b0c2b9c1c3dd3f67c",
|
||||
"zipCrc": "ok",
|
||||
"members": 1673,
|
||||
"bundleVersion": "0.4.51"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-mac-x64.dmg",
|
||||
"size": 184691853,
|
||||
"sha256": "04fecaf012e6ab4ca139823d2220bf61b788568cdffbfdad22b5d0b2c2b63763",
|
||||
"udifTrailer": "ok"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-mac-x64.zip",
|
||||
"size": 184535648,
|
||||
"sha256": "7e9836d62f882fbaac3d9b35831410c5e6138d9b68fb93815cbd2bb9200bd46b",
|
||||
"zipCrc": "ok",
|
||||
"members": 1669,
|
||||
"bundleVersion": "0.4.51"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-Setup-x64.exe",
|
||||
"size": 151882885,
|
||||
"sha256": "a45996f621901c2df8fc41d9eca2ae3fcd6b9ff40de66ed8acfca9415a3a81b0",
|
||||
"peMachine": 332
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-win-x64.zip",
|
||||
"size": 198837858,
|
||||
"sha256": "41791b6ca2c40164eb44cdc29e1281886556c257b88bcd5f86941daddd15d66a",
|
||||
"zipCrc": "ok",
|
||||
"members": 1517
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-linux-x86_64.AppImage",
|
||||
"size": 179993078,
|
||||
"sha256": "5ebbb931276cd3126cb3ecab88c6cde05f490cbd0e0205b418c32d00f765ea42",
|
||||
"elfMachine": 62
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-linux-arm64.AppImage",
|
||||
"size": 171579329,
|
||||
"sha256": "4205422b02b9207bf2cf59ce3eb847b601306cef326df9fb005a7df540d82bb5",
|
||||
"elfMachine": 183
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-linux-amd64.deb",
|
||||
"size": 146747460,
|
||||
"sha256": "7c3a4eae7fb2098c759cbd9a8194c326b70f2fa078030062c047e4c8418b4bef",
|
||||
"debVersion": "0.4.51",
|
||||
"architecture": "amd64"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-linux-arm64.deb",
|
||||
"size": 133829576,
|
||||
"sha256": "793433d21e457d5422ea982678f50d5f3ac0c1c46d40c5dcebf42d16651d4995",
|
||||
"debVersion": "0.4.51",
|
||||
"architecture": "arm64"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
# v0.4.51 candidate gate, 27 October 2026
|
||||
|
||||
The source candidate `91cf774` follows green main merge commit
|
||||
`560f3de5631ce2dca641c68a5cba7b2dd1d2660f`. The main
|
||||
[checks](https://github.com/saphid/frame-control/actions/runs/40450100545)
|
||||
and [native Decky runtime](https://github.com/saphid/frame-control/actions/runs/40450100462)
|
||||
passed on that exact merge commit. This candidate bumps the app and lockfile
|
||||
to 0.4.51 and requires standby-range proximity before the release smoke can
|
||||
change test titles.
|
||||
|
||||
Since v0.4.50, [#362](https://github.com/saphid/frame-control/pull/362)
|
||||
recorded the pinned official LocalSend source match, and
|
||||
[#363](https://github.com/saphid/frame-control/pull/363) added an exact-digest
|
||||
managed ARM64 helper, desktop/headset packaging, and a scoped **partial**
|
||||
compatibility entry. Its PR head passed e2e, native Decky AMD64/ARM64, and all
|
||||
four installer previews. The catalog is **6 works / 52 partial / 47 broken /
|
||||
11 N/A**. A private no-route ARM64 helper marker transfer passed on the real
|
||||
Frame, but the managed Decky service on Frame, user-facing transfer, local
|
||||
discovery, Steam routes, worn VR and physical controllers remain unverified.
|
||||
|
||||
## Locked real Frame smoke
|
||||
|
||||
At 04:33:21 UTC, `scripts/frame-smoke.sh` ran from source candidate `91cf774`
|
||||
with a unique-owner atomic `/tmp/frame-test.lock`. Its read-only preflight
|
||||
found Frame OS 0.5.6 build `20261022.6153909`, SteamVR activity **3
|
||||
(standby)**, proximity raw **4.6875**, and battery **88%**. It passed **12
|
||||
steps**, skipped one optional x86_64 launch, and failed **zero**. ARM64 and
|
||||
Windows/Proton test titles installed, launched and were removed. The x86_64
|
||||
title installed and was removed; Steam Linux Runtime 4 was absent, so no
|
||||
x86_64 launch is claimed. The runner's owned cleanup passed and it released
|
||||
only its lock. The private result is
|
||||
`/tmp/frame-control-v0451-smoke-results/2026-10-27T043321Z-20261022.6153909.json`
|
||||
(SHA-256 `c2b9611e072fd08109c31f4aa05a4ba6b93de82f532a7d87a4a1c6dcd52024a3`).
|
||||
|
||||
A separate read-only audit acquired a fresh unique lock after the smoke. It
|
||||
found activity **3**, proximity raw **4.6875**, battery **91%** while charging,
|
||||
no exact reserved smoke title files or metadata, and no processes owned by
|
||||
those title paths. It released its own lock. The private audit result is
|
||||
`/tmp/frame-control-v0451-cleanup-audit.json` (SHA-256
|
||||
`3953072bb5457f4fef2cb766d83f65f844896fc7893d2479a166cb9325f72b48`).
|
||||
No power setting or security state was changed. No wearer was confirmed and no
|
||||
worn panel or controller interaction was tested; the hourly worn check remains
|
||||
enabled.
|
||||
|
||||
## Remaining release gates
|
||||
|
||||
The candidate needs green exact-head PR CI, e2e, native Decky runtime and all
|
||||
four installer previews. After the candidate merges, its exact main commit
|
||||
needs green CI and e2e. The tagged release then needs every installer build,
|
||||
ten assets, matching checksums, release verification, independent package
|
||||
checks and publisher dry run. A failed, cancelled, skipped, pending or
|
||||
unverified release gate holds publication. The current published release
|
||||
remains v0.4.50.
|
||||
@@ -0,0 +1,75 @@
|
||||
# v0.4.51 tagged release verification, 27 October 2026
|
||||
|
||||
The annotated `v0.4.51` tag dereferences to merged main commit
|
||||
`4da7220660ba345bc46cdb525b6c512bc6a9d8bd`. Its tree
|
||||
`16aa888207e8661280aa659c96a08fee05f17689` exactly matches reviewed
|
||||
[candidate PR #364](https://github.com/saphid/frame-control/pull/364) head
|
||||
`728a4dbe9b049e5f5cfcd6de02d9ca858d3e9aa4`. The candidate source
|
||||
passed the locked smoke described in
|
||||
[its evidence](v0451-candidate-2026-10-27.md).
|
||||
|
||||
## Exact commit and tagged build gates
|
||||
|
||||
- [Main checks](https://github.com/saphid/frame-control/actions/runs/40451752952)
|
||||
passed on the tag commit: all seven jobs on the first attempt, including
|
||||
e2e, iOS, Windows, macOS, Linux and the aggregate check.
|
||||
- [Native Decky runtime](https://github.com/saphid/frame-control/actions/runs/40451752953)
|
||||
passed on the tag commit: both published Decky 3.3.0 AMD64 and ARM64 jobs.
|
||||
- An exact-main [installer preview](https://github.com/saphid/frame-control/actions/runs/40451826490)
|
||||
built all four platform installers successfully before tagging. Its draft
|
||||
and checksum jobs were inapplicable to manual preview runs.
|
||||
- The [exact-tag release workflow](https://github.com/saphid/frame-control/actions/runs/40452627862)
|
||||
passed all six jobs on its first attempt: draft, Linux, Windows, Intel Mac,
|
||||
Apple Silicon Mac and checksums. No tagged job failed, was cancelled,
|
||||
skipped or overridden.
|
||||
|
||||
The unpublished draft held exactly ten installers and `SHA256SUMS` before
|
||||
publication. Every installer was independently downloaded by GitHub asset ID,
|
||||
one at a time. Each downloaded size and SHA-256 matched GitHub's asset
|
||||
metadata; the downloaded `SHA256SUMS` matched all ten entries byte for byte.
|
||||
Its SHA-256 was
|
||||
`9d28eae8610356cba56310c892b6dd16dfc55c0507c9089600ab86e9697a04f8`.
|
||||
The [sanitized asset audit](v0451-assets-2026-10-27.json) records all ten
|
||||
names, sizes, hashes and package checks. It checked both Mac ZIP CRCs and
|
||||
bundle version `0.4.51`, both DMG UDIF trailers, Windows ZIP CRC and PE
|
||||
installer header, both AppImage ELF architectures, and both DEB version and
|
||||
architecture fields.
|
||||
|
||||
The independently downloaded Linux x86_64 AppImage had SHA-256
|
||||
`5ebbb931276cd3126cb3ecab88c6cde05f490cbd0e0205b418c32d00f765ea42`.
|
||||
It launched under isolated Xvfb with a temporary profile, telemetry and
|
||||
update checks disabled. Its bundled server returned HTTP 200 with
|
||||
`FrameControl/1` and stopped after app exit. No physical Mac or Windows
|
||||
launch of these exact installers was performed.
|
||||
|
||||
## Real Frame and publication
|
||||
|
||||
The [candidate evidence](v0451-candidate-2026-10-27.md) records a unique-lock
|
||||
real Frame smoke on the version-bumped source: **12 passed, one optional
|
||||
x86_64 launch skipped, zero failed**. Steam Linux Runtime 4 was absent, so no
|
||||
x86_64 launch is claimed. A separate fresh-lock audit found no reserved smoke
|
||||
title files, metadata or owned processes; both locks were released. SteamVR
|
||||
activity was 3 (standby), proximity raw 4.6875, battery 88% before and 91%
|
||||
while charging at audit. There was no wearer confirmation, wake command or
|
||||
worn interaction test. The private smoke result's SHA-256 is
|
||||
`c2b9611e072fd08109c31f4aa05a4ba6b93de82f532a7d87a4a1c6dcd52024a3`.
|
||||
|
||||
`scripts/publish-release.sh --dry-run v0.4.51` passed exact-main, native
|
||||
runtime and tagged release jobs, all ten installer hashes, regenerated
|
||||
`SHA256SUMS`, draft notes and update-manifest checks. The real publisher
|
||||
repeated those gates, attached `update.json`, and published
|
||||
[v0.4.51](https://github.com/saphid/frame-control/releases/tag/v0.4.51) at
|
||||
**05:10:05 UTC**. GitHub's latest-release API returned `draft=false`,
|
||||
`prerelease=false`, ten installers, `SHA256SUMS` and `update.json`. A fresh
|
||||
public latest-download of `update.json` reported version `0.4.51`, the correct
|
||||
release page, ten hashes matching the independent downloads, and the worn
|
||||
limit in its notes. The public manifest's SHA-256 was
|
||||
`27bc0e45acf6b9a5677639e66ab501f2acc3eb7f5c0f4f867ced8a5d11825c52`.
|
||||
|
||||
The exact-version Decky catalog is **6 works / 52 partial / 47 broken /
|
||||
11 N/A**. Managed Decky Localsend is **partial**: real Frame testing proved
|
||||
only its rebuilt helper's private marker transfer, while the published
|
||||
Decky 3.3.0 runtime gate proved discovery and stopped status/configuration
|
||||
RPCs. Real managed-service operation, user-facing transfer, network
|
||||
discovery, Steam routes, worn visibility and physical-controller input remain
|
||||
unverified. The hourly shared-lock worn check remains enabled.
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"tag": "v0.4.52",
|
||||
"releaseId": 426854020,
|
||||
"tagNameAtAudit": "v0.4.52",
|
||||
"draftAtAudit": true,
|
||||
"sha256sumsSha256": "71306c21818a8842f60633540a16457e962e9ffc7fac01a547a5a2fd17f32904",
|
||||
"assets": [
|
||||
{
|
||||
"name": "Frame-Control-mac-arm64.dmg",
|
||||
"size": 177628410,
|
||||
"sha256": "2dad765390965eaa01ab9a1737bffddcb6c8d481e4d255322e663bcdb7ad1e30",
|
||||
"udifTrailer": "ok"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-mac-arm64.zip",
|
||||
"size": 177451653,
|
||||
"sha256": "abb17c3e3de2ada967d10b7e277c9f122fb731108efc6d70fed9b87e92e66106",
|
||||
"zipCrc": "ok",
|
||||
"members": 1673,
|
||||
"bundleVersion": "0.4.52"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-mac-x64.dmg",
|
||||
"size": 184701118,
|
||||
"sha256": "1317ee7b30de46bc69a19d42d68245dbf55ca02fbc6ceee5761534ddb8706bab",
|
||||
"udifTrailer": "ok"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-mac-x64.zip",
|
||||
"size": 184538083,
|
||||
"sha256": "4d0fbf1904073a7b28d0e968013bd7ca34600536538db32d00b03cf6aaf5b3b4",
|
||||
"zipCrc": "ok",
|
||||
"members": 1669,
|
||||
"bundleVersion": "0.4.52"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-Setup-x64.exe",
|
||||
"size": 151885028,
|
||||
"sha256": "bb190c20abb2fae1878b732819ae64c214c0cfd627c3a6279b25e9d9c0517740",
|
||||
"peMachine": 332
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-win-x64.zip",
|
||||
"size": 198840260,
|
||||
"sha256": "ef7d248967bda011951bd703972f3a1cb00a43e484af0daf58a5f754e2f2486c",
|
||||
"zipCrc": "ok",
|
||||
"members": 1517
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-linux-x86_64.AppImage",
|
||||
"size": 179997222,
|
||||
"sha256": "6241e068222a2c05f41946e94dedb1456dce25ae94d4857bcb7a4d1891feac7d",
|
||||
"elfMachine": 62
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-linux-arm64.AppImage",
|
||||
"size": 171579364,
|
||||
"sha256": "27da455144f8c88ba6a56d6eed90e1806c098d5f6806790089513ba77ec79f52",
|
||||
"elfMachine": 183
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-linux-amd64.deb",
|
||||
"size": 146749772,
|
||||
"sha256": "97425cbd4d460460b9f0574519bbf11afa1b8cc8003e1b784d6c002244da4075",
|
||||
"debVersion": "0.4.52",
|
||||
"architecture": "amd64"
|
||||
},
|
||||
{
|
||||
"name": "Frame-Control-linux-arm64.deb",
|
||||
"size": 133832104,
|
||||
"sha256": "58c98ed203caca60ce022c90232c13591d11c72ff5b28c5860aa7b221ec3e918",
|
||||
"debVersion": "0.4.52",
|
||||
"architecture": "arm64"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
# v0.4.52 candidate gate, 28 October 2026
|
||||
|
||||
This candidate bumps the desktop app and lockfile to 0.4.52. It contains
|
||||
installer-fetch [PR #374](https://github.com/saphid/frame-control/pull/374),
|
||||
merged to main at `5847a810667724ef423f278a83650f9f1c6d18b5`. Its exact
|
||||
reviewed head `082af24115a87b3bf11a77c2cf93b5bcdd3b4409` passed
|
||||
[CI and e2e](https://github.com/saphid/frame-control/actions/runs/40706672771),
|
||||
[native Decky](https://github.com/saphid/frame-control/actions/runs/40706672854)
|
||||
and [all four installer previews](https://github.com/saphid/frame-control/actions/runs/40706672774).
|
||||
The merge commit's own CI is pending. No release has been published from this
|
||||
branch. The versioned source candidate tested below was
|
||||
`c88df07c816f1475268760add93a5ab072463688`.
|
||||
|
||||
Since v0.4.51, main added the exact-archive Network Sentinel and Battery
|
||||
Tracker Framey adaptations and CI/release safety fixes. PR #374 retries
|
||||
transient installer dependency failures, including responses aborted after
|
||||
partial bytes; pinned SHA-256 verification remains required before writing a
|
||||
download. The exact-version official Decky catalog is **6 works / 54 partial /
|
||||
45 broken / 11 N/A**. Network Sentinel and Battery Tracker are partial;
|
||||
managed real Frame operation, worn visibility and physical-controller input
|
||||
remain unverified.
|
||||
|
||||
## Locked real Frame smoke
|
||||
|
||||
At 14:42:13 UTC, `scripts/frame-smoke.sh` ran from the versioned source
|
||||
candidate under its unique-owner atomic `/tmp/frame-test.lock`. Its read-only
|
||||
preflight found Frame OS 0.5.6 build `20261022.6153909`, SteamVR activity
|
||||
**3 (standby)**, proximity raw **2.3125**, and battery **100%**. ARM64 and
|
||||
Windows/Proton test titles installed, launched and were removed. The x86_64
|
||||
title installed and was removed; its optional launch was skipped because
|
||||
Steam Linux Runtime 4 was absent. The result was **12 passed, one optional
|
||||
skip, zero failed**. Owned cleanup passed and the runner released only its
|
||||
lock. The private result is
|
||||
`/tmp/frame-control-v0452-smoke-results/2026-10-28T144213Z-20261022.6153909.json`
|
||||
(SHA-256 `42ea66740d001cc097ce9e871be6b01be1a10f184ab4480019b0a306851cfc57`).
|
||||
|
||||
A separate read-only audit acquired a fresh unique lock after smoke. It
|
||||
found activity **3**, proximity raw **2.3125**, battery **100%**, no exact
|
||||
reserved smoke title files or metadata, and no processes owned by those
|
||||
title paths. It released its own lock. The private audit result is
|
||||
`/tmp/frame-v0452-cleanup-audit.json` (SHA-256
|
||||
`daebfb5c02d6cf59e5cee092e3fa90a07b3dc9f105e167d46a4f4327c541c7ce`).
|
||||
No power setting or security state was changed. No wearer was confirmed; no
|
||||
worn panel, physical-controller or official plugin interaction was tested.
|
||||
The hourly worn check remains enabled.
|
||||
|
||||
## Remaining release gates
|
||||
|
||||
- Main's #374 merge commit needs green exact-commit CI, e2e and native Decky
|
||||
runtime before the candidate can merge.
|
||||
- This candidate needs its own exact-head CI, e2e, native Decky runtime and
|
||||
all four installer previews after its sync with the final main merge.
|
||||
- The locked source smoke and independent cleanup audit above passed. Their
|
||||
optional x86_64 runtime skip does not prove that launch; no worn display or
|
||||
physical-controller claim follows from standby smoke.
|
||||
- After candidate merge, the exact main commit needs green CI, e2e and native
|
||||
Decky runtime. The annotated tag then needs all four installer builds and
|
||||
checksums on that same commit, independent downloads and package checks,
|
||||
release verification and a publisher dry run. Any failed, cancelled,
|
||||
skipped, pending or unverified required gate holds publication.
|
||||
|
||||
The published release and update channel remain at v0.4.51 until all gates
|
||||
pass and the draft is explicitly published.
|
||||
@@ -0,0 +1,70 @@
|
||||
# v0.4.52 tagged release verification, 28 October 2026
|
||||
|
||||
The annotated `v0.4.52` tag dereferences to merged main commit
|
||||
`ac04407bcc799e9e88ce31c3f602020e9f3b1cee`. Its tree
|
||||
`864fde2ec006afe00eb886279af9361a6e3b657e` exactly matches reviewed
|
||||
[candidate PR #375](https://github.com/saphid/frame-control/pull/375) head
|
||||
`432b29f9618d1e300abdf8437a2bd8360d54cffb`. The candidate's locked
|
||||
source smoke and cleanup are recorded in [its evidence](v0452-candidate-2026-10-28.md).
|
||||
|
||||
## Exact commit and tagged build gates
|
||||
|
||||
- [Main checks](https://github.com/saphid/frame-control/actions/runs/40714882563)
|
||||
passed on the tag commit: all seven jobs on the first attempt, including
|
||||
e2e, iOS, Windows, macOS, Linux and the aggregate check.
|
||||
- [Native Decky runtime](https://github.com/saphid/frame-control/actions/runs/40714882617)
|
||||
passed on the tag commit: both published Decky 3.3.0 AMD64 and ARM64 jobs
|
||||
on the first attempt.
|
||||
- The [exact-tag release workflow](https://github.com/saphid/frame-control/actions/runs/40717151326)
|
||||
passed all six jobs on the first attempt: draft, Linux, Windows, Intel Mac,
|
||||
Apple Silicon Mac and checksums. No tagged job failed, was cancelled,
|
||||
skipped or overridden.
|
||||
|
||||
Before publication, the unpublished draft held exactly ten installers and
|
||||
`SHA256SUMS`. Every installer was independently downloaded by GitHub asset
|
||||
ID. Each downloaded size and SHA-256 matched the asset metadata; the
|
||||
downloaded `SHA256SUMS` matched all ten entries byte for byte. Its SHA-256 was
|
||||
`71306c21818a8842f60633540a16457e962e9ffc7fac01a547a5a2fd17f32904`.
|
||||
The [sanitized asset audit](v0452-assets-2026-10-28.json) records all ten
|
||||
names, sizes, hashes and package checks. It checked both Mac ZIP CRCs and
|
||||
bundle version `0.4.52`, both DMG UDIF trailers, Windows ZIP CRC and PE
|
||||
installer header, both AppImage ELF architectures, and both DEB version and
|
||||
architecture fields.
|
||||
|
||||
The independently downloaded Linux x86_64 AppImage had SHA-256
|
||||
`6241e068222a2c05f41946e94dedb1456dce25ae94d4857bcb7a4d1891feac7d`.
|
||||
It launched under isolated Xvfb with a temporary profile, telemetry and
|
||||
update checks disabled. Its bundled server returned HTTP 200 with
|
||||
`FrameControl/1` and stopped after app exit. No physical Mac or Windows
|
||||
launch of these exact installers was performed.
|
||||
|
||||
## Real Frame and publication
|
||||
|
||||
The [candidate evidence](v0452-candidate-2026-10-28.md) records a unique-lock
|
||||
real Frame smoke on the version-bumped source: **12 passed, one optional
|
||||
x86_64 launch skipped, zero failed**. Steam Linux Runtime 4 was absent, so
|
||||
no x86_64 launch is claimed. A separate fresh-lock audit found no reserved
|
||||
smoke title files, metadata or owned processes; both locks were released.
|
||||
SteamVR activity was 3 (standby), proximity raw 2.3125 and battery 100%.
|
||||
There was no wearer confirmation, wake command or worn interaction test.
|
||||
The private smoke result's SHA-256 is
|
||||
`42ea66740d001cc097ce9e871be6b01be1a10f184ab4480019b0a306851cfc57`.
|
||||
|
||||
`scripts/publish-release.sh --dry-run v0.4.52` passed exact-main, native
|
||||
runtime and tagged release jobs, all ten installer hashes, regenerated
|
||||
`SHA256SUMS`, draft notes and update-manifest checks. Its local log had
|
||||
SHA-256 `d4264b5ad3106387b63d3fc800f558c912ff36b546ea83d792a5a0e8a4949cc6`.
|
||||
The real publisher repeated those gates, attached `update.json`, and published
|
||||
[v0.4.52](https://github.com/saphid/frame-control/releases/tag/v0.4.52) at
|
||||
**15:52:29 UTC**. GitHub's latest-release API returned `draft=false`, ten
|
||||
installers, `SHA256SUMS` and `update.json`. A fresh public latest-download of
|
||||
`update.json` reported version `0.4.52`, the correct release page, ten
|
||||
names, sizes and hashes matching the independent downloads, and the explicit
|
||||
limit that no person wore the headset. The public manifest's SHA-256 was
|
||||
`486bafc8ae4f1feed0f3a1db28b5034498eed78af2a11083be48175489e6d461`.
|
||||
|
||||
The exact-version Decky catalog is **6 works / 54 partial / 45 broken /
|
||||
11 N/A**. Network Sentinel and Battery Tracker remain **partial**. Their
|
||||
real managed ARM64 operation and wearer-visible behavior are unverified.
|
||||
Worn VR visibility and physical-controller interaction also remain
|
||||
unverified. The separate hourly worn check remains enabled.
|
||||
@@ -77,7 +77,7 @@ none of these frontend controls has passed a worn-headset check yet.
|
||||
| `localStorage` | Synchronous, per-plugin cache backed by the Frame Control panel origin. Up to 64 keys, 64 KiB per value and 128 KiB total; `clear()` removes only this plugin's cache. Falls back to memory if panel storage is denied. | Parent/iframe round-trip and reopen in the local host test; real headset persistence remains unverified. |
|
||||
| `SteamClient.Storage.GetJSON` / `SetObject` | Uses the same bounded per-plugin cache; a missing key rejects with `Not found`. It cannot read Steam account or global storage. | Host contract tests and local Chromium settings views; headset persistence after reopen remains unverified. |
|
||||
| IndexedDB / localForage | IndexedDB is deliberately absent inside the opaque iframe so feature detection chooses the bounded localStorage driver. Plugins requiring IndexedDB directly remain unsupported. | HLTB official frontend in local Chromium: its previous `IDBFactory.open` SecurityError disappeared. No live HLTB data request was run. |
|
||||
| `routerHook.addRoute` / `removeRoute`, `Navigation.Navigate` / `NavigateBack`, `useParams` | Local routes inside this plugin panel, with named segments such as `/plugin/game/:appid/:view?`; `useParams()` reads the current segment values | Host unit test; VR navigation unverified |
|
||||
| `routerHook.addRoute` / `removeRoute`, `Navigation.Navigate` / `NavigateBack`, `useParams` | Local routes inside this plugin panel, with named segments such as `/plugin/game/:appid/:view?`; `useParams()` reads the current segment values. Back walks up to 32 local page transitions and skips routes removed since navigation. | Host unit test; VR navigation unverified |
|
||||
| `routerHook.addPatch`, global components | No-op; cannot patch Steam routes | Not applicable |
|
||||
| `findModule`, `findModuleChild`, `findModuleExport`, class lookup and module mapping | Searches a finite registry of Framey controls and CSS maps. Unknown Steam modules return `undefined` or an empty mapping. The optional UI-audio export cannot play sound. | Host contract tests; pinned TabMaster 2.15.1 QAM and documentation render in local Chromium. Steam library editing and worn VR remain unverified. |
|
||||
| `Navigation.CloseSideMenus` | Returns to this plugin's main panel | Pinned HLTB for Deck 2.0.12 shallow settings view in local host test. Live HLTB search remains unverified. |
|
||||
@@ -91,6 +91,9 @@ Frame Control store; only paths registered by that same plugin can navigate.
|
||||
Optional route segments must be at the end. Framey accepts local path segments
|
||||
only; query strings, external URLs, Steam library routes and duplicate parameter
|
||||
names are rejected. Removing an active route returns to the plugin panel.
|
||||
`Navigation.NavigateBack()` returns to the preceding local page, then the
|
||||
plugin root. `Navigation.CloseSideMenus()` returns directly to the root and
|
||||
clears that panel's history. Neither operation navigates Steam's own UI.
|
||||
Toasts stay inside the panel, so keep essential errors in the plugin UI too.
|
||||
Treat module lookup results as Framey-local exports. Do not depend on Steam
|
||||
webpack IDs, function-source signatures, or Steam-only CSS class names; a
|
||||
|
||||
@@ -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: 6 · Partial: 50 · Broken: 49 · N/A: 11.**
|
||||
**Works: 6 · Partial: 54 · Broken: 45 · 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,9 +27,15 @@ Open the browser audit URL in local Chromium, run `runAudit()` and inspect/downl
|
||||
|
||||
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.
|
||||
|
||||
A [2026-10-28 static action-limit review](evidence/framey-static-action-limits-2026-10-28.md) hash-checked the exact AutoSuspend and CheatDeck archives. AutoSuspend contains Steam suspend/shutdown paths, and CheatDeck's advertised trainer flow needs a Steam game context menu and launch-option writes. Neither plugin was executed; both retain their broken labels.
|
||||
|
||||
A [Network Sentinel user-service check](evidence/framey-network-sentinel-2026-10-28.md) adapts only the exact 1.0.4 digest. In isolated actual Decky 3.3.0 amd64, the pinned plugin-local psutil wheel, private settings RPC and one monitoring-loop sample pass. External ping/DNS, Frame ARM64, panel controls and worn VR remain unverified; this is partial compatibility.
|
||||
|
||||
A [Battery Tracker user-service check](evidence/framey-battery-tracker-2026-10-28.md) adapts only the exact 0.2.0 digest. A locked read-only Frame probe confirmed a Battery-type supply with the required fields; actual Decky 3.3.0 amd64 recorded private Frame-shaped samples and returned history/per-app power. The managed plugin has not run on the real Frame, and panel controls and worn VR remain unverified; this is partial compatibility.
|
||||
|
||||
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. That initial pass left Simple Timer 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, RSSLine 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. That initial pass left Simple Timer 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, RSSLine truncated
|
||||
|
||||
## Every current plugin
|
||||
|
||||
@@ -38,11 +44,11 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| Plugin / version | Manifest / bundle API | Status | Reason | Verified by |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1: XR Gaming 1.6.2 | 1 / 2 | N/A | Not supported on Frame: Requires supported external XR glasses and the Breezy/XRLinuxDriver stack; it does not target Steam Frame displays. | static archive, local browser, fake-Frame |
|
||||
| 7: CSS Loader 2.1.3 | legacy / legacy | partial | Panel initialization succeeds; CSS themes patch Steam UI and cannot theme the isolated VR shell. | static archive, local browser, fake-Frame |
|
||||
| 7: CSS Loader 2.1.3 | legacy / legacy | partial | Panel initialization succeeds; CSS themes patch Steam UI and cannot theme the isolated VR shell. Shared Toggle value rendering is repaired; other listed blockers remain. | static archive, local browser, fake-Frame |
|
||||
| 10: vibrantDeck 2.0.1 | legacy / legacy | N/A | Not supported on Frame: Changes X11 gamescope color properties, not the Frame SteamVR compositor. | static archive, local browser, fake-Frame |
|
||||
| 11: Fantastic 0.5.1 | legacy / legacy | N/A | Not supported on Frame: Targets Steam Deck fan hardware; no Frame fan-control mapping is provided. | static archive, local browser, fake-Frame |
|
||||
| 12: MusicControl 1.1.6 | legacy / legacy | partial | Managed reinstall preserves Decky’s legacy calling convention and repairs the Frame OS shell library-path failure. Chromium transport/seek/volume against a silent MPRIS fixture and real ARM64 empty-player discovery pass; actual Frame media playback, album art and worn VR remain unverified. Reinstall existing copies to apply. | static archive, local browser, fake-Frame, isolated Decky 3.3.0 + MPRIS fixture, real Frame SSH |
|
||||
| 13: Pause Games 1.0.3 | 1 / 2 | partial | Settings and running-game list initialize in local Chromium with an explicit synthetic game; live Frame empty-list reads and lifetime registration/cleanup passed. Process pause/resume, automatic pausing and suspend integration remain unverified/unsupported; backend writes were denied. | static archive, local browser, fake-Frame |
|
||||
| 13: Pause Games 1.0.3 | 1 / 2 | partial | Settings and a synthetic running-game list render locally. Actual Decky 3.3.0 discovery, private settings writes/readback and exclusion-list persistence pass on amd64 and ARM64 Frame; standalone Toggle now honors its value prop. Live empty-list/lifetime checks passed earlier. Real process pause/resume, automatic pausing, suspend integration and worn VR remain unverified/unsupported. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 14: ProtonDB Badges 1.3.4 | 2 / 2 | partial | Settings render locally and read-only get_setting RPC returned the default through real Decky 3.3.0 on ARM64 Frame. Steam game-page badges have no patch target; live writes and VR controls unverified. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 16: Audio Loader 1.6.1 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. Steam UI sound lookup is present as explicitly unsupported; the frontend still requires Steam m_GamepadUIAudioStore.m_AudioPlaybackManager, which is unavailable in the isolated panel. | static archive, local browser, fake-Frame |
|
||||
| 18: Bluetooth 3.1.1 | 1 / 2 | partial | Controls render with fixture replies; actual Bluetooth discovery, labels and connections remain unverified. | static archive, local browser, fake-Frame |
|
||||
@@ -52,10 +58,10 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 23: Animation Changer 1.3.2 | legacy / legacy | partial | Initializer succeeds; forced backend failure prevents loading animations. Boot-video changes untested. | static archive, local browser, fake-Frame |
|
||||
| 24: RadiYo! 1.3 | legacy / legacy | partial | Initializer succeeds; radio discovery and playback remain unverified. | static archive, local browser, fake-Frame |
|
||||
| 26: HLTB for Deck 2.0.12 | 1 / 2 | partial | Settings render without the prior IndexedDB SecurityError; Steam library badges have no patch target, and live lookup is unverified. | static archive, local browser, fake-Frame |
|
||||
| 27: AutoSuspend 2.2.0 | legacy / legacy | broken | Frontend audit failed: Cannot read properties of null (reading '6'). | static archive, local browser, fake-Frame |
|
||||
| 27: AutoSuspend 2.2.0 | legacy / legacy | broken | Frontend audit failed: Cannot read properties of null (reading '6'). The exact frontend also contains SteamOS suspend and shutdown paths; threshold behavior and safe Frame operation are unverified. | static archive, local browser, fake-Frame |
|
||||
| 28: Emuchievements 2.0.5 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: Emuchievements/bin/hash. | static archive, local browser, fake-Frame |
|
||||
| 30: TunnelDeck 1.0.4 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 32: AutoFlatpaks 1.6.8 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 32: AutoFlatpaks 1.6.8 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. Shared Toggle value rendering is repaired; other listed blockers remain. | static archive, local browser, fake-Frame |
|
||||
| 35: MoonDeck 1.13.0 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: moondeck/python/externals/yarl/_quoting_c.cpython-313-x86_64-linux-gnu.so. Frontend initializes; native CPython wheels and executables in the published archive are x86_64. | static archive, local browser, fake-Frame |
|
||||
| 36: SteamGridDB 1.8.1 | 1 / 2 | broken | Local module mapping is available; frontend still requires the Steam LibraryContextMenu implementation, which has no target in the isolated Framey panel. | static archive, local browser, fake-Frame |
|
||||
| 37: Discord Status 1.5.0 | 1 / 2 | 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 |
|
||||
@@ -72,21 +78,21 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 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 | 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 |
|
||||
| 60: DeckyFileServer 1.2.0 | 1 / 2 | partial | Managed install supplies a native ARM64 helper and removes the exact archive's unnecessary root request. Real Decky 3.3.0 on Frame passes private read-only HTTPS listing/download and upload denial. Frontend address/picker limitations remain; use HTTPS and a preconfigured directory. Uploads, generated thumbnails and worn VR are unverified. Shared Toggle value rendering is repaired; other listed blockers remain. | static archive, local browser, fake-Frame, real Decky 3.3.0 amd64, real Frame SSH |
|
||||
| 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 | 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 |
|
||||
| 64: Syncthing 0.2.7 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: decky-syncthing/bin/decky-syncthing-watchdog. Shared Toggle value rendering is repaired; other listed blockers remain. | 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 |
|
||||
| 67: Battery Tracker 0.2.0 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 67: Battery Tracker 0.2.0 | legacy / legacy | partial | Exact managed install removes the unnecessary root flag, selects a readable Battery-type power supply, and repairs empty history and per-app sample grouping. Actual Decky 3.3.0 amd64 recorded Frame-shaped private battery samples and returned history/power data. A locked real Frame read verified the required sysfs fields, not the managed plugin. Live ARM64 recording, long-term battery history, foreground-app labeling, panel controls and worn VR remain unverified; existing copies need update or reinstall. | static archive, local browser, fake-Frame, isolated Decky amd64 backend, real Frame read-only sysfs |
|
||||
| 68: Wine Cellar 0.2.1 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-wine-cellar/bin/backend. Frontend audit failed: Audit blocks WebSocket. | static archive, local browser, fake-Frame |
|
||||
| 70: MicroSDeck 0.11.2 | legacy / 2 | broken | Plugin contains a non-ARM64 Linux binary: MicroSDeck/bin/backend. | static archive, local browser, fake-Frame |
|
||||
| 72: ScreenshotUploader 0.0.3 | legacy / legacy | partial | Initializer succeeds; screenshot upload requires external credentials/network and was not run. | static archive, local browser, fake-Frame |
|
||||
| 73: EmuDecky 1.0.2-1 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: EmuDecky/bin/hello. | static archive, local browser, fake-Frame |
|
||||
| 74: KDE Connect 0.1.0-1 | legacy / legacy | partial | Initializer succeeds; KDE Connect installation/pairing/file transfers unverified. | static archive, local browser, fake-Frame |
|
||||
| 75: Reshadeck 0.2.0 | legacy / legacy | partial | Initializer succeeds; ReShade deployment into Proton games is unverified. | static archive, local browser, fake-Frame |
|
||||
| 76: CheatDeck 1.1.6 | 1 / 2 | broken | Frontend audit failed: Cannot convert undefined or null to object. | static archive, local browser, fake-Frame |
|
||||
| 76: CheatDeck 1.1.6 | 1 / 2 | broken | Frontend audit failed: Cannot convert undefined or null to object. Its primary trainer workflow uses a Steam game context menu and launch-option writes, with no target in the isolated Framey panel; no trainer launch was verified. | static archive, local browser, fake-Frame |
|
||||
| 77: BT Wake Control 1.2.0 | 1 / 2 | partial | Initializer succeeds; backend uses DMI board paths and Bluetooth wake permissions, unverified on ARM64 Frame. | static archive, local browser, fake-Frame |
|
||||
| 78: Brightness Bar 1.0.4 | legacy / legacy | N/A | Not supported on Frame: Targets Steam/QAM brightness shortcuts and a global Steam overlay; neither is attached to the Framey panel. | static archive, local browser, fake-Frame |
|
||||
| 79: Decky Notifications 1.0.1 | legacy / legacy | partial | Initializer succeeds; device/account setup and notification delivery unverified. | static archive, local browser, fake-Frame |
|
||||
@@ -121,10 +127,10 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 110: Dimmer Deck 0.0.4 | 1 / 2 | N/A | Not supported on Frame: Changes gamescope/X11 dimming properties; no mapping to Frame VR display brightness exists. | static archive, local browser, fake-Frame |
|
||||
| 111: Music Control 1.1.7 | legacy / legacy | partial | Managed reinstall preserves Decky’s legacy calling convention and repairs the Frame OS shell library-path failure. Chromium transport/seek/volume against a silent MPRIS fixture and real ARM64 empty-player discovery pass; actual Frame media playback, album art and worn VR remain unverified. Reinstall existing copies to apply. | static archive, local browser, fake-Frame, isolated Python backend, isolated Decky 3.3.0 + MPRIS fixture, real Frame SSH |
|
||||
| 112: Better Keyboard 0.3.4 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. Frontend audit failed: window.SteamClient.UI.GetUIMode is not a function. | static archive, local browser, fake-Frame |
|
||||
| 113: Decky LSFG-VK 0.12.2 | 1 / 2 | partial | Initializer succeeds; runtime LSFG/Windows DLL downloads and Vulkan integration are not ARM64-validated. | static archive, local browser, fake-Frame |
|
||||
| 114: Network Sentinel 1.0.4 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 113: Decky LSFG-VK 0.12.2 | 1 / 2 | partial | Initializer succeeds; runtime LSFG/Windows DLL downloads and Vulkan integration are not ARM64-validated. Shared Toggle value rendering is repaired; other listed blockers remain. | static archive, local browser, fake-Frame |
|
||||
| 114: Network Sentinel 1.0.4 | 1 / 2 | partial | Exact managed install removes the unnecessary root flag and adds pinned plugin-local psutil wheels. Actual Decky 3.3.0 amd64 in network isolation loads the backend and records a network sample; private settings and read-only RPCs pass. External ping/DNS, real Frame ARM64, Wi-Fi scan behavior, panel controls and worn VR remain unverified. Existing copies need update or reinstall. | static archive, local browser, fake-Frame, isolated Decky amd64 backend |
|
||||
| 115: Decky Clash 1.3.0 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 116: Muon 0.2.1 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 116: Muon 0.2.1 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. Shared Toggle value rendering is repaired; other listed blockers remain. | static archive, local browser, fake-Frame |
|
||||
| 117: DeckyParty File Manager 1.0.0 | legacy / legacy | partial | Initializer succeeds; privileged file operations and backend are unverified. No file changes were run. | static archive, local browser, fake-Frame |
|
||||
| 118: Speed Test 1.0.2 | 1 / 2 | partial | Initializer succeeds; automatic bandwidth tests were blocked by the audit network policy. | static archive, local browser, fake-Frame |
|
||||
| 119: Decky Clipboard 1.0.1 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-clipboard/bin/xclip. | static archive, local browser, fake-Frame |
|
||||
@@ -132,11 +138,11 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 121: DeckPass 1.1.1 | 1 / 2 | partial | Initializer succeeds; KeePass Flatpak discovery and secrets integration were not exercised. | static archive, local browser, fake-Frame |
|
||||
| 123: Steam Status 1.2.3 | 2 / 2 | partial | Settings/public status rendered locally; cache errors fixed. Read-only get_settings RPC returned actual Decky 3.3.0 defaults on ARM64 Frame. Persistence, live backend diagnostics and VR controls unverified. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 124: AlarMe 1.6.3 | 1 / 2 | partial | Silent labeled timer create/ticks/pause/cancel, preset save/cancel-confirm/delete and stopwatch start/lap/pause/reset/reopen pass in local Chromium with the official Decky 3.3.0 ARM64 backend. Audio, alarms/reminders, suspend inhibition, Steam overlays and worn VR remain unverified; notifications require the panel to stay open. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 125: Decky Localsend 0.4.5 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-localsend/bin/localsend-core. | static archive, local browser, fake-Frame |
|
||||
| 126: Launch Options 1.9.2 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-launch-options/bin/hello. Frontend audit failed: Cannot convert undefined or null to object. | static archive, local browser, fake-Frame |
|
||||
| 125: Decky Localsend 0.4.5 | 1 / 2 | partial | Exact managed 0.4.5 archive replaces its x86 helper with a pinned ARM64 build. Published Decky 3.3.0 ARM64 stopped status/configuration RPCs and a private real-Frame ARM64 helper marker transfer passed. The managed service, user-facing discovery/transfer, Steam routes and worn VR remain unverified. | exact archive and source hashes, local Chromium initial panel, host managed transaction, fake Frame managed install/remove, private x86 backend transfer, published Decky 3.3.0 AMD64/ARM64 stopped status/configuration RPCs, private real Frame ARM64 helper marker transfer |
|
||||
| 126: Launch Options 1.9.2 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-launch-options/bin/hello. Frontend audit failed: Cannot convert undefined or null to object. Shared Toggle value rendering is repaired; other listed blockers remain. | static archive, local browser, fake-Frame |
|
||||
| 127: Decky Translator 0.9.0 | 1 / 2 | partial | Initializer succeeds; x86_64 download and hidraw/controller assumptions need an ARM64 audit. OCR/translation not exercised. | static archive, local browser, fake-Frame |
|
||||
| 128: Quick User Switcher 1.0.0 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 129: ShadowDeck 1.1.11 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 129: ShadowDeck 1.1.11 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. Shared Toggle value rendering is repaired; other listed blockers remain. | static archive, local browser, fake-Frame |
|
||||
| 130: Games Prefix Manager 1.0.1 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 131: Download All 1.1.0 | 1 / 2 | broken | Initializer succeeds; Steam download registration is absent, so the primary action is unavailable. | static archive, local browser, fake-Frame |
|
||||
| 132: SuggestMe 1.5.3 | 1 / 2 | partial | Initializer succeeds; recommendation backend and Steam library data unverified. | static archive, local browser, fake-Frame |
|
||||
@@ -279,9 +285,11 @@ DFL includes exports imported through decky-frontend-lib / @decky/ui. Steam stor
|
||||
|
||||
## Native payload portability
|
||||
|
||||
All published Linux native payloads in this snapshot are x86_64; no published aarch64 payload was found. The three Windows DLLs belong to Decky-Framegen. The installer still rejects these archives. Apart from the explicitly described LocalSend experiment, ARM64 builds were not attempted: the constraints below remain unresolved.
|
||||
All published Linux native payloads in this snapshot are x86_64; no published aarch64 payload was found. The three Windows DLLs belong to Decky-Framegen. Except for the exact managed DeckyFileServer and Decky Localsend adaptations below, the installer still rejects these archives. Apart from LocalSend and DeckyFileServer, ARM64 builds were not attempted: the constraints below remain unresolved.
|
||||
|
||||
[LocalSend ARM64 build/runtime evidence](evidence/framey-localsend-arm64-2026-10-26.md) covers a reproducible MIT helper build and its isolated real-Frame protocol 2.0 info response. The official x86 archive remains blocked; full plugin integration and file transfers are unverified.
|
||||
A [DeckyFileServer ARM64 follow-up](evidence/framey-fileserver-arm64-2026-10-27.md) ships an offline, digest-checked GPL helper build, corresponding vendored source and a build recipe. Only its exact managed archive can replace the x86 helper and remove its unnecessary root flag. Actual Decky 3.3.0 discovery/RPC and real Frame HTTPS private-fixture listing/download pass; uploads stay disabled. Hostname-DNS failures and empty optional arguments are repaired. Decky 3.3.0 still exposes `decky.HOME`; that call is unchanged. The plugin moves from broken to partial: its frontend advertises HTTP/steamdeck URLs and needs a Steam folder picker. Use HTTPS and a preconfigured directory. Uploads, generated thumbnails and worn VR remain unverified.
|
||||
|
||||
[LocalSend ARM64 build/runtime evidence](evidence/framey-localsend-arm64-2026-10-26.md) covers an isolated real-Frame protocol 2.0 info response from an earlier helper build. [Exact official source matching](evidence/framey-localsend-source-match-2026-10-27.md) now identifies the official helper source and a reproducible ARM64 build from that revision. The exact official x86 backend passed one local private-namespace transfer, and the rebuilt ARM64 helper passed one locked real-Frame private marker transfer. Published Decky 3.3.0 AMD64/ARM64 runtime checks discovered the adapted plugin and read its stopped status and configuration. Only the exact managed 0.4.5 archive can replace its x86 helper on Frame. The actual managed Decky service on Frame, user-facing transfer, local discovery and worn VR remain unverified ([managed adaptation evidence](evidence/framey-localsend-managed-2026-10-27.md)).
|
||||
|
||||
| Plugin | Native payloads | ARM64 result / remaining blocker |
|
||||
| --- | --- | --- |
|
||||
@@ -293,7 +301,7 @@ All published Linux native payloads in this snapshot are x86_64; no published aa
|
||||
| Controller Tools | bin/backend | Native backend needs an ARM64 build and controller/HID dependencies; its direct websocket also bypasses the Framey bridge. |
|
||||
| DeckMTP | bin/umtprd | umtprd needs ARM64 libusb/MTP dependencies and USB gadget permissions; root user-service blocker remains. |
|
||||
| Decky Cloud Save | bin/rclone | An ARM64 rclone artifact and safe package policy are needed; the official 60,776,600-byte x86_64 member fails archive validation. No cloud writes were run (docs/evidence/framey-native-port-blockers-2026-10-26.md). |
|
||||
| DeckyFileServer | bin/backend | Native file-server backend needs ARM64 dependencies; root requirement also blocks the user service. |
|
||||
| DeckyFileServer | bin/backend | GPL-3.0 helper rebuilt offline for ARM64 with vendored corresponding source. Actual Frame user service passes HTTPS listing/download of a private public fixture; uploads disabled. Exact managed digest only; other native/root archives remain blocked (docs/evidence/framey-fileserver-arm64-2026-10-27.md). |
|
||||
| TS3 QuickAccess | bin/ts3-qs4sd.so | TeamSpeak client plugin must match an ARM64 TeamSpeak client and SDK ABI, not merely rebuild the .so. |
|
||||
| Syncthing | bin/decky-syncthing-watchdog | The watchdog needs an ARM64 build, plus a supported Syncthing runtime/package and safe path validation. |
|
||||
| Wine Cellar | bin/backend | Native backend needs an ARM64 build; direct websocket and Wine runtime assumptions also need integration. |
|
||||
@@ -306,7 +314,7 @@ All published Linux native payloads in this snapshot are x86_64; no published aa
|
||||
| HueSync | bin/hello, py_modules/portio.so | portio and handheld LED backends depend on x86 I/O/unsupported hardware; ARM64 compilation alone is insufficient. |
|
||||
| Decky Clipboard | bin/xclip | xclip needs ARM64 X11 libraries/server; root and gamescope assumptions also block this archive. |
|
||||
| Reroll | bin/memscan.so | memscan.so needs ARM64 process-memory support and ABI testing; root requirement remains. Frontend initializes but no memory access was tested. |
|
||||
| Decky Localsend | bin/localsend-core | MIT LocalSend-Go 66edc509a28b9f877e7374504892dcbc653a4da4 reproducibly cross-built with Go 1.25.7/CGO_ENABLED=0 for ARM64; SHA-256 e79238f6051ce34f172f765ae918d3d902b819e27377989a55e5b1eba10e46f9. Help/official backend CLI flags and protocol 2.0 info endpoint passed on real Frame in an isolated network namespace. No file transfer or full plugin/CEF integration tested; exact source match to shipped helper unproven. Official ZIP stays x86_64 and blocked. |
|
||||
| Decky Localsend | bin/localsend-core | Exact official x86 helper is reproducible from pinned MIT source; the managed installer replaces only the exact 0.4.5 digest with SHA-256 e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae on ARM64. The Frame rechecks original Python/helper bytes, replacement digest and ELF machine; x86 keeps the original helper. Local official x86 backend/proxy and real Frame rebuilt ARM64 helper each passed one private marker transfer. Published Decky 3.3.0 AMD64/ARM64 discovery and stopped status/configuration RPCs passed. The actual managed Decky service on Frame, network discovery, user-facing transfer, Steam routes and worn VR remain unverified (docs/evidence/framey-localsend-managed-2026-10-27.md). |
|
||||
| Launch Options | bin/hello | Remove the template x86 bin/hello or publish an ARM64 rebuild; missing Steam module mapping also prevents frontend initialization. |
|
||||
| Decky-Clipper | bin/gstreamer-1.0/libgstproxy.so, bin/gstreamer-1.0/libgstmidi.so, bin/gstreamer-1.0/libgstmpegtsmux.so, bin/gstreamer-1.0/gst-plugin-scanner, bin/gstreamer-1.0/libgstivfparse.so, … 115 total; full list in JSON | Rebuild all bundled GStreamer libraries/plugins for ARM64 and verify capture/codec ABI against Frame SteamVR; gamescope capture differs. |
|
||||
|
||||
|
||||
+10
-7
@@ -203,12 +203,14 @@ the managed service is stopped; it does not restart the service.
|
||||
|
||||
Install, Update, and Remove show the current job stage in the headset panel.
|
||||
The store verifies each downloaded archive and the headset applies it with a
|
||||
rollback if Decky cannot start. The panel remembers the current job ID for this
|
||||
browser session, so **Check result** can resume polling after a navigation or
|
||||
temporary connection error. After a polling error, Install, Update, and Remove
|
||||
rollback if Decky cannot start. The panel saves a random request ID before
|
||||
submitting a change. The server uses that ID as the job ID and returns the
|
||||
same job for a repeated, identical request while its result is retained;
|
||||
reusing the ID for a different change is rejected. **Check result** can resume
|
||||
polling even if the POST response is lost. After a connection error, Install, Update, and Remove
|
||||
stay disabled while the saved job's result is unknown; use **Check result**
|
||||
before starting another change.
|
||||
If Chromium denies session storage, the open panel keeps the accepted job ID in
|
||||
If Chromium denies session storage, the open panel keeps the request ID in
|
||||
memory and **Check result** still works after a connection error. A page reload
|
||||
loses that memory fallback; wait for the old job to finish and refresh installed
|
||||
status before starting another change.
|
||||
@@ -216,9 +218,10 @@ Refresh the installed list before starting another
|
||||
operation after an error. The headset panel and desktop app send the hash of
|
||||
the version you saw; if the official store changes that version before the job
|
||||
starts, Frame Control stops before downloading or changing the headset. Refresh
|
||||
the store and review the new version before retrying. If Frame Control restarts, it cannot recover the old
|
||||
job result. The panel clears that job ID and asks you to refresh the installed
|
||||
list before making another change.
|
||||
the store and review the new version before retrying. Job results live in server
|
||||
memory for one hour after completion. If Frame Control restarts, or the request
|
||||
never reached it, the panel cannot recover the result. It clears that ID and
|
||||
asks you to refresh the installed list before making another change.
|
||||
|
||||
An open plugin panel closes before its own Update or Remove starts, so its old
|
||||
frontend cannot call the changing backend. Open panel is unavailable while a
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
# Frame Control standing goal and historical recovery handoff
|
||||
|
||||
Owner: Alex. This records the 27 October 2026 AEDT recovery handoff; status
|
||||
checks below use UTC. Alex subsequently authorized the existing coordinator to
|
||||
continue the roadmap and publish releases when every gate passes. This document
|
||||
does not authorize a second coordinator to run concurrently.
|
||||
|
||||
## Alex's top priority
|
||||
|
||||
> Make Frame Control's released version always stable and running cleanly. Never release something that isn't working correctly. I don't want to break things for people.
|
||||
|
||||
Keep the released app stable and main green. Investigate failures before
|
||||
shipping; a version number or an uploaded artifact is not proof of readiness.
|
||||
|
||||
## Standing roadmap
|
||||
|
||||
Continue official Decky compatibility, test gaps, fixes and useful features.
|
||||
The existing owner is T3 job `mppkd9qczt` on lxso2. Preserve its work and avoid
|
||||
competing coordinator or release jobs. Verify its current state before any
|
||||
takeover; this historical handoff is not a takeover instruction.
|
||||
|
||||
## Mandatory release gates
|
||||
|
||||
- CI must be green on the **exact release commit**, not merely an earlier
|
||||
commit, another branch, or a previous successful workflow attempt.
|
||||
- End-to-end tests and release verification must pass for that candidate.
|
||||
- Every required installer build must succeed, including Linux, Windows,
|
||||
macOS Intel and macOS Apple Silicon.
|
||||
- Never publish a release with failed or cancelled required builds. A retry
|
||||
must actually finish successfully; a cancelled build is not a pass.
|
||||
- Keep main green. Resolve required failures before merging or releasing.
|
||||
- Preserve evidence linking the release tag/commit, successful workflow run,
|
||||
installer assets/checksums, e2e results and real-device verification.
|
||||
- Use `scripts/publish-release.sh --dry-run TAG` before publishing. Alex's
|
||||
standing full-auto instruction authorizes the existing coordinator to tag and
|
||||
publish after every gate passes. Stop for secrets, money, destructive Git
|
||||
actions or wiping the headset; never bypass a failed release gate.
|
||||
|
||||
## Current release state, checked 27 October 2026 at 05:15 UTC
|
||||
|
||||
- The latest public release is
|
||||
[v0.4.51](https://github.com/saphid/frame-control/releases/tag/v0.4.51),
|
||||
published at 05:10:05 UTC from main commit
|
||||
`4da7220660ba345bc46cdb525b6c512bc6a9d8bd`.
|
||||
- [Exact-commit checks](https://github.com/saphid/frame-control/actions/runs/40451752952),
|
||||
[native Decky runtime](https://github.com/saphid/frame-control/actions/runs/40451752953),
|
||||
and the [tagged installer release](https://github.com/saphid/frame-control/actions/runs/40452627862)
|
||||
succeeded. The tag release built Linux, Windows, Intel Mac and Apple Silicon
|
||||
Mac installers and checksums. The published update manifest reports 0.4.51.
|
||||
- The locked candidate smoke passed 12 checks, skipped one optional x86_64
|
||||
launch, and failed none. SteamVR was in standby; worn display and physical
|
||||
controller use remain unverified. The hourly shared-lock worn check remains
|
||||
enabled. See [candidate evidence](../evidence/v0451-candidate-2026-10-27.md)
|
||||
and [release evidence](../evidence/v0451-release-2026-10-27.md) for the exact
|
||||
asset audit and publication record.
|
||||
- Main matched the release commit and its checks were green at this check.
|
||||
Revalidate before another merge or release. Disk capacity on lxso2 remains
|
||||
critical; do not delete pre-existing data to make space without Alex's OK.
|
||||
|
||||
## Incident snapshot supplied by Alex
|
||||
|
||||
At the time of the recovery request, latest published was **v0.4.47** with a
|
||||
passed release run; **v0.4.48 RC was not published**, after a failed run with a
|
||||
cancelled macOS build. PRs **#337 and #338 were open**. Preserve this as the
|
||||
historical incident snapshot, not a claim that remote state stays unchanged.
|
||||
|
||||
## Verified update during recovery
|
||||
|
||||
Checked 26 October 2026 around 21:58 UTC:
|
||||
|
||||
- v0.4.47 was published at 15:07:28 UTC.
|
||||
- A subsequent v0.4.48 release run
|
||||
[40406781383](https://github.com/saphid/frame-control/actions/runs/40406781383)
|
||||
completed successfully on commit
|
||||
`5438e1d9d05ca472f200dcde50799c8ae882fafb`. Its Linux, Windows, Intel macOS
|
||||
and Apple Silicon macOS installer jobs, draft and checksum jobs all succeeded.
|
||||
- GitHub now reports **v0.4.48 published at 21:53:29 UTC**. This publication
|
||||
happened outside the recovery task. Recheck the exact-commit checks and
|
||||
release evidence before drawing conclusions about all release gates.
|
||||
- [#337](https://github.com/saphid/frame-control/pull/337) is merged; lxso1's
|
||||
clean main was fast-forwarded to
|
||||
`1ddb6278b8ae0a98ecc79a181bba4d3cc67a52b6`.
|
||||
- [#338](https://github.com/saphid/frame-control/pull/338) was open at that
|
||||
time; it has since merged.
|
||||
- lxso2 T3 recovered at **21:42:54 UTC**; port 3773/local HTTP and existing
|
||||
HTTPS returned 200 by 21:43:58 UTC. Root disk capacity remains critical;
|
||||
recovery is not a capacity fix.
|
||||
- Fleet guards, probe units, safety tests and incident evidence are proposed
|
||||
in [fleet #38](https://github.com/saphid/fleet/pull/38); units were installed
|
||||
under the owner's incident authorization. Disk critical alerts use the
|
||||
existing fleet Prometheus/watchdog pipeline.
|
||||
|
||||
## Historical lxso1 readiness
|
||||
|
||||
Main checkout: `/home/saphid/projects/steam-frame` (GitHub repository
|
||||
`saphid/frame-control`). Shell default Node may be older; use
|
||||
`~/.nvm/versions/node/v24.22.1/bin` first in PATH for Node 24 CI commands.
|
||||
Python 3.9.25 and 3.13.14 were installed through uv for the CI matrix; system
|
||||
Python 3.12.3 already existed. Workflows at that time required no Rust/Go
|
||||
toolchain.
|
||||
|
||||
Readiness commands, per-gate logs and exact-commit JSON results live at
|
||||
`/tmp/frame-control-readiness-20261027/` on lxso1. These are local readiness
|
||||
checks, not a substitute for GitHub CI on a future release commit. The Linux
|
||||
installer command uses `--publish never`; no release was published by these
|
||||
checks. Native ARM64, Windows/macOS platform gates, Xcode/iOS and real-headset
|
||||
checks still require their proper runners/hardware.
|
||||
|
||||
Completed on main commit `1ddb6278b8ae0a98ecc79a181bba4d3cc67a52b6`,
|
||||
26 October 2026 at 22:00 UTC:
|
||||
|
||||
| Gate | Result on lxso1 |
|
||||
| --- | --- |
|
||||
| Shell syntax, Python compile | Passed |
|
||||
| Python 3.9 / 3.12 / 3.13 server suites | 1,639 tests each; passed with platform/optional skips |
|
||||
| Linux wireless doctor, app syntax/tests, website tests/syntax | Passed |
|
||||
| `npm ci`, `npm run dist:linux` | Passed; AppImage and deb built without publishing |
|
||||
| AppImage launch verification | Passed |
|
||||
| `scripts/e2e.sh` on x86_64 Docker | Passed: 50 tests, one optional catalog audit skipped; not native ARM64 hardware proof |
|
||||
| Publish dry runs for v0.4.47 and v0.4.48 | Passed, including all ten installer digests and SHA256SUMS; no mutation |
|
||||
| Native PC host build and required-native tests | Blocked/failed: GStreamer/GLib development packages absent; required native adapter unavailable |
|
||||
|
||||
The workflows name system GStreamer packages but do not pin package versions.
|
||||
They were left uninstalled under the owner's pinned-versions-only constraint.
|
||||
Resolve that dependency policy before claiming full Linux native readiness.
|
||||
Main was clean and matched origin/main on the final 21:59 UTC fetch.
|
||||
|
||||
## How a takeover should resume
|
||||
|
||||
1. Read this file, `docs/testing.md`, the current roadmap/evidence and the
|
||||
existing coordinator's recorded work through supported read-only tooling.
|
||||
Verify remote releases, PR states and workflow conclusions again.
|
||||
2. Check lxso2 health and disk headroom. Disk-full recovery requires capacity
|
||||
decisions; do not delete protected data or alter T3 state databases.
|
||||
3. Confirm that the existing coordinator is no longer active and that the
|
||||
takeover is authorized by the current instructions. Preserve uncommitted
|
||||
changes and active worktrees; do not run competing coordinator/release jobs.
|
||||
4. Fetch origin and inspect local status before updating main. Resume the
|
||||
roadmap from recorded gaps and existing PRs, not from assumptions about
|
||||
what an interrupted worker completed.
|
||||
5. Work on an isolated branch and use PRs. Validate the actual candidate
|
||||
commit with every release gate above; record hardware/platform blockers.
|
||||
6. Publish only after all required gates pass under Alex's current full-auto
|
||||
instruction or a newer explicit instruction.
|
||||
|
||||
The lxso1 five-minute `fleet-t3-lxso2-health.timer` only checks and alerts; it
|
||||
does not start takeover threads. Automatic stand-in launch is not configured
|
||||
or proven by this recovery deployment. It would need supported gateway
|
||||
capability and authorized credentials; do not copy or mint gateway secrets.
|
||||
|
||||
The original handoff copy was placed at
|
||||
`/home/saphid/frame-control-handoff/STANDING_GOAL.md` on lxso1. This repository
|
||||
document may be newer.
|
||||
+3
-1
@@ -51,7 +51,7 @@ count. So a build reaches people only when you publish it, after testing it.
|
||||
The script checks that the tag is on GitHub, that exact-commit main CI
|
||||
(including e2e), native amd64/ARM64 Decky runtime checks, and the tagged
|
||||
release workflow are green with no failed,
|
||||
cancelled, skipped or running jobs, and that all ten installers
|
||||
cancelled, skipped or running jobs in any run attempt, and that all ten installers
|
||||
are attached, each with the SHA-256 digest GitHub records. It also checks
|
||||
that the draft does not begin with a `WITHDRAWN` notice, so the preserved
|
||||
v0.4.48 draft cannot accidentally be offered again. It checks
|
||||
@@ -59,6 +59,8 @@ count. So a build reaches people only when you publish it, after testing it.
|
||||
installer digests. It attaches
|
||||
`update.json` (the version, the notes, the release page and each
|
||||
installer's digest), then publishes the release and marks it latest. It
|
||||
refuses an already published release before replacing its live update
|
||||
manifest; use a new tested release for changes after publication. It
|
||||
uses only the REST API: `gh release view` can't find a draft whose
|
||||
`tag_name` still reads `untagged-…`, and GraphQL is often rate-limited.
|
||||
Such a draft is found by its exact title (`Frame Control 0.4.0`, or that
|
||||
|
||||
@@ -23,6 +23,11 @@ for each future schema instead of changing this guard silently.
|
||||
The panel also guards its own read and write calls synchronously. If two
|
||||
controller events arrive before React redraws a disabled control, the second
|
||||
event is ignored; after the request settles, a new change can be saved.
|
||||
**Reset example setting** opens a local `ConfirmModal`. **Keep setting** closes
|
||||
it without a backend call; **Reset** saves `false` through the same guarded
|
||||
write path as the toggle. This is a safe example of putting a settings change
|
||||
behind an explicit confirmation in Framey. It has not been tried with a
|
||||
physical controller in a worn headset.
|
||||
|
||||
The **About this setting** button opens a local detail page at
|
||||
`/framey-sample/details/saved-setting`. It demonstrates `routerHook.addRoute`,
|
||||
@@ -54,7 +59,8 @@ plugin directory does not enable its VR panel.
|
||||
Run `python3 -m unittest discover -s tests -p test_framey_sample.py` from the
|
||||
repository root. It checks backend persistence and evaluates the built bundle
|
||||
against Framey's actual host script, including the quick action's scoped
|
||||
backend call and local route registration/navigation. The fake Frame store flow is exercised by
|
||||
backend call, modal cancel/confirm behavior, and local route
|
||||
registration/navigation. The fake Frame store flow is exercised by
|
||||
the repository's e2e suite. No worn-headset visibility or controller-input test
|
||||
has passed for this sample yet.
|
||||
|
||||
|
||||
+9
-1
@@ -31,6 +31,7 @@ function Content() {
|
||||
const [busy, setBusy] = SP_REACT.useState(true);
|
||||
const [message, setMessage] = SP_REACT.useState('Loading setting…');
|
||||
const pending = SP_REACT.useRef(false);
|
||||
const resetModalOpen = SP_REACT.useRef(false);
|
||||
async function refresh() {
|
||||
if (pending.current)
|
||||
return;
|
||||
@@ -72,7 +73,14 @@ function Content() {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
return SP_JSX.jsxs(DFL.PanelSection, { title: "Framey settings sample", children: [SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ToggleField, { label: "Example setting", description: "Stored by the Decky Python backend", checked: enabled, disabled: busy, onChange: (value) => { void change(value); } }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { disabled: busy, onClick: () => { void refresh(); }, children: "Reload saved setting" }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { onClick: () => DFL.Navigation.Navigate('/framey-sample/details/saved-setting'), children: "About this setting" }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx("p", { role: "status", children: message }) })] });
|
||||
function confirmReset() {
|
||||
if (pending.current || resetModalOpen.current)
|
||||
return;
|
||||
const dialog = DFL.showModal(SP_JSX.jsx(DFL.ConfirmModal, { strTitle: "Reset example setting?", strDescription: "This changes only the sample plugin's saved setting.", strOKButtonText: "Reset", strCancelButtonText: "Keep setting", onOK: () => { void change(false); } }));
|
||||
resetModalOpen.current = true;
|
||||
void dialog.ClosedPromise.then(() => { resetModalOpen.current = false; });
|
||||
}
|
||||
return SP_JSX.jsxs(DFL.PanelSection, { title: "Framey settings sample", children: [SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ToggleField, { label: "Example setting", description: "Stored by the Decky Python backend", checked: enabled, disabled: busy, onChange: (value) => { void change(value); } }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { disabled: busy, onClick: () => { void refresh(); }, children: "Reload saved setting" }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { disabled: busy, onClick: confirmReset, children: "Reset example setting" }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx(DFL.ButtonItem, { onClick: () => DFL.Navigation.Navigate('/framey-sample/details/saved-setting'), children: "About this setting" }) }), SP_JSX.jsx(DFL.PanelSectionRow, { children: SP_JSX.jsx("p", { role: "status", children: message }) })] });
|
||||
}
|
||||
function Details() {
|
||||
const { topic } = DFL.useParams();
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"index.js","sources":["decky://decky/plugin/Framey%20Settings%20Sample/node_modules/.pnpm/@decky+api@1.1.3/node_modules/@decky/api/dist/index.js","decky://decky/plugin/Framey%20Settings%20Sample/src/index.tsx"],"sourcesContent":["import _manifest from '@decky/manifest';\nexport * from \"./types\";\nconst manifest = _manifest;\nconst API_VERSION = 2;\nif (!manifest?.name) {\n throw new Error('[@decky/api]: Failed to find plugin manifest.');\n}\nconst internalAPIConnection = window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit;\nif (!internalAPIConnection) {\n throw new Error('[@decky/api]: Failed to connect to the loader as as the loader API was not initialized. This is likely a bug in Decky Loader.');\n}\nlet api;\ntry {\n api = internalAPIConnection.connect(API_VERSION, manifest.name);\n}\ncatch {\n api = internalAPIConnection.connect(1, manifest.name);\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version 1. Some features may not work.`);\n}\nif (api._version != API_VERSION) {\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version ${api._version}. Some features may not work.`);\n}\nexport const call = api.call;\nexport const callable = api.callable;\nexport const addEventListener = api.addEventListener;\nexport const removeEventListener = api.removeEventListener;\nexport const routerHook = api.routerHook;\nexport const toaster = api.toaster;\nexport const openFilePicker = api.openFilePicker;\nexport const executeInTab = api.executeInTab;\nexport const injectCssIntoTab = api.injectCssIntoTab;\nexport const removeCssFromTab = api.removeCssFromTab;\nexport const fetchNoCors = api.fetchNoCors;\nexport const getExternalResourceURL = api.getExternalResourceURL;\nexport const useQuickAccessVisible = api.useQuickAccessVisible;\nexport const definePlugin = (fn) => {\n return (...args) => {\n return fn(...args);\n };\n};\n","import {callable, definePlugin, routerHook, toaster} from '@decky/api';\nimport {ButtonItem, Navigation, PanelSection, PanelSectionRow, ToggleField, useParams} from '@decky/ui';\nimport {useEffect, useRef, useState} from 'react';\n\nconst getSetting = callable<[key: string, fallback: boolean], boolean>('get_setting');\nconst setSetting = callable<[key: string, value: boolean], boolean>('set_setting');\n\nfunction Content() {\n const [enabled, setEnabled] = useState(false);\n const [busy, setBusy] = useState(true);\n const [message, setMessage] = useState('Loading setting…');\n const pending = useRef(false);\n\n async function refresh() {\n if (pending.current) return;\n pending.current = true;\n setBusy(true);\n try {\n const value = await getSetting('enabled', false);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n } catch (error) {\n setMessage(`Could not read setting: ${String(error)}`);\n } finally {\n pending.current = false;\n setBusy(false);\n }\n }\n\n useEffect(() => {void refresh();}, []);\n\n async function change(value: boolean) {\n // Two controller events can arrive before React redraws the disabled toggle.\n // Keep writes ordered so a late first reply cannot replace the final choice.\n if (pending.current) return;\n pending.current = true;\n setBusy(true);\n try {\n await setSetting('enabled', value);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n toaster.toast({title: 'Framey sample',\n body: <span>Setting saved: <strong>{value ? 'on' : 'off'}</strong></span>});\n } catch (error) {\n setMessage(`Could not save setting: ${String(error)}`);\n } finally {\n pending.current = false;\n setBusy(false);\n }\n }\n\n return <PanelSection title=\"Framey settings sample\">\n <PanelSectionRow>\n <ToggleField label=\"Example setting\" description=\"Stored by the Decky Python backend\"\n checked={enabled} disabled={busy} onChange={(value: boolean) => {void change(value);}} />\n </PanelSectionRow>\n <PanelSectionRow><ButtonItem disabled={busy} onClick={() => {void refresh();}}>Reload saved setting</ButtonItem></PanelSectionRow>\n <PanelSectionRow><ButtonItem onClick={() => Navigation.Navigate('/framey-sample/details/saved-setting')}>About this setting</ButtonItem></PanelSectionRow>\n <PanelSectionRow><p role=\"status\">{message}</p></PanelSectionRow>\n </PanelSection>;\n}\n\nfunction Details() {\n const {topic} = useParams<{topic?: string}>();\n return <PanelSection title=\"Sample detail page\">\n <PanelSectionRow><p>Topic: {topic || 'overview'}</p></PanelSectionRow>\n <PanelSectionRow><p>The toggle is stored by this plugin's Decky backend in the user's settings directory.</p></PanelSectionRow>\n <PanelSectionRow><ButtonItem onClick={() => Navigation.NavigateBack()}>Back Line truncated
|
||||
{"version":3,"file":"index.js","sources":["decky://decky/plugin/Framey%20Settings%20Sample/node_modules/.pnpm/@decky+api@1.1.3/node_modules/@decky/api/dist/index.js","decky://decky/plugin/Framey%20Settings%20Sample/src/index.tsx"],"sourcesContent":["import _manifest from '@decky/manifest';\nexport * from \"./types\";\nconst manifest = _manifest;\nconst API_VERSION = 2;\nif (!manifest?.name) {\n throw new Error('[@decky/api]: Failed to find plugin manifest.');\n}\nconst internalAPIConnection = window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit;\nif (!internalAPIConnection) {\n throw new Error('[@decky/api]: Failed to connect to the loader as as the loader API was not initialized. This is likely a bug in Decky Loader.');\n}\nlet api;\ntry {\n api = internalAPIConnection.connect(API_VERSION, manifest.name);\n}\ncatch {\n api = internalAPIConnection.connect(1, manifest.name);\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version 1. Some features may not work.`);\n}\nif (api._version != API_VERSION) {\n console.warn(`[@decky/api] Requested API version ${API_VERSION} but the running loader only supports version ${api._version}. Some features may not work.`);\n}\nexport const call = api.call;\nexport const callable = api.callable;\nexport const addEventListener = api.addEventListener;\nexport const removeEventListener = api.removeEventListener;\nexport const routerHook = api.routerHook;\nexport const toaster = api.toaster;\nexport const openFilePicker = api.openFilePicker;\nexport const executeInTab = api.executeInTab;\nexport const injectCssIntoTab = api.injectCssIntoTab;\nexport const removeCssFromTab = api.removeCssFromTab;\nexport const fetchNoCors = api.fetchNoCors;\nexport const getExternalResourceURL = api.getExternalResourceURL;\nexport const useQuickAccessVisible = api.useQuickAccessVisible;\nexport const definePlugin = (fn) => {\n return (...args) => {\n return fn(...args);\n };\n};\n","import {callable, definePlugin, routerHook, toaster} from '@decky/api';\nimport {ButtonItem, ConfirmModal, Navigation, PanelSection, PanelSectionRow, ToggleField, showModal, useParams} from '@decky/ui';\nimport {useEffect, useRef, useState} from 'react';\n\nconst getSetting = callable<[key: string, fallback: boolean], boolean>('get_setting');\nconst setSetting = callable<[key: string, value: boolean], boolean>('set_setting');\n\nfunction Content() {\n const [enabled, setEnabled] = useState(false);\n const [busy, setBusy] = useState(true);\n const [message, setMessage] = useState('Loading setting…');\n const pending = useRef(false);\n const resetModalOpen = useRef(false);\n\n async function refresh() {\n if (pending.current) return;\n pending.current = true;\n setBusy(true);\n try {\n const value = await getSetting('enabled', false);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n } catch (error) {\n setMessage(`Could not read setting: ${String(error)}`);\n } finally {\n pending.current = false;\n setBusy(false);\n }\n }\n\n useEffect(() => {void refresh();}, []);\n\n async function change(value: boolean) {\n // Two controller events can arrive before React redraws the disabled toggle.\n // Keep writes ordered so a late first reply cannot replace the final choice.\n if (pending.current) return;\n pending.current = true;\n setBusy(true);\n try {\n await setSetting('enabled', value);\n setEnabled(value);\n setMessage(`Saved setting: ${value ? 'on' : 'off'}`);\n toaster.toast({title: 'Framey sample',\n body: <span>Setting saved: <strong>{value ? 'on' : 'off'}</strong></span>});\n } catch (error) {\n setMessage(`Could not save setting: ${String(error)}`);\n } finally {\n pending.current = false;\n setBusy(false);\n }\n }\n\n function confirmReset() {\n if (pending.current || resetModalOpen.current) return;\n const dialog = showModal(<ConfirmModal strTitle=\"Reset example setting?\"\n strDescription=\"This changes only the sample plugin's saved setting.\"\n strOKButtonText=\"Reset\" strCancelButtonText=\"Keep setting\"\n onOK={() => {void change(false);}} />);\n resetModalOpen.current = true;\n void dialog.ClosedPromise.then(() => {resetModalOpen.current = false;});\n }\n\n return <PanelSection title=\"Framey settings sample\">\n <PanelSectionRow>\n <ToggleField label=\"Example setting\" description=\"Stored by the Decky Python backend\"\n checked={enabled} disabled={busy} onChange={(value: boolean) => {void change(value);}} />\n </PanelSectionRow>\n <PanelSectionRow><ButtonItem disabled={busy} onClick={() => {void refresh();}}>Reload saved setting</ButtonItem></PanelSectionRow>\n <PanelSectionRow><ButtonItem disabled={busy} onClick={confirmReset}>Reset example setting</ButtonItem></PanelSectionRLine truncated
|
||||
@@ -1,5 +1,5 @@
|
||||
import {callable, definePlugin, routerHook, toaster} from '@decky/api';
|
||||
import {ButtonItem, Navigation, PanelSection, PanelSectionRow, ToggleField, useParams} from '@decky/ui';
|
||||
import {ButtonItem, ConfirmModal, Navigation, PanelSection, PanelSectionRow, ToggleField, showModal, useParams} from '@decky/ui';
|
||||
import {useEffect, useRef, useState} from 'react';
|
||||
|
||||
const getSetting = callable<[key: string, fallback: boolean], boolean>('get_setting');
|
||||
@@ -10,6 +10,7 @@ function Content() {
|
||||
const [busy, setBusy] = useState(true);
|
||||
const [message, setMessage] = useState('Loading setting…');
|
||||
const pending = useRef(false);
|
||||
const resetModalOpen = useRef(false);
|
||||
|
||||
async function refresh() {
|
||||
if (pending.current) return;
|
||||
@@ -49,12 +50,23 @@ function Content() {
|
||||
}
|
||||
}
|
||||
|
||||
function confirmReset() {
|
||||
if (pending.current || resetModalOpen.current) return;
|
||||
const dialog = showModal(<ConfirmModal strTitle="Reset example setting?"
|
||||
strDescription="This changes only the sample plugin's saved setting."
|
||||
strOKButtonText="Reset" strCancelButtonText="Keep setting"
|
||||
onOK={() => {void change(false);}} />);
|
||||
resetModalOpen.current = true;
|
||||
void dialog.ClosedPromise.then(() => {resetModalOpen.current = false;});
|
||||
}
|
||||
|
||||
return <PanelSection title="Framey settings sample">
|
||||
<PanelSectionRow>
|
||||
<ToggleField label="Example setting" description="Stored by the Decky Python backend"
|
||||
checked={enabled} disabled={busy} onChange={(value: boolean) => {void change(value);}} />
|
||||
</PanelSectionRow>
|
||||
<PanelSectionRow><ButtonItem disabled={busy} onClick={() => {void refresh();}}>Reload saved setting</ButtonItem></PanelSectionRow>
|
||||
<PanelSectionRow><ButtonItem disabled={busy} onClick={confirmReset}>Reset example setting</ButtonItem></PanelSectionRow>
|
||||
<PanelSectionRow><ButtonItem onClick={() => Navigation.Navigate('/framey-sample/details/saved-setting')}>About this setting</ButtonItem></PanelSectionRow>
|
||||
<PanelSectionRow><p role="status">{message}</p></PanelSectionRow>
|
||||
</PanelSection>;
|
||||
|
||||
@@ -7,8 +7,11 @@ ios/scripts/make_frame_bundle.py runs this; app/build/fetch-deps.js does the sam
|
||||
in Node. Usage: python3 frame/kdeconnect/fetch.py
|
||||
"""
|
||||
import hashlib
|
||||
import http.client
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
@@ -24,6 +27,25 @@ def sha256(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def download_verified(url, expected_sha256):
|
||||
for attempt in range(3):
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=120) as response:
|
||||
data = response.read()
|
||||
except (OSError, http.client.HTTPException) as error:
|
||||
# GitHub release downloads occasionally close during connection or
|
||||
# transfer. Retry those failures, but fail fast on permanent HTTP errors.
|
||||
if isinstance(error, urllib.error.HTTPError) and error.code not in (408, 429) and error.code < 500:
|
||||
raise
|
||||
if attempt == 2:
|
||||
raise
|
||||
time.sleep(attempt + 1)
|
||||
continue
|
||||
if sha256(data) != expected_sha256:
|
||||
raise SystemExit(f"checksum mismatch for {url}: {sha256(data)}")
|
||||
return data
|
||||
|
||||
|
||||
def fetch():
|
||||
m = manifest()
|
||||
PACKAGES.mkdir(exist_ok=True)
|
||||
@@ -32,10 +54,7 @@ def fetch():
|
||||
if path.is_file() and sha256(path.read_bytes()) == p["sha256"]:
|
||||
continue
|
||||
url = m["release"] + p["file"]
|
||||
with urllib.request.urlopen(url, timeout=120) as r:
|
||||
data = r.read()
|
||||
if sha256(data) != p["sha256"]:
|
||||
raise SystemExit(f"checksum mismatch for {url}: {sha256(data)}")
|
||||
data = download_verified(url, p["sha256"])
|
||||
tmp = path.with_suffix(".part")
|
||||
tmp.write_bytes(data)
|
||||
tmp.replace(path)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild the pinned GPL DeckyFileServer helper offline (Go 1.25.7 required)."""
|
||||
import argparse
|
||||
import gzip
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ASSETS = ROOT/'ui/vendor/deckyfileserver-1.2.0'
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--go', default='go')
|
||||
parser.add_argument('--output', type=Path, default=ASSETS)
|
||||
args = parser.parse_args()
|
||||
assert subprocess.check_output([args.go, 'version'], text=True).split()[2] == 'go1.25.7', 'Use Go 1.25.7'
|
||||
assert hashlib.sha256((ASSETS/'source.tar.gz').read_bytes()).hexdigest() == '50d90a7328daa412bf963ec68bb4855a9ec0860a8475cdf7d2019e6c2507112d', 'Source archive digest mismatch'
|
||||
args.output.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix='framey-fileserver-build-') as temp:
|
||||
base = Path(temp)
|
||||
with tarfile.open(ASSETS/'source.tar.gz') as archive:
|
||||
assert all(m.isfile() and m.name.startswith('source/') and '..' not in Path(m.name).parts
|
||||
for m in archive.getmembers())
|
||||
archive.extractall(base, filter='data')
|
||||
env = dict(os.environ, CGO_ENABLED='0', GOOS='linux', GOPROXY='off', GOSUMDB='off', GOTOOLCHAIN='local',
|
||||
GOCACHE=str(base/'cache'), GOMODCACHE=str(base/'modules'))
|
||||
for arch, name in [('amd64', 'x86_64'), ('arm64', 'aarch64')]:
|
||||
output = base/'backend'
|
||||
subprocess.run([args.go, 'build', '-mod=vendor', '-trimpath', '-buildvcs=false',
|
||||
'-ldflags=-s -w -buildid=', '-o', str(output), '.'], cwd=base/'source',
|
||||
env=dict(env, GOARCH=arch), check=True)
|
||||
data = output.read_bytes()
|
||||
packed = gzip.compress(data, mtime=0)
|
||||
(args.output/f'{name}.gz').write_bytes(packed)
|
||||
print(json.dumps({'arch': name, 'sha256': hashlib.sha256(data).hexdigest(),
|
||||
'gzipSha256': hashlib.sha256(packed).hexdigest(), 'bytes': len(data)}), flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rebuild the pinned LocalSend-Go helper (Go 1.25.7 and module access required)."""
|
||||
import argparse
|
||||
import gzip
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ASSETS = ROOT / 'ui/vendor/localsend-0.4.5'
|
||||
SOURCE_SHA256 = 'f1f86292624b989d540689dbfad503bf15f3937d156978157b8dd418d54415d1'
|
||||
EXPECTED = {
|
||||
'amd64': 'd448d03861d578685e6c70ea3ace25eb34fca99cb9d4f98bf51ba23aa5cae843',
|
||||
'arm64': 'e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae',
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--go', default='go', help='Path to Go 1.25.7')
|
||||
parser.add_argument('--output', type=Path, default=ASSETS)
|
||||
args = parser.parse_args()
|
||||
assert subprocess.check_output([args.go, 'version'], text=True).split()[2] == 'go1.25.7', 'Use Go 1.25.7'
|
||||
source_data = (ASSETS/'source.tar.gz').read_bytes()
|
||||
assert hashlib.sha256(source_data).hexdigest() == SOURCE_SHA256, 'Pinned source archive changed'
|
||||
args.output.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix='framey-localsend-build-') as temporary:
|
||||
base = Path(temporary)
|
||||
with tarfile.open(fileobj=io.BytesIO(source_data), mode='r:gz') as archive:
|
||||
members = archive.getmembers()
|
||||
assert all((m.isfile() or m.isdir()) and
|
||||
(m.name == 'source' or m.name.startswith('source/'))
|
||||
and '..' not in Path(m.name).parts for m in members), 'Unsafe source archive'
|
||||
archive.extractall(base, filter='data')
|
||||
env = dict(os.environ, CGO_ENABLED='0', GOOS='linux', GOTOOLCHAIN='local',
|
||||
GOCACHE=str(base/'cache'), GOMODCACHE=str(base/'modules'))
|
||||
for arch in ('amd64', 'arm64'):
|
||||
output = base/'localsend-core'
|
||||
subprocess.run([args.go, 'build', '-mod=readonly', '-trimpath', '-buildvcs=false',
|
||||
'-ldflags=-s -w', '-o', str(output), '.'], cwd=base/'source',
|
||||
env=dict(env, GOARCH=arch), check=True)
|
||||
data = output.read_bytes()
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
assert digest == EXPECTED[arch], f'{arch} helper does not match pinned bytes'
|
||||
if arch == 'arm64':
|
||||
packed = gzip.compress(data, mtime=0)
|
||||
(args.output/'aarch64.gz').write_bytes(packed)
|
||||
packed_digest = hashlib.sha256(packed).hexdigest()
|
||||
else:
|
||||
packed_digest = None
|
||||
print(json.dumps({'arch': arch, 'sha256': digest, 'gzipSha256': packed_digest,
|
||||
'bytes': len(data)}), flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -41,6 +41,9 @@ def render(data):
|
||||
'```', '',
|
||||
'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.', '',
|
||||
'A [2026-10-28 static action-limit review](evidence/framey-static-action-limits-2026-10-28.md) hash-checked the exact AutoSuspend and CheatDeck archives. AutoSuspend contains Steam suspend/shutdown paths, and CheatDeck\'s advertised trainer flow needs a Steam game context menu and launch-option writes. Neither plugin was executed; both retain their broken labels.', '',
|
||||
'A [Network Sentinel user-service check](evidence/framey-network-sentinel-2026-10-28.md) adapts only the exact 1.0.4 digest. In isolated actual Decky 3.3.0 amd64, the pinned plugin-local psutil wheel, private settings RPC and one monitoring-loop sample pass. External ping/DNS, Frame ARM64, panel controls and worn VR remain unverified; this is partial compatibility.', '',
|
||||
'A [Battery Tracker user-service check](evidence/framey-battery-tracker-2026-10-28.md) adapts only the exact 0.2.0 digest. A locked read-only Frame probe confirmed a Battery-type supply with the required fields; actual Decky 3.3.0 amd64 recorded private Frame-shaped samples and returned history/per-app power. The managed plugin has not run on the real Frame, and panel controls and worn VR remain unverified; this is partial compatibility.', '',
|
||||
'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. '
|
||||
@@ -97,7 +100,12 @@ def render(data):
|
||||
'the real ARM64 Frame repeats those lifecycle checks under the shared lock. Local Chromium with that live backend verifies '
|
||||
'Begin/Cancel, countdown and a silent completion toast. Simple Timer qualifies as works for silent in-panel session timers. '
|
||||
'Update/reinstall is required; old receipts stay partial. Sound/suspend are unsupported, closed-panel alerts and worn VR unverified. '
|
||||
'The private service, files, test listeners and processes were removed; independent cleanup audit passed and battery stayed 99 → 99.', '',
|
||||
'The private service, files, test listeners and processes were removed; independent cleanup audit passed and battery stayed 99 → 99. '
|
||||
'A [shared Toggle follow-up](evidence/framey-toggle-2026-10-27.md) separates standalone Toggle.value from ToggleField.checked. '
|
||||
'Nine official archives consume this API; their remaining blockers and status labels are preserved. '
|
||||
'Local opaque Chromium controls and the exact Pause Games frontend callback pass. Actual published Decky 3.3.0 discovery and '
|
||||
'private settings/exclusion-list persistence pass on amd64 and the ARM64 Frame through the normal CEF bridge. '
|
||||
'Process control and worn VR remain unverified; independent cleanup audit passed.', '',
|
||||
'## Every current plugin', '',
|
||||
'The API column shows manifest / actual bundled frontend API. Many manifests say v1 while their bundle connects API2. '
|
||||
'Missing manifest API is labelled legacy; an unrecognized bundle is unknown. Globals and hardware are static signals, not exhaustive dependencies.', '',
|
||||
@@ -118,11 +126,21 @@ def render(data):
|
||||
p['backend']+('; '+', '.join(native) if native else ''),p['hardware']]))+' |')
|
||||
lines += ['', '## Native payload portability', '',
|
||||
'All published Linux native payloads in this snapshot are x86_64; no published aarch64 payload was found. '
|
||||
'The three Windows DLLs belong to Decky-Framegen. The installer still rejects these archives. '
|
||||
'Apart from the explicitly described LocalSend experiment, ARM64 builds were not attempted: the constraints below remain unresolved.', '',
|
||||
'The three Windows DLLs belong to Decky-Framegen. Except for the exact managed DeckyFileServer and Decky Localsend adaptations below, the installer still rejects these archives. '
|
||||
'Apart from LocalSend and DeckyFileServer, ARM64 builds were not attempted: the constraints below remain unresolved.', '',
|
||||
'A [DeckyFileServer ARM64 follow-up](evidence/framey-fileserver-arm64-2026-10-27.md) ships an offline, digest-checked GPL helper build, corresponding vendored source and a build recipe. '
|
||||
'Only its exact managed archive can replace the x86 helper and remove its unnecessary root flag. Actual Decky 3.3.0 discovery/RPC and real Frame HTTPS private-fixture listing/download pass; uploads stay disabled. '
|
||||
'Hostname-DNS failures and empty optional arguments are repaired. Decky 3.3.0 still exposes `decky.HOME`; that call is unchanged. '
|
||||
'The plugin moves from broken to partial: its frontend advertises HTTP/steamdeck URLs and needs a Steam folder picker. Use HTTPS and a preconfigured directory. Uploads, generated thumbnails and worn VR remain unverified.', '',
|
||||
'[LocalSend ARM64 build/runtime evidence](evidence/framey-localsend-arm64-2026-10-26.md) '
|
||||
'covers a reproducible MIT helper build and its isolated real-Frame protocol 2.0 info response. '
|
||||
'The official x86 archive remains blocked; full plugin integration and file transfers are unverified.', '',
|
||||
'covers an isolated real-Frame protocol 2.0 info response from an earlier helper build. '
|
||||
'[Exact official source matching](evidence/framey-localsend-source-match-2026-10-27.md) '
|
||||
'now identifies the official helper source and a reproducible ARM64 build from that revision. '
|
||||
'The exact official x86 backend passed one local private-namespace transfer, and the rebuilt ARM64 helper passed one locked real-Frame private marker transfer. '
|
||||
'Published Decky 3.3.0 AMD64/ARM64 runtime checks discovered the adapted plugin and read its stopped status and configuration. '
|
||||
'Only the exact managed 0.4.5 archive can replace its x86 helper on Frame. '
|
||||
'The actual managed Decky service on Frame, user-facing transfer, local discovery and worn VR remain unverified '
|
||||
'([managed adaptation evidence](evidence/framey-localsend-managed-2026-10-27.md)).', '',
|
||||
'| Plugin | Native payloads | ARM64 result / remaining blocker |', '| --- | --- | --- |']
|
||||
for p in data['plugins']:
|
||||
if p['native']:
|
||||
|
||||
@@ -40,7 +40,8 @@ def check_coverage():
|
||||
raise ValueError('Managed shims must declare a literal name for runtime test coverage')
|
||||
shims.add(node.args[0].value)
|
||||
cases = json.loads((ROOT/'tests/decky-runtime/cases.json').read_text())
|
||||
if shims != {case['shim'] for case in cases}:
|
||||
# Frontend-only compatibility probes need no archive/backend adaptation.
|
||||
if shims != {case['shim'] for case in cases if case['shim'] is not None}:
|
||||
raise ValueError('Every managed shim needs an actual Decky runtime case')
|
||||
return cases
|
||||
|
||||
@@ -83,7 +84,8 @@ def main():
|
||||
loader.write_bytes(data)
|
||||
loader.chmod(0o644) # Private container user copies/chmods its own executable.
|
||||
probes = [(case['id'], None) for case in cases] + [
|
||||
(111, '--reproduce-bound-method-regression'), (96, '--reproduce-timer-regression')]
|
||||
(111, '--reproduce-bound-method-regression'), (96, '--reproduce-timer-regression'),
|
||||
(60, '--reproduce-fileserver-empty-flag-regression')]
|
||||
for plugin_id, negative in probes:
|
||||
name = 'framey-decky-'+uuid.uuid4().hex[:12]
|
||||
command = ['docker', 'run', '--rm', '--name', name, '--network', 'none', '--read-only',
|
||||
|
||||
@@ -56,7 +56,7 @@ if len(hits) != 1:
|
||||
sys.exit("found %s for %s; expected one draft" % (why, tag))
|
||||
r = hits[0]
|
||||
if not r.get("draft"):
|
||||
print("note: %s is already published; refreshing update.json and marking it latest" % tag, file=sys.stderr)
|
||||
sys.exit("not publishing: %s is already published; leave its live update manifest intact" % tag)
|
||||
json.dump(r, sys.stdout)
|
||||
EOF
|
||||
id=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["id"])' "$tmp/release.json")
|
||||
|
||||
@@ -77,19 +77,25 @@ def successful_jobs(run, required):
|
||||
attempt = run.get("run_attempt")
|
||||
if not isinstance(attempt, int) or attempt < 1:
|
||||
raise GateError(f"run {run['id']} has no valid attempt number")
|
||||
data = api(f"actions/runs/{run['id']}/attempts/{attempt}/jobs?per_page=100")
|
||||
jobs = data.get("jobs") or []
|
||||
if data.get("total_count") != len(jobs):
|
||||
raise GateError(f"run {run['id']} has more jobs than the gate inspected")
|
||||
names = [job.get("name") for job in jobs]
|
||||
if len(names) != len(set(names)) or not required.issubset(names):
|
||||
latest_jobs = None
|
||||
for number in range(1, attempt + 1):
|
||||
data = api(f"actions/runs/{run['id']}/attempts/{number}/jobs?per_page=100")
|
||||
jobs = data.get("jobs") or []
|
||||
if data.get("total_count") != len(jobs):
|
||||
raise GateError(f"run {run['id']} attempt {number} has more jobs than the gate inspected")
|
||||
names = [job.get("name") for job in jobs]
|
||||
if not jobs or len(names) != len(set(names)):
|
||||
raise GateError(f"run {run['id']} attempt {number} has missing or duplicate jobs")
|
||||
bad = [f"{job.get('name')}: {job.get('status')}/{job.get('conclusion')}"
|
||||
for job in jobs if job.get("status") != "completed" or job.get("conclusion") != "success"]
|
||||
if bad:
|
||||
raise GateError(f"run {run['id']} attempt {number} has unfinished, failed, cancelled, or skipped jobs: {bad}")
|
||||
latest_jobs = jobs
|
||||
names = [job.get("name") for job in latest_jobs]
|
||||
if not required.issubset(names):
|
||||
missing = sorted(required.difference(names))
|
||||
raise GateError(f"run {run['id']} has missing or duplicate jobs: {missing}")
|
||||
bad = [f"{job.get('name')}: {job.get('status')}/{job.get('conclusion')}"
|
||||
for job in jobs if job.get("status") != "completed" or job.get("conclusion") != "success"]
|
||||
if bad:
|
||||
raise GateError(f"run {run['id']} has unfinished, failed, cancelled, or skipped jobs: {bad}")
|
||||
print(f"gate passed: {run.get('html_url')} ({len(jobs)} jobs)")
|
||||
raise GateError(f"run {run['id']} has missing jobs in its latest attempt: {missing}")
|
||||
print(f"gate passed: {run.get('html_url')} ({len(latest_jobs)} jobs, {attempt} attempts)")
|
||||
|
||||
|
||||
def main(tag):
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
[
|
||||
{
|
||||
"id": 67,
|
||||
"shim": "battery-tracker-user-supply",
|
||||
"probe": "battery-tracker"
|
||||
},
|
||||
{
|
||||
"id": 114,
|
||||
"shim": "network-sentinel-user-psutil",
|
||||
"probe": "network-sentinel"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"shim": null,
|
||||
"probe": "toggle-settings"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"shim": "music-control-system-dbus-env",
|
||||
@@ -23,5 +38,15 @@
|
||||
"id": 96,
|
||||
"shim": "simple-timer-lifecycle",
|
||||
"probe": "timer-lifecycle"
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"shim": "deckyfileserver-user-native",
|
||||
"probe": "fileserver"
|
||||
},
|
||||
{
|
||||
"id": 125,
|
||||
"shim": "localsend-digest-native",
|
||||
"probe": "localsend"
|
||||
}
|
||||
]
|
||||
@@ -7,6 +7,8 @@ import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
@@ -59,6 +61,7 @@ def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('id', type=int)
|
||||
parser.add_argument('--reproduce-bound-method-regression', action='store_true')
|
||||
parser.add_argument('--reproduce-fileserver-empty-flag-regression', action='store_true')
|
||||
parser.add_argument('--reproduce-timer-regression', action='store_true')
|
||||
args = parser.parse_args()
|
||||
assert os.getuid() == 1100 and ROOT.is_dir(), 'Run through framey-test-decky-runtime.py'
|
||||
@@ -82,12 +85,12 @@ def main():
|
||||
Path('/tmp/frame-control-decky-plugin-runtime-test').write_bytes(data)
|
||||
install = subprocess.run([sys.executable, '-', 'install', 'runtime-test', str(record['id']),
|
||||
record['sha256'], record['folder'], record['version'], '-'], input=frame_decky_store.apply_script(record['id'], record['sha256']),
|
||||
capture_output=True, text=True, env=env, timeout=15)
|
||||
capture_output=True, text=True, env=env, timeout=30)
|
||||
assert install.returncode == 0, install.stderr
|
||||
plugin = base/'plugins'/record['folder']
|
||||
receipt = json.loads((plugin/'.frame-control-store.json').read_text())
|
||||
if not args.reproduce_timer_regression:
|
||||
assert receipt['compatShims'] == [case['shim']], receipt
|
||||
assert receipt.get('compatShims', []) == ([case['shim']] if case['shim'] else []), receipt
|
||||
else:
|
||||
assert record['id'] == 96
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as original:
|
||||
@@ -95,6 +98,11 @@ def main():
|
||||
# Exercise the production manifest's API convention, never import Plugin ourselves.
|
||||
manifest = json.loads((plugin/'plugin.json').read_text())
|
||||
assert manifest.get('api_version', 'legacy') == record['api_version'], manifest
|
||||
assert '_root' not in manifest.get('flags', []), manifest
|
||||
if args.reproduce_fileserver_empty_flag_regression:
|
||||
assert record['id'] == 60
|
||||
source = plugin/'main.py'
|
||||
source.write_text(source.read_text().replace('[arg for arg in [', '[').replace('] if arg],', '],'))
|
||||
if args.reproduce_bound_method_regression:
|
||||
assert record['id'] == 111
|
||||
source = (plugin/'main.py').read_text()
|
||||
@@ -111,6 +119,13 @@ def main():
|
||||
env.update(UNPRIVILEGED_PATH=str(base), PRIVILEGED_PATH=str(base), CHOWN_PLUGIN_PATH='0',
|
||||
LIVE_RELOAD='0', USE_CEF_CLOSE_WORKAROUND='0', KEEP_SYSTEMD_SERVICE='1',
|
||||
DBUS_SESSION_BUS_ADDRESS='unix:path=/run/user/1100/bus')
|
||||
if case['probe'] == 'battery-tracker':
|
||||
supply = home/'battery-supply'/'max1720x_bat_7-36'
|
||||
supply.mkdir(parents=True)
|
||||
for name, value in {'type':'Battery', 'capacity':'89', 'status':'Charging',
|
||||
'voltage_now':'8746718', 'current_now':'1030625'}.items():
|
||||
(supply/name).write_text(value+'\n')
|
||||
env['FRAMEY_BATTERY_ROOT'] = str(supply.parent)
|
||||
processes = []
|
||||
report = {'id': record['id'], 'name': record['name'], 'version': record['version'],
|
||||
'sha256': record['sha256'], 'shim': case['shim'], 'calls': []}
|
||||
@@ -139,7 +154,111 @@ def main():
|
||||
report['calls'].append({'method': method, 'result': response['result']})
|
||||
return response['result']
|
||||
|
||||
if case['probe'] == 'timer-lifecycle':
|
||||
def current(method, *values):
|
||||
response = rpc('loader/call_plugin_method', record['name'], method, *values)
|
||||
report['calls'].append({'method': method, 'result': response})
|
||||
return response
|
||||
|
||||
if case['probe'] == 'battery-tracker':
|
||||
empty_or_first = legacy('get_recent_data', lookback=2)
|
||||
assert isinstance(empty_or_first['x'], list) and isinstance(empty_or_first['cap'], list)
|
||||
assert isinstance(empty_or_first['power_data'], list)
|
||||
initial = until_ready(lambda: legacy('get_recent_data', lookback=2)['cap'], 'private first battery sample')
|
||||
assert initial[-1] == .89, initial
|
||||
assert legacy('set_app', app='Framey private') is True
|
||||
(supply/'status').write_text('Discharging\n')
|
||||
sample = until_ready(lambda: next((row for row in legacy('get_recent_data', lookback=2)['power_data']
|
||||
if row['name']=='Framey private'), None), 'private discharging battery sample')
|
||||
assert sample['average_power'] > 0, sample
|
||||
history = legacy('get_recent_data', lookback=2)
|
||||
assert len(history['x']) == len(history['cap']) >= 2, history
|
||||
report['result'] = 'discovery, Frame-shaped private battery samples, history and per-app power passed; real managed Frame backend unverified'
|
||||
elif case['probe'] == 'network-sentinel':
|
||||
assert current('get_settings')['auto_monitor'] is False
|
||||
assert current('get_network_history') == []
|
||||
assert current('get_live_ping') == 0
|
||||
assert current('get_bandwidth_stats') == {'download_bps': 0, 'upload_bps': 0}
|
||||
assert current('update_settings', {'show_bandwidth': False}) is True
|
||||
assert current('get_settings')['show_bandwidth'] is False
|
||||
connection = current('get_connection_info')
|
||||
assert isinstance(connection.get('hostname', connection.get('error')), str), connection
|
||||
status = current('get_network_status')
|
||||
assert status['quality']['quality'] == 'disconnected', status
|
||||
assert status['quality']['jitter'] == 0, status
|
||||
assert current('start_monitoring') is True
|
||||
history = until_ready(lambda: current('get_network_history'), 'isolated network sample')
|
||||
assert 'bandwidth' in history[0] and 'quality' in history[0], history
|
||||
assert current('stop_monitoring') is True
|
||||
report['result'] = 'discovery, native psutil, private settings and isolated monitoring loop passed; external latency/DNS and live Frame unverified'
|
||||
elif case['probe'] == 'localsend':
|
||||
helper = plugin/'bin/localsend-core'
|
||||
expected = ('e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae'
|
||||
if __import__('platform').machine() == 'aarch64'
|
||||
else 'd448d03861d578685e6c70ea3ace25eb34fca99cb9d4f98bf51ba23aa5cae843')
|
||||
assert hashlib.sha256(helper.read_bytes()).hexdigest() == expected
|
||||
status = current('get_backend_status')
|
||||
assert status == {'running': False, 'url': 'https://127.0.0.1:53317'}, status
|
||||
settings = current('get_backend_config')
|
||||
assert settings['use_https'] and not settings['auto_save'], settings
|
||||
assert settings['network_interface'] == '*', settings
|
||||
assert settings['download_folder'].startswith(str(base)), settings
|
||||
stopped = current('proxy_get', '/api/localsend/v2/info')
|
||||
assert stopped['status'] == 503 and stopped['data']['error'] == 'Backend not running', stopped
|
||||
report['result'] = 'published Decky discovery and public read-only settings/status RPC passed; network helper remained stopped'
|
||||
elif case['probe'] == 'fileserver':
|
||||
assert until_ready(lambda: current('get_port') == 8000, 'default port')
|
||||
assert current('get_directory') == str(home)
|
||||
assert current('get_server_running') is False
|
||||
fixture = home/'public-fixture'
|
||||
fixture.mkdir()
|
||||
marker = b'Framey public read-only fixture\n'
|
||||
(fixture/'marker.txt').write_bytes(marker)
|
||||
# A thumbnail-compatible file must remain a normal link when disabled.
|
||||
(fixture/'image.png').write_bytes(b'public placeholder; thumbnails disabled')
|
||||
with socket.socket() as listener:
|
||||
listener.bind(('127.0.0.1', 0))
|
||||
port = listener.getsockname()[1]
|
||||
status = current('set_status', {'directory': str(fixture), 'port': port, 'timeout': 1,
|
||||
'allow_uploads': False, 'disable_thumbnails': True, 'server_running': True})
|
||||
assert status['server_running'] and not status['allow_uploads'] and status['disable_thumbnails'], status
|
||||
context = ssl._create_unverified_context() # Upstream ships its public self-signed test cert.
|
||||
url = f'https://127.0.0.1:{port}'
|
||||
def fetch(path):
|
||||
return urllib.request.urlopen(url+path, context=context, timeout=3).read()
|
||||
listing = until_ready(lambda: fetch('/files/'), 'private file server')
|
||||
assert b'marker.txt' in listing and b'image.png' in listing, listing
|
||||
assert (b'/preview/files/image.png' in listing) == args.reproduce_fileserver_empty_flag_regression, listing
|
||||
assert fetch('/files/marker.txt') == marker
|
||||
try:
|
||||
fetch('/upload')
|
||||
raise AssertionError('uploads must be disabled')
|
||||
except urllib.error.HTTPError as error:
|
||||
assert error.code == 401, error.code
|
||||
assert current('set_server_running', False) is False
|
||||
assert current('get_server_running') is False
|
||||
assert current('get_history') == [str(fixture)]
|
||||
report['result'] = ('original empty argument bug reproduced: thumbnail policy ignored'
|
||||
if args.reproduce_fileserver_empty_flag_regression else
|
||||
'discovery, API1 settings, native HTTPS listing/download, thumbnail policy and stop passed; uploads disabled')
|
||||
elif case['probe'] == 'toggle-settings':
|
||||
def call(method, *values):
|
||||
result = rpc('loader/call_plugin_method', record['name'], method, *values)
|
||||
report['calls'].append({'method': method, 'result': result})
|
||||
return result
|
||||
|
||||
assert call('load_settings') == {}
|
||||
# Settings only. Never call pause/resume/kill or signal a process.
|
||||
for value in (True, False, True):
|
||||
call('save_setting', 'autoPause', value)
|
||||
assert call('load_settings')['autoPause'] is value
|
||||
call('add_no_auto_pause_set', 424242)
|
||||
assert call('load_settings')['noAutoPauseSet'] == [424242]
|
||||
call('remove_no_auto_pause_set', 424242)
|
||||
assert call('load_settings')['noAutoPauseSet'] == []
|
||||
settings = json.loads((base/'settings'/record['folder']/'settings.json').read_text())
|
||||
assert settings['autoPause'] is True and settings['noAutoPauseSet'] == []
|
||||
report['result'] = 'discovery and Toggle-backed private settings persistence passed; no process actions'
|
||||
elif case['probe'] == 'timer-lifecycle':
|
||||
from timer_lifecycle import probe
|
||||
|
||||
def restart(settings=None):
|
||||
|
||||
@@ -107,9 +107,16 @@ u=h/'.config/systemd/user/frame-control-decky.service'; u.parent.mkdir(parents=T
|
||||
self.assertEqual((catalogue[0]['version'], catalogue[0]['sha256']), ('1.0.0', HASHES[0]))
|
||||
status, _, _ = api('POST', '/api/decky/plugin', {'action':'install','id':999})
|
||||
self.assertEqual(status, 400) # Third-party code needs explicit acknowledgement.
|
||||
job = finished(ok('POST', '/api/decky/plugin',
|
||||
{'action':'install','id':999,'acknowledged':True}))
|
||||
request = {'action':'install','id':999,'acknowledged':True,
|
||||
'requestId':'0123456789abcdef' * 2}
|
||||
started = ok('POST', '/api/decky/plugin', request)
|
||||
self.assertEqual(ok('POST', '/api/decky/plugin', request)['job'], started['job'])
|
||||
status, conflict, _ = api('POST', '/api/decky/plugin', {**request, 'id':998})
|
||||
self.assertEqual(status, 409)
|
||||
self.assertIn('already used', conflict['error'])
|
||||
job = finished(started)
|
||||
self.assertFalse(job.get('error'), job)
|
||||
self.assertEqual(ok('POST', '/api/decky/plugin', request)['job'], started['job'])
|
||||
self.assertEqual(ok('GET', '/api/decky')['plugins'][0]['version'], '1.0.0')
|
||||
self.assertIn('Sample 1.0.0', ssh('cat ~/.local/share/frame-control/decky/plugins/sample-panel/dist/index.js'))
|
||||
receipt = HASHES[0]
|
||||
@@ -195,6 +202,21 @@ u=h/'.config/systemd/user/frame-control-decky.service'; u.parent.mkdir(parents=T
|
||||
self.assertEqual(
|
||||
ssh(f'sha256sum {installed}/dist/{chunk}').split()[0],
|
||||
hashlib.sha256(expected_chunk).hexdigest())
|
||||
if plugin['id'] == 60:
|
||||
self.assertEqual(json.loads(ssh(f'cat {installed}/plugin.json'))['flags'], [])
|
||||
receipt = json.loads(ssh(f'cat {installed}/.frame-control-store.json'))
|
||||
self.assertEqual(receipt['compatShims'], ['deckyfileserver-user-native'])
|
||||
import frame_decky_store
|
||||
arch = ssh('uname -m').strip()
|
||||
self.assertEqual(ssh(f'sha256sum {installed}/bin/backend').split()[0],
|
||||
frame_decky_store.FILESERVER_BINARIES[arch])
|
||||
if plugin['id'] == 125:
|
||||
receipt = json.loads(ssh(f'cat {installed}/.frame-control-store.json'))
|
||||
self.assertEqual(receipt['compatShims'], ['localsend-digest-native'])
|
||||
arch = ssh('uname -m').strip()
|
||||
self.assertEqual(ssh(f'sha256sum {installed}/bin/localsend-core').split()[0],
|
||||
frame_decky_store.LOCALSEND_AARCH64 if arch == 'aarch64'
|
||||
else frame_decky_store.LOCALSEND_ORIGINAL)
|
||||
if plugin['id'] == 53:
|
||||
self.assertEqual(ssh(f'test -f {installed}/py_modules/click.py && echo yes').strip(), 'yes')
|
||||
receipt = json.loads(ssh(f'cat {installed}/.frame-control-store.json'))
|
||||
@@ -203,6 +225,17 @@ u=h/'.config/systemd/user/frame-control-decky.service'; u.parent.mkdir(parents=T
|
||||
receipt = json.loads(ssh(f'cat {installed}/.frame-control-store.json'))
|
||||
self.assertEqual(receipt['compatShims'], ['deckyspy-psutil-and-battery'])
|
||||
self.assertEqual(ssh(f'test -f {installed}/py_modules/psutil/_psutil_linux.abi3.so && test -f {installed}/py_modules/framey_battery.py && echo yes').strip(), 'yes')
|
||||
if plugin['id'] == 67:
|
||||
receipt = json.loads(ssh(f'cat {installed}/.frame-control-store.json'))
|
||||
self.assertEqual(receipt['compatShims'], ['battery-tracker-user-supply'])
|
||||
self.assertEqual(found[0]['compatibility']['status'], 'partial')
|
||||
self.assertEqual(json.loads(ssh(f'cat {installed}/plugin.json'))['flags'], [])
|
||||
if plugin['id'] == 114:
|
||||
receipt = json.loads(ssh(f'cat {installed}/.frame-control-store.json'))
|
||||
self.assertEqual(receipt['compatShims'], ['network-sentinel-user-psutil'])
|
||||
self.assertEqual(found[0]['compatibility']['status'], 'partial')
|
||||
self.assertEqual(json.loads(ssh(f'cat {installed}/plugin.json'))['flags'], ['debug'])
|
||||
self.assertEqual(ssh(f'test -f {installed}/py_modules/psutil/_psutil_linux.abi3.so && echo yes').strip(), 'yes')
|
||||
if plugin['id'] in (12, 111):
|
||||
receipt = json.loads(ssh(f'cat {installed}/.frame-control-store.json'))
|
||||
self.assertEqual(receipt['compatShims'], ['music-control-system-dbus-env'])
|
||||
|
||||
+8
-3
@@ -91,17 +91,22 @@ elif method == "GET" and endpoint.startswith(repo + "actions/workflows/"):
|
||||
is_main = workflow != "release.yml"
|
||||
run_id = {"checks.yml": 501, "release.yml": 502, "decky-runtime.yml": 503}[workflow]
|
||||
run = {"id": run_id, "head_sha": gate.get("main_sha" if is_main else "release_sha", commit),
|
||||
"head_branch": branch, "event": "push", "run_attempt": 1,
|
||||
"head_branch": branch, "event": "push", "run_attempt": gate.get("release_attempts", 1) if run_id == 502 else 1,
|
||||
"status": gate.get("decky_status" if run_id == 503 else ("main_status" if is_main else "release_status"), "completed"),
|
||||
"conclusion": gate.get("decky_conclusion" if run_id == 503 else ("main_conclusion" if is_main else "release_conclusion"), "success"),
|
||||
"html_url": "https://github.com/saphid/frame-control/actions/runs/%s" % run_id}
|
||||
print(json.dumps({"workflow_runs": [run]}))
|
||||
elif method == "GET" and endpoint.startswith(repo + "actions/runs/"):
|
||||
run_id = endpoint[len(repo + "actions/runs/"):].split("/")[0]
|
||||
parts = endpoint[len(repo + "actions/runs/"):].split("/")
|
||||
run_id = parts[0]
|
||||
attempt = int(parts[2])
|
||||
names = {"501": main_jobs, "502": release_jobs, "503": decky_jobs}[run_id]
|
||||
names = [name for name in names if name != gate.get("omit_job")]
|
||||
jobs = [{"name": name, "status": "completed",
|
||||
"conclusion": gate.get("bad_job_conclusion", "success") if name == gate.get("bad_job") else "success"}
|
||||
"conclusion": (gate.get("prior_bad_job_conclusion", "failure")
|
||||
if run_id == "502" and attempt == 1 and name == gate.get("prior_bad_job")
|
||||
else gate.get("bad_job_conclusion", "success") if name == gate.get("bad_job")
|
||||
else "success")}
|
||||
for name in names]
|
||||
print(json.dumps({"total_count": len(jobs), "jobs": jobs}))
|
||||
elif method == "GET" and endpoint.startswith(repo + "releases?"):
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+32
-2
@@ -1,13 +1,20 @@
|
||||
# Pinned Decky store test archives
|
||||
|
||||
These are unmodified ZIPs downloaded from the official Decky store CDN on
|
||||
2026-10-25, except `96.zip` and `111.zip` downloaded on 2026-10-26. Each ZIP includes its
|
||||
upstream `LICENSE`. They are test data, not plugins shipped or installed by
|
||||
2026-10-25, except `96.zip` and `111.zip` downloaded on 2026-10-26 and `60.zip` on
|
||||
2026-10-27; `13.zip` and `125.zip` were added from the digest-verified audit cache on 2026-10-27,
|
||||
and `67.zip` and `114.zip` on 2026-10-28.
|
||||
Each ZIP includes its upstream `LICENSE`. They are test data, not plugins shipped or installed by
|
||||
Frame Control. The fake Frame tests serve them
|
||||
from a local fixture so CI does not depend on the store being online.
|
||||
|
||||
| File | Official plugin | Version | Store SHA-256 |
|
||||
| --- | --- | --- | --- |
|
||||
| `13.zip` | Pause Games | 1.0.3 | `4aefdfb0ef25dea9c6d9fd037731c8b99ee8a1be966c2f7d5b0602112ef29f62` |
|
||||
| `60.zip` | DeckyFileServer | 1.2.0 | `7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0` |
|
||||
| `125.zip` | Decky Localsend | 0.4.5 | `e271ccb55eb23f918b99a69f79aec905df4c9b2ef7b23bbf4ec1c2d1a6f0c6b7` |
|
||||
| `114.zip` | Network Sentinel | 1.0.4 | `d3e07f40a0c800e72d15594e03c8c61e9bf3092524a578a0260520e735b971a5` |
|
||||
| `67.zip` | Battery Tracker | 0.2.0 | `91c388d3c1b52079314045b79671ad55c767510658b6193f6936ed6cad383b94` |
|
||||
| `96.zip` | Simple Timer | 1.0.0 | `973d927e9d9a1feec04679c564af7e389a729b6735d2a988ffc2cb46ce4eb312` |
|
||||
| `7.zip` | CSS Loader | 2.1.3 | `99beaf1374af82408e6d056e0b0147e5fca544e1035fa7ab88419231697bc231` |
|
||||
| `12.zip` | MusicControl | 1.1.6 | `246b89fd653c60a735b1cc401d1b0937d5cb969eea9024561661ff741f081d62` |
|
||||
@@ -48,3 +55,26 @@ RPC and events in the native isolated runtime gate. Cancel/replace/completion an
|
||||
active/canceled/expired backend restarts are regression checks; the unmodified
|
||||
archive must reproduce its historical lifecycle errors. Only private silent
|
||||
timers/settings are used. The upstream BSD license stays unchanged.
|
||||
|
||||
Pause Games runs through actual published Decky 3.3.0 discovery and instance RPC
|
||||
for private settings and a synthetic exclusion list. No process-control method
|
||||
is called. The official frontend's standalone Toggle component separately
|
||||
checks saved true/false state and matching settings callbacks; effects are inert.
|
||||
Network Sentinel runs through the same isolated native Decky gate. Its exact
|
||||
archive gets plugin-local psutil and a user-service manifest; the probe checks
|
||||
discovery, private settings and one monitoring-loop sample without external
|
||||
network access. It does not verify real network latency or worn VR.
|
||||
Battery Tracker's exact archive also has a user-service adaptation. The native
|
||||
Decky gate checks discovery, empty history, Frame-shaped private battery
|
||||
samples and per-app power aggregation. The real Frame probe checked only the
|
||||
readable Battery-type sysfs fields; the plugin itself did not run there.
|
||||
DeckyFileServer also runs through actual Decky 3.3.0 discovery/API1 RPC with a
|
||||
native helper replacement. HTTPS listing/download use only public markers in a
|
||||
private directory; uploads remain disabled. The exact source build, GPL licenses
|
||||
and reproducible recipe are in [the native assets](../../../ui/vendor/deckyfileserver-1.2.0/README.md).
|
||||
The original empty optional-argument bug is reproduced by a negative runtime case.
|
||||
Decky Localsend's exact original x86 backend/proxy passed an isolated private
|
||||
marker transfer outside Decky. The native Decky gate checks discovery and
|
||||
read-only backend status/configuration with its network helper stopped. The
|
||||
managed ARM64 helper and transfer remain separately gated; the fixture does
|
||||
not prove worn VR or a transfer on Frame.
|
||||
@@ -4,6 +4,16 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).parent / 'fixtures/framey-official'
|
||||
PLUGINS = (
|
||||
{'id': 67, 'name': 'Battery Tracker', 'version': '0.2.0', 'api_version': 'legacy',
|
||||
'folder': 'steam-deck-battery-tracker', 'sha256': '91c388d3c1b52079314045b79671ad55c767510658b6193f6936ed6cad383b94'},
|
||||
{'id': 114, 'name': 'Network Sentinel', 'version': '1.0.4', 'api_version': 1,
|
||||
'folder': 'network-sentinel', 'sha256': 'd3e07f40a0c800e72d15594e03c8c61e9bf3092524a578a0260520e735b971a5'},
|
||||
{'id': 13, 'name': 'Pause Games', 'version': '1.0.3', 'api_version': 1,
|
||||
'folder': 'SDH-PauseGames', 'sha256': '4aefdfb0ef25dea9c6d9fd037731c8b99ee8a1be966c2f7d5b0602112ef29f62'},
|
||||
{'id': 60, 'name': 'DeckyFileServer', 'version': '1.2.0', 'api_version': 1,
|
||||
'folder': 'DeckyFileServer', 'sha256': '7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0'},
|
||||
{'id': 125, 'name': 'Decky Localsend', 'version': '0.4.5', 'api_version': 1,
|
||||
'folder': 'decky-localsend', 'sha256': 'e271ccb55eb23f918b99a69f79aec905df4c9b2ef7b23bbf4ec1c2d1a6f0c6b7'},
|
||||
{'id': 89, 'name': 'DeckySpy', 'version': '0.6.7', 'api_version': 'legacy',
|
||||
'folder': 'decky-spy', 'sha256': '2d431a89826ed5cf3fd37258373eb3b46c841c39795aa8b25f83e59f4eee87c7'},
|
||||
{'id': 96, 'name': 'Simple Timer', 'version': '1.0.0', 'api_version': 1,
|
||||
|
||||
@@ -46,6 +46,22 @@ const nodes=(tree,type)=>Array.isArray(tree)?tree.flatMap(child=>nodes(child,typ
|
||||
[...(tree?.type===type?[tree]:[]),...(tree?.children||[]).flatMap(child=>nodes(child,type))];
|
||||
// Disabled controls stay inert even if a caller invokes their handlers directly.
|
||||
let changed=0;
|
||||
// DFL.Toggle is a standalone value-controlled input. ToggleField uses checked.
|
||||
const toggleChanges=[];
|
||||
for(const value of [true,false]) {
|
||||
const input=nodes(window.DFL.Toggle({value,onChange:v=>toggleChanges.push(v)}),'input')[0];
|
||||
assert.equal(input.props.checked,value,'Toggle must render its value prop');
|
||||
input.props.onChange({target:{checked:!value}});
|
||||
}
|
||||
assert.deepEqual(toggleChanges,[false,true]);
|
||||
const disabledToggle=nodes(window.DFL.Toggle({value:true,disabled:true,
|
||||
onChange:()=>changed++}),'input')[0];
|
||||
assert.equal(disabledToggle.props.checked,true);
|
||||
assert.equal(disabledToggle.props.disabled,true);
|
||||
disabledToggle.props.onChange({target:{checked:false}});
|
||||
assert.equal(changed,0);
|
||||
const fieldToggle=nodes(window.DFL.ToggleField({checked:true,value:false}),'input')[0];
|
||||
assert.equal(fieldToggle.props.checked,true,'ToggleField must retain its checked contract');
|
||||
const toggle=nodes(window.DFL.ToggleField({label:'Enabled',checked:true,disabled:true,
|
||||
onChange:()=>changed++}),'input')[0];
|
||||
const options=[{label:'Modes',options:[{label:'Slow',data:'slow'},{label:'Fast',data:'fast'}]}];
|
||||
@@ -472,11 +488,47 @@ assert.equal(window.DFL.useParams().view,'art');
|
||||
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123/art/extra'),false);
|
||||
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123?admin=1'),false);
|
||||
assert.equal(window.DFL.Navigation.Navigate('/plugin/game//art'),false);
|
||||
assert.equal(window.DFL.Navigation.NavigateBack(),true);
|
||||
assert.equal(window.DFL.useParams().appid,'123');
|
||||
assert.equal(window.DFL.useParams().view,undefined,'Back returns to the previous local page');
|
||||
assert.equal(window.DFL.Navigation.NavigateBack(),true);
|
||||
assert.equal(window.DFL.useParams().appid,undefined,'A second Back returns to the plugin root');
|
||||
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123'),true);
|
||||
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123'),true);
|
||||
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123/art'),true);
|
||||
assert.equal(window.DFL.Navigation.NavigateBack(),true);
|
||||
assert.equal(window.DFL.useParams().view,undefined,'Navigating to the same page did not add a history entry');
|
||||
assert.equal(window.DFL.Navigation.NavigateBack(),true);
|
||||
assert.equal(window.DFL.useParams().appid,undefined);
|
||||
assert.throws(()=>api.routerHook.addRoute('/plugin/:id/:id',GameRoute));
|
||||
assert.throws(()=>api.routerHook.addRoute('/plugin/:id?/art',GameRoute));
|
||||
api.routerHook.addRoute('/plugin/other',LocalRoute);
|
||||
assert.equal(window.DFL.Navigation.Navigate('/plugin/game/123'),true);
|
||||
assert.equal(window.DFL.Navigation.Navigate('/plugin/other'),true);
|
||||
removeGame();
|
||||
assert.equal(window.DFL.Navigation.NavigateBack(),true);
|
||||
assert.equal(nodes(window.__surface({plugin}),LocalRoute).length,0,
|
||||
'Back skips a route that the plugin unregistered');
|
||||
assert.equal(window.DFL.useParams().appid,undefined);
|
||||
assert.equal(nodes(window.__surface({plugin}),GameRoute).length,0);
|
||||
api.routerHook.removeRoute('/plugin/other');
|
||||
api.routerHook.addRoute('/plugin/first',LocalRoute);
|
||||
api.routerHook.addRoute('/plugin/second',LocalRoute);
|
||||
for(let index=0;index<40;index++)
|
||||
assert.equal(window.DFL.Navigation.Navigate(index%2 ? '/plugin/first' : '/plugin/second'),true);
|
||||
for(let index=0;index<32;index++)window.DFL.Navigation.NavigateBack();
|
||||
assert.equal(nodes(window.__surface({plugin}),LocalRoute).length,1);
|
||||
window.DFL.Navigation.NavigateBack();
|
||||
assert.equal(nodes(window.__surface({plugin}),LocalRoute).length,0,
|
||||
'local route history is bounded and falls back to the plugin root');
|
||||
window.DFL.Navigation.Navigate('/plugin/first');
|
||||
window.DFL.Navigation.Navigate('/plugin/second');
|
||||
window.DFL.Navigation.CloseSideMenus();
|
||||
window.DFL.Navigation.NavigateBack();
|
||||
assert.equal(nodes(window.__surface({plugin}),LocalRoute).length,0,
|
||||
'CloseSideMenus clears the local route history');
|
||||
api.routerHook.removeRoute('/plugin/first');
|
||||
api.routerHook.removeRoute('/plugin/second');
|
||||
|
||||
// Event subscription is acknowledged before a method that immediately emits.
|
||||
const onMessage=listeners.find(x=>x.type==='message').handler;
|
||||
|
||||
@@ -8,6 +8,7 @@ const bundle=readFileSync(new URL('../../examples/framey-settings/dist/index.js'
|
||||
assert.match(bundle,/SP_JSX\.jsx/);
|
||||
const React={Fragment:Symbol('fragment'),Component:class {},
|
||||
createElement:(type,props,...children)=>({type,props:props||{},children}),
|
||||
cloneElement:(element,props)=>({...element,props:{...element.props,...props}}),
|
||||
useState:value=>[value,()=>{}],useRef:value=>({current:value}),
|
||||
useEffect(){},isValidElement:value=>!!value?.type};
|
||||
class Element {
|
||||
@@ -79,6 +80,28 @@ toggle.props.onChange(false);
|
||||
await new Promise(resolve=>setImmediate(resolve));
|
||||
assert.equal(calls.filter(call=>call.method==='set_setting').length,2,
|
||||
'a completed save must release the sample setting lock');
|
||||
// The sample reset is a real Framey modal: Cancel cannot write, OK writes the
|
||||
// sample setting only after explicit confirmation.
|
||||
const reset=content.children[0][2].children[0];
|
||||
const setCalls=()=>calls.filter(call=>call.method==='set_setting');
|
||||
const dialog=()=>roots.get(elements.get('framey-modal')).value[0].children[1];
|
||||
reset.props.onClick();
|
||||
reset.props.onClick();
|
||||
assert.equal(roots.get(elements.get('framey-modal')).value.length,1,
|
||||
'a rapid second tap must not stack reset dialogs');
|
||||
assert.equal(dialog().props.strTitle,'Reset example setting?');
|
||||
let rendered=dialog().type(dialog().props);
|
||||
rendered.children.find(child=>child?.children?.[0]==='Keep setting').props.onClick();
|
||||
await new Promise(resolve=>setImmediate(resolve));
|
||||
assert.equal(setCalls().length,2,'cancel leaves the saved value untouched');
|
||||
assert.equal(roots.get(elements.get('framey-modal')).value,null);
|
||||
reset.props.onClick();
|
||||
rendered=dialog().type(dialog().props);
|
||||
rendered.children.find(child=>child?.children?.[0]==='Reset').props.onClick();
|
||||
await new Promise(resolve=>setImmediate(resolve));
|
||||
assert.equal(setCalls().length,3);
|
||||
assert.deepEqual(Array.from(setCalls().at(-1).args),['enabled',false]);
|
||||
assert.equal(roots.get(elements.get('framey-modal')).value,null);
|
||||
assert.equal(window.DFL.Navigation.Navigate('/framey-sample/details/saved-setting'),true);
|
||||
assert.equal(window.DFL.useParams().topic,'saved-setting');
|
||||
assert.equal(window.DFL.Navigation.Navigate('/library/home'),false);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {webcrypto} from 'node:crypto';
|
||||
import {readFileSync} from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
@@ -17,6 +18,7 @@ const document = {getElementById(id) {if(!elements.has(id)) elements.set(id,new
|
||||
createElement() {return new Element();}, hidden:false};
|
||||
const session = new Map();
|
||||
const ctx = vm.createContext({document, location:{hash:'',pathname:'/'},
|
||||
crypto:webcrypto,
|
||||
sessionStorage:{getItem:key=>session.get(key)||'',setItem:(key,value)=>session.set(key,value),removeItem:key=>session.delete(key)},
|
||||
history:{replaceState(){}}, confirm:()=>true,
|
||||
window:{addEventListener(){}}, setInterval(){}, setTimeout(){},
|
||||
@@ -61,6 +63,45 @@ assert.equal(el('deckyJobProgress').value,100);
|
||||
assert.equal(el('deckyJobCheck').hidden,true);
|
||||
assert.equal(session.has('frameyPluginJob'),false);
|
||||
|
||||
// If the server accepts the POST but its response is lost, the panel already
|
||||
// has the ID needed to find the job. It must not offer a second install.
|
||||
let lostJobId, lostPosts=0;
|
||||
ctx.mockApi=async (path,body) => {
|
||||
if(path==='/api/decky/plugin'){
|
||||
lostPosts++; lostJobId=body.requestId;
|
||||
assert.match(lostJobId,/^[0-9a-f]{32}$/);
|
||||
throw Error('POST response lost');
|
||||
}
|
||||
if(path.startsWith('/api/job')){
|
||||
assert.equal(new URL(path,'http://localhost').searchParams.get('id'),lostJobId);
|
||||
return {done:true,message:'Recovered install'};
|
||||
}
|
||||
if(path==='/api/decky')return {managed:true,running:true,plugins:[]};
|
||||
throw Error(`unexpected ${path}`);
|
||||
};
|
||||
vm.runInContext('api=mockApi',ctx);
|
||||
await vm.runInContext('deckyChange("install",{id:2,name:"Plugin 2",sha256:"a".repeat(64)})',ctx);
|
||||
assert.equal(JSON.parse(session.get('frameyPluginJob')).job,lostJobId);
|
||||
assert.equal(el('deckyJobCheck').hidden,false);
|
||||
await vm.runInContext('deckyChange("install",{id:2,name:"Plugin 2",sha256:"a".repeat(64)})',ctx);
|
||||
assert.equal(lostPosts,1);
|
||||
await vm.runInContext('checkDeckyJob()',ctx);
|
||||
assert.equal(el('deckyJobStatus').textContent,'Recovered install');
|
||||
assert.equal(session.has('frameyPluginJob'),false);
|
||||
|
||||
// A definite validation rejection never reserved a job on the server.
|
||||
ctx.mockApi=async path => {
|
||||
if(path==='/api/decky/plugin'){
|
||||
const error=Error('Invalid reviewed store version');error.status=400;throw error;
|
||||
}
|
||||
if(path==='/api/decky')return {managed:true,running:true,plugins:[]};
|
||||
throw Error(`unexpected ${path}`);
|
||||
};
|
||||
vm.runInContext('api=mockApi',ctx);
|
||||
await vm.runInContext('deckyChange("install",{id:2,name:"Plugin 2",sha256:"bad"})',ctx);
|
||||
assert.equal(session.has('frameyPluginJob'),false);
|
||||
assert.equal(el('deckyJobCheck').hidden,true);
|
||||
|
||||
// A stopped Decky service must still offer recovery by removing its owned plugin.
|
||||
ctx.mockApi=async path => path==='/api/decky' ?
|
||||
{managed:true,running:false,plugins:[{storeId:2,name:'Plugin 2',version:'1.0'}]} :
|
||||
@@ -149,7 +190,7 @@ ctx.mockApi=async path => {
|
||||
};
|
||||
vm.runInContext('api=mockApi',ctx);
|
||||
await vm.runInContext('checkDeckyJob()',ctx);
|
||||
assert.match(el('deckyJobStatus').textContent,/app restarted and lost this job result/);
|
||||
assert.match(el('deckyJobStatus').textContent,/server has no record of this request/);
|
||||
assert.equal(el('deckyJobCheck').hidden,true);
|
||||
assert.equal(session.has('frameyPluginJob'),false);
|
||||
assert.equal(el('deckyStoreList').children[0].children[1].disabled,false);
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Exact official Pause Games component and settings callbacks; effects/process
|
||||
// actions never execute here. Actual Decky discovery/RPC has a separate gate.
|
||||
import assert from 'node:assert/strict';
|
||||
import {readFileSync} from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const bundle=readFileSync(process.argv[2],'utf8');
|
||||
const official=bundle.replace(/export \{ index as default \};/,
|
||||
'window.__toggleTest = {AppItem, Settings};');
|
||||
assert.notEqual(official,bundle);
|
||||
const source=readFileSync(new URL('../../ui/framey-host.js',import.meta.url),'utf8');
|
||||
const host=source.replace(/\n start\(\);\n\}\)\(\);\s*$/, '\n})();');
|
||||
assert.notEqual(host,source);
|
||||
const React={Fragment:'fragment',Component:class {},
|
||||
createElement:(type,props,...children)=>({type,props:props||{},children}),
|
||||
createContext:()=>({}),useState:value=>[value,()=>{}],useEffect(){},
|
||||
useRef:value=>({current:value}),useMemo:fn=>fn(),useCallback:fn=>fn};
|
||||
const messages=[],listeners=[];
|
||||
const parent={postMessage:message=>messages.push(message)};
|
||||
const window={React,ReactDOM:{createRoot:()=>({render(){}})},location:{hash:''}};
|
||||
const context=vm.createContext({window,parent,navigator:{language:'en'},
|
||||
document:{getElementById:()=>({})},console,URL,Blob,
|
||||
addEventListener:(type,handler)=>listeners.push({type,handler}),setTimeout:()=>1,clearTimeout(){}});
|
||||
vm.runInContext(host,context);
|
||||
Object.assign(context,{DFL:window.DFL,SP_REACT:React});
|
||||
vm.runInContext(official,context,{timeout:2000});
|
||||
const {AppItem,Settings}=window.__toggleTest;
|
||||
const find=(node,type)=>node?.type===type?node:(node?.children||[]).flat(Infinity)
|
||||
.map(child=>find(child,type)).find(Boolean);
|
||||
for(const excluded of [false,true]) {
|
||||
vm.runInContext(`window.__toggleTest.Settings.data.noAutoPauseSet=new Set(${excluded?'[424242]':'[]'});`,context);
|
||||
const row=AppItem({app:{appid:'424242',display_name:'Private fixture'},autoPause:true});
|
||||
const toggle=find(row,window.DFL.Toggle);
|
||||
assert.ok(toggle);
|
||||
const input=window.DFL.Toggle(toggle.props);
|
||||
assert.equal(input.props.checked,!excluded,'official persisted exclusion must display correctly');
|
||||
input.props.onChange({target:{checked:excluded}});
|
||||
const call=messages.at(-1);
|
||||
assert.equal(call.type,'framey:call');
|
||||
assert.equal(call.method,excluded?'remove_no_auto_pause_set':'add_no_auto_pause_set');
|
||||
assert.deepEqual(Array.from(call.args),[424242]);
|
||||
assert.equal(Settings.data.noAutoPauseSet.has(424242),!excluded);
|
||||
listeners.find(x=>x.type==='message').handler({source:parent,
|
||||
data:{type:'framey:reply',id:call.id,result:null}});
|
||||
}
|
||||
assert.equal(messages.filter(x=>x.type==='framey:call').length,2);
|
||||
@@ -0,0 +1,33 @@
|
||||
// Exercise the real page functions for desktop direct copy and browser size guard.
|
||||
import { readFileSync } from "fs";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const src = readFileSync(new URL("../../ui/index.html", import.meta.url), "utf8");
|
||||
const lines = src.split("\n");
|
||||
function grab(name) {
|
||||
const start = lines.findIndex(line => line.startsWith(`function ${name}(`));
|
||||
assert.notEqual(start, -1, `${name} exists`);
|
||||
let depth = 0;
|
||||
for (let i = start; i < lines.length; i++) {
|
||||
for (const c of lines[i]) depth += c === "{" ? 1 : c === "}" ? -1 : 0;
|
||||
if (depth === 0) return lines.slice(start, i + 1).join("\n");
|
||||
}
|
||||
throw new Error(`${name} is unbalanced`);
|
||||
}
|
||||
|
||||
const calls = [];
|
||||
const copy = new Function("api", "upload", grab("copyToDownloads") + "\nreturn copyToDownloads;")(
|
||||
async (...args) => { calls.push(["api", ...args]); return { message: "sent" }; },
|
||||
async (...args) => { calls.push(["upload", ...args]); return { message: "uploaded" }; });
|
||||
const large = { name: "usb-file.bin", size: 27 * 1024 ** 3 };
|
||||
assert.deepEqual(await copy(large, "/media/usb/usb-file.bin", "frame-a"), { message: "sent" });
|
||||
assert.deepEqual(calls.pop(), ["api", "/api/transfer", { action: "copy-local", path: "/media/usb/usb-file.bin" }, "frame-a"]);
|
||||
assert.deepEqual(await copy({ name: "small.bin", size: 100 }, "", "frame-a"), { message: "uploaded" });
|
||||
assert.deepEqual(calls.pop(), ["upload", { name: "small.bin", size: 100 }, "push", "frame-a"]);
|
||||
|
||||
let opened = false;
|
||||
const browserUpload = new Function("XMLHttpRequest", grab("upload") + "\nreturn upload;")(
|
||||
class { constructor() { opened = true; } });
|
||||
await assert.rejects(browserUpload(large, "push", "frame-a"), /limited to 8 GiB/);
|
||||
assert.equal(opened, false, "oversized browser file is refused before creating a request");
|
||||
console.log("ok");
|
||||
@@ -6,8 +6,9 @@ BUILD_ID in tests/smoke/results/<time>-<BUILD_ID>.json (git-ignored):
|
||||
|
||||
Before any status read or title change, it atomically owns /tmp/frame-test.lock
|
||||
on the Frame and checks SteamVR standby, proximity and battery read-only. A
|
||||
busy lock, awake headset or battery below 20% stops the run. Only this run's
|
||||
lock is released, including after test failures.
|
||||
busy lock, awake headset, missing or active-range proximity reading, or battery
|
||||
below 20% stops the run. Only this run's lock is released, including after
|
||||
test failures.
|
||||
|
||||
- properties.json from Valve's devkit service names the login user;
|
||||
- status (ui/frame_status.py) reads the build, battery and storage;
|
||||
@@ -107,6 +108,11 @@ def preflight():
|
||||
state = json.loads(result.stdout)
|
||||
if state.get('activity') != 3:
|
||||
raise RuntimeError('Frame is awake or SteamVR activity is unknown; active session untouched')
|
||||
proximity = state.get('proximity')
|
||||
if (not isinstance(proximity, list) or not proximity or
|
||||
any(not isinstance(value, (int, float)) or isinstance(value, bool) or
|
||||
value < 0 or value > 10 for value in proximity)):
|
||||
raise RuntimeError('Frame proximity is active or unknown; active session untouched')
|
||||
battery = state.get('battery')
|
||||
if not battery or min(battery) < 20:
|
||||
raise RuntimeError('Frame battery is unavailable or below the smoke threshold')
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
"""Official Decky store transport and owned plugin transaction."""
|
||||
import sandbox # noqa: F401
|
||||
import hashlib
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
import warnings
|
||||
@@ -65,6 +69,33 @@ class ArchiveTests(unittest.TestCase):
|
||||
return_value=io.BytesIO(json.dumps(records).encode())):
|
||||
self.assertEqual([(p['id'], p['root']) for p in frame_decky_store.catalog()], [(42, True)])
|
||||
|
||||
def test_only_exact_fileserver_adaptation_clears_store_root(self):
|
||||
records = [{'id': 60, 'name': 'DeckyFileServer', 'tags': ['root'],
|
||||
'versions': [{'name': '1.2.0', 'hash': frame_decky_store.FILESERVER_DIGEST}]}]
|
||||
with patch.object(frame_decky_store.urllib.request, 'urlopen', return_value=io.BytesIO(json.dumps(records).encode())):
|
||||
self.assertFalse(frame_decky_store.catalog()[0]['root'])
|
||||
records[0]['versions'][0]['hash'] = 'a'*64
|
||||
with patch.object(frame_decky_store.urllib.request, 'urlopen', return_value=io.BytesIO(json.dumps(records).encode())):
|
||||
self.assertTrue(frame_decky_store.catalog()[0]['root'])
|
||||
|
||||
def test_only_exact_network_sentinel_adaptation_clears_store_root(self):
|
||||
records = [{'id': 114, 'name': 'Network Sentinel', 'tags': ['_root'],
|
||||
'versions': [{'name': '1.0.4', 'hash': frame_decky_store.NETWORK_SENTINEL_DIGEST}]}]
|
||||
with patch.object(frame_decky_store.urllib.request, 'urlopen', return_value=io.BytesIO(json.dumps(records).encode())):
|
||||
self.assertFalse(frame_decky_store.catalog()[0]['root'])
|
||||
records[0]['versions'][0]['hash'] = 'a'*64
|
||||
with patch.object(frame_decky_store.urllib.request, 'urlopen', return_value=io.BytesIO(json.dumps(records).encode())):
|
||||
self.assertTrue(frame_decky_store.catalog()[0]['root'])
|
||||
|
||||
def test_only_exact_battery_tracker_adaptation_clears_store_root(self):
|
||||
records = [{'id': 67, 'name': 'Battery Tracker', 'tags': ['_root'],
|
||||
'versions': [{'name': '0.2.0', 'hash': frame_decky_store.BATTERY_TRACKER_DIGEST}]}]
|
||||
with patch.object(frame_decky_store.urllib.request, 'urlopen', return_value=io.BytesIO(json.dumps(records).encode())):
|
||||
self.assertFalse(frame_decky_store.catalog()[0]['root'])
|
||||
records[0]['versions'][0]['hash'] = 'a'*64
|
||||
with patch.object(frame_decky_store.urllib.request, 'urlopen', return_value=io.BytesIO(json.dumps(records).encode())):
|
||||
self.assertTrue(frame_decky_store.catalog()[0]['root'])
|
||||
|
||||
def test_accepts_hashed_non_root_archive(self):
|
||||
data, digest = archive()
|
||||
self.assertEqual(frame_decky_store.archive_info(data, digest, 'Example'), 'Example')
|
||||
@@ -161,6 +192,212 @@ class StoreTransaction(unittest.TestCase):
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertTrue(external.exists())
|
||||
|
||||
def test_modified_fileserver_archive_does_not_inherit_exception(self):
|
||||
from framey_official import PLUGINS, archive as official_archive
|
||||
record = next(p for p in PLUGINS if p['id'] == 60)
|
||||
original = official_archive(record)
|
||||
with zipfile.ZipFile(io.BytesIO(original)) as source:
|
||||
stream = io.BytesIO()
|
||||
with zipfile.ZipFile(stream, 'w', zipfile.ZIP_DEFLATED) as target:
|
||||
for entry in source.infolist():
|
||||
target.writestr(entry, source.read(entry))
|
||||
target.writestr(record['folder']+'/changed.txt', b'new version')
|
||||
data = stream.getvalue()
|
||||
with self.assertRaisesRegex(ValueError, 'non-ARM64'):
|
||||
frame_decky_store.archive_info(data, hashlib.sha256(data).hexdigest(), record['name'])
|
||||
result = self.apply('install', data, plugin_id=60, folder=record['folder'], version=record['version'])
|
||||
# Remote defence rejects root even when the desktop validation is bypassed.
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn('Root', result.stderr)
|
||||
|
||||
def test_corrupt_fileserver_offline_helper_is_rejected(self):
|
||||
with patch.object(frame_decky_store, 'FILESERVER_BINARIES', {'x86_64': '0'*64}):
|
||||
with self.assertRaisesRegex(ValueError, 'SHA-256 mismatch'):
|
||||
frame_decky_store.apply_script(60, frame_decky_store.FILESERVER_DIGEST)
|
||||
self.assertEqual(frame_decky_store.apply_script(60, '0'*64), frame_decky_store.APPLY)
|
||||
|
||||
def test_exact_fileserver_gets_native_user_helper(self):
|
||||
from framey_official import PLUGINS, archive as official_archive
|
||||
import platform
|
||||
if platform.machine() not in frame_decky_store.FILESERVER_BINARIES:
|
||||
self.skipTest('native Linux helper architecture')
|
||||
record = next(p for p in PLUGINS if p['id'] == 60)
|
||||
data = official_archive(record)
|
||||
self.assertEqual(frame_decky_store.archive_info(data, record['sha256'], record['name']), record['folder'])
|
||||
result = self.apply('install', data, plugin_id=60, folder=record['folder'], version=record['version'])
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
plugin = self.folder.parent/record['folder']
|
||||
self.assertEqual(json.loads((plugin/'plugin.json').read_text())['flags'], [])
|
||||
self.assertEqual(json.loads((plugin/'.frame-control-store.json').read_text())['compatShims'], ['deckyfileserver-user-native'])
|
||||
helper = plugin/'bin/backend'
|
||||
self.assertEqual(hashlib.sha256(helper.read_bytes()).hexdigest(), frame_decky_store.FILESERVER_BINARIES[platform.machine()])
|
||||
self.assertEqual(helper.stat().st_mode & 0o777, 0o700)
|
||||
compile((plugin/'main.py').read_text(), str(plugin/'main.py'), 'exec')
|
||||
self.assertIn('if arg]', (plugin/'main.py').read_text())
|
||||
|
||||
@unittest.skipUnless(__import__('platform').machine() in ('x86_64', 'aarch64'), 'native Linux helper architecture')
|
||||
def test_exact_localsend_gets_verified_native_helper_and_preserves_source(self):
|
||||
from framey_official import PLUGINS, archive as official_archive
|
||||
import platform
|
||||
plugin = next(p for p in PLUGINS if p['id'] == 125)
|
||||
data = official_archive(plugin)
|
||||
self.assertEqual(frame_decky_store.archive_info(data, plugin['sha256'], plugin['name']), plugin['folder'])
|
||||
result = self.apply('install', data, plugin_id=125, folder=plugin['folder'], version=plugin['version'])
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
folder = self.folder.parent/plugin['folder']
|
||||
receipt = json.loads((folder/'.frame-control-store.json').read_text())
|
||||
self.assertEqual(receipt['compatShims'], ['localsend-digest-native'])
|
||||
self.assertEqual(json.loads((folder/'plugin.json').read_text())['flags'], [])
|
||||
expected = (frame_decky_store.LOCALSEND_AARCH64 if platform.machine() == 'aarch64'
|
||||
else frame_decky_store.LOCALSEND_ORIGINAL)
|
||||
self.assertEqual(hashlib.sha256((folder/'bin/localsend-core').read_bytes()).hexdigest(), expected)
|
||||
self.assertEqual((folder/'bin/localsend-core').stat().st_mode & 0o777, 0o700)
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as source:
|
||||
for name in ('main.py', 'LICENSE'):
|
||||
self.assertEqual((folder/name).read_bytes(), source.read(plugin['folder']+'/'+name))
|
||||
result = self.apply('remove', plugin_id=125, folder=plugin['folder'])
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertFalse(folder.exists())
|
||||
|
||||
def test_localsend_exception_does_not_accept_another_archive_or_corrupt_helper(self):
|
||||
from framey_official import PLUGINS, archive as official_archive
|
||||
plugin = next(p for p in PLUGINS if p['id'] == 125)
|
||||
with zipfile.ZipFile(io.BytesIO(official_archive(plugin))) as source:
|
||||
stream = io.BytesIO()
|
||||
with zipfile.ZipFile(stream, 'w', zipfile.ZIP_DEFLATED) as target:
|
||||
for entry in source.infolist():
|
||||
target.writestr(entry, source.read(entry))
|
||||
target.writestr(plugin['folder']+'/changed.txt', b'new version')
|
||||
changed = stream.getvalue()
|
||||
with self.assertRaisesRegex(ValueError, 'non-ARM64'):
|
||||
frame_decky_store.archive_info(changed, hashlib.sha256(changed).hexdigest(), plugin['name'])
|
||||
self.assertEqual(frame_decky_store.apply_script(125, '0'*64), frame_decky_store.APPLY)
|
||||
with patch.object(frame_decky_store, 'LOCALSEND_AARCH64', '0'*64):
|
||||
with self.assertRaisesRegex(ValueError, 'SHA-256 mismatch'):
|
||||
frame_decky_store.apply_script(125, frame_decky_store.LOCALSEND_DIGEST)
|
||||
|
||||
def test_localsend_arm64_transaction_checks_written_helper(self):
|
||||
from framey_official import PLUGINS, archive as official_archive
|
||||
plugin = next(p for p in PLUGINS if p['id'] == 125)
|
||||
# The local x86 host only writes the ELF; native ARM execution is gated
|
||||
# separately by the published Decky runtime runner.
|
||||
with patch.object(frame_decky_store, 'APPLY',
|
||||
"import platform\nplatform.machine=lambda:'aarch64'\n"+frame_decky_store.APPLY):
|
||||
result = self.apply('install', official_archive(plugin), plugin_id=125,
|
||||
folder=plugin['folder'], version=plugin['version'])
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
helper = self.folder.parent/plugin['folder']/'bin/localsend-core'
|
||||
self.assertEqual(hashlib.sha256(helper.read_bytes()).hexdigest(),
|
||||
frame_decky_store.LOCALSEND_AARCH64)
|
||||
self.assertEqual(int.from_bytes(helper.read_bytes()[18:20], 'little'), 183)
|
||||
@unittest.skipUnless(__import__('platform').machine() in ('x86_64', 'aarch64'), 'native Linux wheel architecture')
|
||||
def test_exact_battery_tracker_uses_user_battery_supply_and_preserves_license(self):
|
||||
from framey_official import PLUGINS, archive as official_archive
|
||||
record = next(p for p in PLUGINS if p['id'] == 67)
|
||||
data = official_archive(record)
|
||||
self.assertEqual(frame_decky_store.archive_info(data, record['sha256'], record['name']), record['folder'])
|
||||
result = self.apply('install', data, plugin_id=67,
|
||||
folder=record['folder'], version=record['version'])
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
folder = self.folder.parent / record['folder']
|
||||
receipt = json.loads((folder / '.frame-control-store.json').read_text())
|
||||
self.assertEqual(receipt['compatShims'], ['battery-tracker-user-supply'])
|
||||
self.assertEqual(json.loads((folder / 'plugin.json').read_text())['flags'], [])
|
||||
source = (folder / 'main.py').read_text()
|
||||
self.assertNotIn('/sys/class/power_supply/BAT1/', source)
|
||||
self.assertIn("root / 'BAT1'", source)
|
||||
compile(source, str(folder / 'main.py'), 'exec')
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as original:
|
||||
self.assertEqual((folder / 'LICENSE').read_bytes(), original.read(record['folder']+'/LICENSE'))
|
||||
self.assertEqual(self.apply('remove', plugin_id=67, folder=record['folder']).returncode, 0)
|
||||
self.assertFalse(folder.exists())
|
||||
changed = io.BytesIO(data)
|
||||
with zipfile.ZipFile(changed, 'a') as archive:
|
||||
archive.writestr(record['folder']+'/changed.txt', 'changed')
|
||||
altered = changed.getvalue()
|
||||
with self.assertRaisesRegex(ValueError, 'root'):
|
||||
frame_decky_store.archive_info(altered, hashlib.sha256(altered).hexdigest(), record['name'])
|
||||
result = self.apply('install', altered, plugin_id=67,
|
||||
folder=record['folder'], version=record['version'])
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertFalse(folder.exists())
|
||||
|
||||
def test_adapted_battery_tracker_empty_history_and_unload_flush(self):
|
||||
from framey_official import PLUGINS, archive as official_archive
|
||||
record = next(p for p in PLUGINS if p['id'] == 67)
|
||||
result = self.apply('install', official_archive(record), plugin_id=67,
|
||||
folder=record['folder'], version=record['version'])
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
folder = self.folder.parent / record['folder']
|
||||
runtime = Path(self.tmp.name) / 'battery-runtime'
|
||||
runtime.mkdir()
|
||||
supply = Path(self.tmp.name) / 'battery-supply' / 'frame-battery'
|
||||
supply.mkdir(parents=True)
|
||||
for name, value in {'type': 'Battery', 'capacity': '89', 'status': 'Charging',
|
||||
'voltage_now': '8746718', 'current_now': '1030625'}.items():
|
||||
(supply / name).write_text(value + '\n')
|
||||
decky = types.ModuleType('decky_plugin')
|
||||
decky.logger = logging.getLogger('battery-tracker-test')
|
||||
decky.DECKY_PLUGIN_RUNTIME_DIR = str(runtime)
|
||||
with patch.dict(sys.modules, {'decky_plugin': decky}), patch.dict(os.environ, {'FRAMEY_BATTERY_ROOT': str(supply.parent)}):
|
||||
spec = importlib.util.spec_from_file_location('framey_battery_tracker_test', folder / 'main.py')
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
async def exercise():
|
||||
plugin = module.Plugin()
|
||||
await plugin._main()
|
||||
self.assertEqual(await plugin.get_recent_data(), {'x': [], 'cap': [], 'power_data': []})
|
||||
# The first sample is still pending when Decky unloads the plugin.
|
||||
await asyncio.sleep(0)
|
||||
self.assertEqual(len(plugin._pending_samples), 1)
|
||||
self.assertEqual((await plugin.get_recent_data())['cap'], [.89])
|
||||
await plugin._unload()
|
||||
self.assertEqual(plugin._pending_samples, [])
|
||||
|
||||
asyncio.run(exercise())
|
||||
import sqlite3
|
||||
with sqlite3.connect(runtime / 'battery.db') as db:
|
||||
self.assertEqual(db.execute('select capacity from battery').fetchall(), [(89,)])
|
||||
|
||||
@unittest.skipUnless(__import__('platform').machine() in ('x86_64', 'aarch64'), 'native Linux wheel architecture')
|
||||
def test_exact_network_sentinel_is_user_scoped_and_digest_guarded(self):
|
||||
from framey_official import PLUGINS, archive as official_archive
|
||||
import platform
|
||||
record = next(p for p in PLUGINS if p['id'] == 114)
|
||||
data = official_archive(record)
|
||||
self.assertEqual(frame_decky_store.archive_info(data, record['sha256'], record['name']), record['folder'])
|
||||
result = self.apply('install', data, plugin_id=114,
|
||||
folder=record['folder'], version=record['version'])
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
folder = self.folder.parent / record['folder']
|
||||
receipt = json.loads((folder / '.frame-control-store.json').read_text())
|
||||
self.assertEqual(receipt['compatShims'], ['network-sentinel-user-psutil'])
|
||||
self.assertEqual(json.loads((folder / 'plugin.json').read_text())['flags'], ['debug'])
|
||||
source = (folder / 'main.py').read_text()
|
||||
self.assertNotIn('decky.DECKY_SETTINGS_DIR', source)
|
||||
self.assertIn('decky.DECKY_PLUGIN_SETTINGS_DIR', source)
|
||||
self.assertIn(" jitter = 0\n cmd = ['ping'", source)
|
||||
compile(source, str(folder / 'main.py'), 'exec')
|
||||
native = (folder / 'py_modules/psutil/_psutil_linux.abi3.so').read_bytes()
|
||||
self.assertEqual(int.from_bytes(native[18:20], 'little'),
|
||||
{'x86_64': 62, 'aarch64': 183}[platform.machine()])
|
||||
self.assertIn('BSD 3-Clause', (folder / 'py_modules/psutil-7.2.2.dist-info/LICENSE').read_text())
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as original:
|
||||
self.assertEqual((folder / 'LICENSE').read_bytes(), original.read(record['folder']+'/LICENSE'))
|
||||
self.assertEqual(self.apply('remove', plugin_id=114, folder=record['folder']).returncode, 0)
|
||||
self.assertFalse(folder.exists())
|
||||
changed = io.BytesIO(data)
|
||||
with zipfile.ZipFile(changed, 'a') as archive:
|
||||
archive.writestr(record['folder']+'/changed.txt', 'changed')
|
||||
altered = changed.getvalue()
|
||||
with self.assertRaisesRegex(ValueError, 'root'):
|
||||
frame_decky_store.archive_info(altered, hashlib.sha256(altered).hexdigest(), record['name'])
|
||||
result = self.apply('install', altered, plugin_id=114,
|
||||
folder=record['folder'], version=record['version'])
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertFalse(folder.exists())
|
||||
|
||||
@unittest.skipUnless(__import__('platform').machine() in ('x86_64', 'aarch64'), 'native Linux wheel architecture')
|
||||
def test_exact_deckyspy_gets_native_dependency_and_license_only_in_its_folder(self):
|
||||
from framey_official import PLUGINS, archive as official_archive
|
||||
|
||||
@@ -26,6 +26,11 @@ class HeadsetBundle(unittest.TestCase):
|
||||
self.assertIn('ui/headset.html', names)
|
||||
self.assertIn('ui/frame_headset_runtime.py', names)
|
||||
self.assertIn('ui/server.py', names)
|
||||
for filename in ('aarch64.gz', 'x86_64.gz', 'source.tar.gz', 'LICENSE.txt', 'GO-LICENSE.txt', 'README.md'):
|
||||
self.assertIn('ui/vendor/deckyfileserver-1.2.0/'+filename, names)
|
||||
for filename in ('aarch64.gz', 'source.tar.gz', 'LICENSE.txt', 'GO-LICENSE.txt', 'README.md'):
|
||||
self.assertIn('ui/vendor/localsend-0.4.5/'+filename, names)
|
||||
self.assertIn('scripts/build-framey-fileserver.py', names)
|
||||
for name in ('aarch64.whl', 'x86_64.whl', 'LICENSE.txt'):
|
||||
self.assertIn('ui/vendor/psutil-7.2.2/'+name, names)
|
||||
self.assertFalse(any('\\' in name for name in names))
|
||||
|
||||
@@ -19,7 +19,9 @@ class RuntimeCoverage(unittest.TestCase):
|
||||
self.assertEqual(len(cases), len({case['id'] for case in cases}))
|
||||
for case in cases:
|
||||
self.assertIn(case['id'], official)
|
||||
self.assertIn(case['probe'], ('mpris', 'screenshots', 'system-monitor', 'timer-lifecycle'))
|
||||
self.assertIn(case['probe'], ('mpris', 'screenshots', 'system-monitor', 'timer-lifecycle',
|
||||
'toggle-settings', 'fileserver', 'localsend', 'network-sentinel',
|
||||
'battery-tracker'))
|
||||
|
||||
def test_new_shim_without_runtime_case_fails_coverage(self):
|
||||
import frame_decky_store
|
||||
|
||||
@@ -21,6 +21,16 @@ from framey_official import LEGACY_PLUGINS, PLUGINS, archive
|
||||
|
||||
|
||||
class OfficialBundles(unittest.TestCase):
|
||||
@unittest.skipUnless(shutil.which('node'), 'Node is needed for the official frontend check')
|
||||
def test_pause_games_toggle_renders_saved_value_and_dispatches_settings_only(self):
|
||||
plugin = next(p for p in PLUGINS if p['id'] == 13)
|
||||
with zipfile.ZipFile(io.BytesIO(archive(plugin))) as bundle, tempfile.TemporaryDirectory() as root:
|
||||
frontend = Path(root)/'index.js'
|
||||
frontend.write_bytes(bundle.read(plugin['folder']+'/dist/index.js'))
|
||||
result = subprocess.run(['node', str(Path(__file__).parent/'page/framey_toggle.mjs'), str(frontend)],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_legacy_css_loader_store_archive_has_unversioned_frontend(self):
|
||||
plugin = LEGACY_PLUGINS[0]
|
||||
data = archive(plugin)
|
||||
@@ -74,6 +84,14 @@ class OfficialBundles(unittest.TestCase):
|
||||
folder = plugins / plugin['folder']
|
||||
(folder / '.frame-control-store.json').write_text(json.dumps({
|
||||
'storeId': plugin['id'], 'sha256': plugin['sha256']}))
|
||||
if plugin['id'] in (60, 67, 114):
|
||||
# This renderer fixture extracts the original root ZIP.
|
||||
# Transaction and Decky tests cover managed adaptation;
|
||||
# an unadapted root copy must fail here.
|
||||
with patch.object(framey_panel, 'PLUGINS', plugins), self.assertRaises(FileNotFoundError):
|
||||
framey_panel.panel(plugin['id'])
|
||||
manifest['flags'] = []
|
||||
(folder/'plugin.json').write_text(json.dumps(manifest))
|
||||
with patch.object(framey_panel, 'PLUGINS', plugins):
|
||||
page = framey_panel.panel(plugin['id']).decode()
|
||||
encoded = re.search(r'<script id="framey-bundle" type="application/json"[^>]*>([^<]+)</script>', page)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Store requests can reconnect when the headset panel loses the POST response."""
|
||||
import sandbox # noqa: F401
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'ui'))
|
||||
import server
|
||||
|
||||
|
||||
class StoreJobs(unittest.TestCase):
|
||||
def test_same_request_id_runs_once_and_conflicting_reuse_is_rejected(self):
|
||||
request_id = 'a' * 32
|
||||
started = threading.Event()
|
||||
finish = threading.Event()
|
||||
calls = []
|
||||
|
||||
def change(_ssh, action, plugin_id, report, reviewed_hash=None):
|
||||
calls.append((action, plugin_id, reviewed_hash))
|
||||
started.set()
|
||||
self.assertTrue(finish.wait(5))
|
||||
report('Installed', 100)
|
||||
return {'message': 'Installed once'}
|
||||
|
||||
body = {'action': 'install', 'id': 2, 'sha256': 'b' * 64,
|
||||
'acknowledged': True, 'requestId': request_id}
|
||||
try:
|
||||
with patch.object(server.frame_decky_store, 'change', side_effect=change):
|
||||
first = server.decky_plugin(body)
|
||||
self.assertTrue(started.wait(5))
|
||||
self.assertEqual(server.decky_plugin(body), first)
|
||||
for changed in ({'action': 'update'}, {'id': 3}, {'sha256': 'c' * 64}):
|
||||
with self.subTest(changed=changed), self.assertRaises(server.Failure) as error:
|
||||
server.decky_plugin({**body, **changed})
|
||||
self.assertEqual(error.exception.status, 409)
|
||||
self.assertEqual(calls, [('install', 2, 'b' * 64)])
|
||||
self.assertNotIn('_request', server.job_status('id=' + request_id))
|
||||
finish.set()
|
||||
for _ in range(100):
|
||||
if server.job_status('id=' + request_id)['done']:
|
||||
break
|
||||
time.sleep(0.01)
|
||||
self.assertEqual(server.job_status('id=' + request_id)['message'], 'Installed once')
|
||||
self.assertEqual(server.decky_plugin(body), first)
|
||||
self.assertEqual(len(calls), 1)
|
||||
finally:
|
||||
finish.set()
|
||||
with server._jobs_lock:
|
||||
server._jobs.pop(request_id, None)
|
||||
|
||||
def test_invalid_request_id_does_not_start_work(self):
|
||||
with patch.object(server.frame_decky_store, 'change') as change:
|
||||
for request_id in (1, '', 'A' * 32, 'f' * 31, 'f' * 33, '../' * 11):
|
||||
with self.subTest(request_id=request_id), self.assertRaises(server.Failure) as error:
|
||||
server.decky_plugin({'action': 'install', 'id': 2,
|
||||
'acknowledged': True, 'requestId': request_id})
|
||||
self.assertEqual(error.exception.status, 400)
|
||||
change.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,53 @@
|
||||
"""The iOS Frame bundle must recover transient pinned package downloads."""
|
||||
import hashlib
|
||||
import http.client
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
import urllib.error
|
||||
import io
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SPEC = importlib.util.spec_from_file_location('kdeconnect_fetch', ROOT / 'frame/kdeconnect/fetch.py')
|
||||
fetch = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(fetch)
|
||||
|
||||
|
||||
class VerifiedDownload(unittest.TestCase):
|
||||
def test_remote_disconnect_retries_then_checks_hash(self):
|
||||
data = b'pinned package'
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
with patch.object(fetch.urllib.request, 'urlopen', side_effect=[
|
||||
http.client.RemoteDisconnected('closed'), io.BytesIO(data)]) as open_url, \
|
||||
patch.object(fetch.time, 'sleep') as sleep:
|
||||
self.assertEqual(fetch.download_verified('https://example.invalid/package', digest), data)
|
||||
self.assertEqual(open_url.call_count, 2)
|
||||
sleep.assert_called_once_with(1)
|
||||
|
||||
def test_permanent_http_error_and_wrong_hash_fail_closed(self):
|
||||
url = 'https://example.invalid/package'
|
||||
missing = urllib.error.HTTPError(url, 404, 'missing', {}, None)
|
||||
with patch.object(fetch.urllib.request, 'urlopen', side_effect=missing) as open_url, \
|
||||
patch.object(fetch.time, 'sleep') as sleep:
|
||||
with self.assertRaises(urllib.error.HTTPError):
|
||||
fetch.download_verified(url, '0' * 64)
|
||||
open_url.assert_called_once()
|
||||
sleep.assert_not_called()
|
||||
with patch.object(fetch.urllib.request, 'urlopen', return_value=io.BytesIO(b'changed')) as open_url:
|
||||
with self.assertRaisesRegex(SystemExit, 'checksum mismatch'):
|
||||
fetch.download_verified(url, '0' * 64)
|
||||
open_url.assert_called_once()
|
||||
|
||||
def test_exhausted_transient_error_is_not_silenced(self):
|
||||
with patch.object(fetch.urllib.request, 'urlopen', side_effect=http.client.RemoteDisconnected('closed')) as open_url, \
|
||||
patch.object(fetch.time, 'sleep') as sleep:
|
||||
with self.assertRaises(http.client.RemoteDisconnected):
|
||||
fetch.download_verified('https://example.invalid/package', '0' * 64)
|
||||
self.assertEqual(open_url.call_count, 3)
|
||||
self.assertEqual(sleep.call_count, 2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Large desktop file copies use the selected local path, not an 8 GiB HTTP upload."""
|
||||
import sandbox # noqa: F401 (first: keeps tests off real data and services)
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "ui"))
|
||||
import server
|
||||
|
||||
|
||||
class LargeTransfer(unittest.TestCase):
|
||||
def test_desktop_http_copy_stays_on_selected_headset(self):
|
||||
with tempfile.TemporaryDirectory() as tmp, server.LoopbackServer(("127.0.0.1", 0), server.Handler) as httpd:
|
||||
source = Path(tmp) / "usb-file.bin"
|
||||
source.write_bytes(b"a small stand-in for a large file")
|
||||
worker = threading.Thread(target=httpd.serve_forever, daemon=True)
|
||||
worker.start()
|
||||
link = mock.Mock()
|
||||
link.active_device.return_value = {"id": "frame-a"}
|
||||
body = json.dumps({"action": "copy-local", "path": str(source)})
|
||||
|
||||
def copy(device):
|
||||
conn = http.client.HTTPConnection("127.0.0.1", httpd.server_port, timeout=5)
|
||||
try:
|
||||
conn.request("POST", "/api/transfer", body, {"X-Frame-UI": server.UI_KEY,
|
||||
"X-Frame-Device": device, "Content-Type": "application/json"})
|
||||
response = conn.getresponse()
|
||||
return response.status, json.loads(response.read())
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
try:
|
||||
with mock.patch.dict(os.environ, {"FRAME_CONTROL_APP": "1"}), \
|
||||
mock.patch.object(server, "LINK", link), \
|
||||
mock.patch.object(server, "push_file", return_value="Sent usb-file.bin to ~/Downloads/") as push:
|
||||
self.assertEqual(copy("frame-b")[0], 409)
|
||||
push.assert_not_called()
|
||||
self.assertEqual(copy("frame-a"), (200, {"message": "Sent usb-file.bin to ~/Downloads/"}))
|
||||
push.assert_called_once_with(source)
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
worker.join(timeout=5)
|
||||
|
||||
def test_27_gib_local_file_reaches_direct_copy(self):
|
||||
source = Path(os.path.abspath("large-file.bin"))
|
||||
size = 27 * 1024 ** 3
|
||||
info = SimpleNamespace(st_mode=stat.S_IFREG | 0o644, st_size=size)
|
||||
with mock.patch.dict(os.environ, {"FRAME_CONTROL_APP": "1"}), \
|
||||
mock.patch.object(Path, "stat", return_value=info), \
|
||||
mock.patch.object(server, "push_file", return_value="Sent large-file.bin to ~/Downloads/") as push:
|
||||
self.assertEqual(server.transfer_local({"action": "copy-local", "path": str(source)}),
|
||||
{"message": "Sent large-file.bin to ~/Downloads/"})
|
||||
push.assert_called_once_with(source)
|
||||
|
||||
def test_direct_copy_validates_desktop_file_before_ssh(self):
|
||||
with tempfile.TemporaryDirectory() as tmp, \
|
||||
mock.patch.dict(os.environ, {"FRAME_CONTROL_APP": "1"}), \
|
||||
mock.patch.object(server, "push_file") as push:
|
||||
empty = Path(tmp) / "empty.bin"
|
||||
empty.touch()
|
||||
hidden = Path(tmp) / ".hidden"
|
||||
hidden.write_bytes(b"x")
|
||||
for path in ("relative.bin", str(Path(tmp) / "missing.bin"), tmp,
|
||||
str(empty), str(hidden)):
|
||||
with self.subTest(path=path), self.assertRaises(server.Failure):
|
||||
server.transfer_local({"action": "copy-local", "path": path})
|
||||
push.assert_not_called()
|
||||
|
||||
def test_large_copy_gets_more_than_one_hour_without_loading_file(self):
|
||||
info = SimpleNamespace(st_size=27 * 1024 ** 3)
|
||||
result = subprocess.CompletedProcess([], 0, "", "")
|
||||
with mock.patch.object(Path, "stat", return_value=info), \
|
||||
mock.patch.object(server.shutil, "which", return_value=None), \
|
||||
mock.patch.object(server.frame_host, "run_ssh", return_value=result) as run:
|
||||
self.assertEqual(server.push_file("/tmp/large-file.bin"),
|
||||
"Sent large-file.bin to ~/Downloads/")
|
||||
self.assertGreater(run.call_args.kwargs["timeout"], 3600)
|
||||
@@ -309,6 +309,12 @@ class Renderer(unittest.TestCase):
|
||||
resources = {r['from']: r.get('filter', ['**/*']) for r in config['build']['extraResources']}
|
||||
self.assertIn('*.js', resources['../ui'])
|
||||
self.assertIn('*.js', resources['../frame/android'])
|
||||
self.assertIn('*.gz', resources['../ui/vendor/localsend-0.4.5'])
|
||||
self.assertIn('*.txt', resources['../ui/vendor/localsend-0.4.5'])
|
||||
self.assertIn('build-framey-localsend.py', resources['../scripts'])
|
||||
checker = (ROOT / 'app/build/check-resources.js').read_text()
|
||||
for name in ('aarch64.gz', 'source.tar.gz', 'LICENSE.txt', 'GO-LICENSE.txt', 'README.md'):
|
||||
self.assertIn(f'ui/vendor/localsend-0.4.5/{name}', checker)
|
||||
|
||||
|
||||
class SteamGridDB(unittest.TestCase):
|
||||
|
||||
@@ -76,6 +76,16 @@ class PublishRelease(unittest.TestCase):
|
||||
self.assertEqual(p.returncode, 0, p.stderr + p.stdout)
|
||||
self.assertEqual(next(c for c in log if "PATCH" in c)[3], "repos/saphid/frame-control/releases/2")
|
||||
|
||||
def test_already_published_release_keeps_its_live_update_manifest(self):
|
||||
old = {"id": 7, "name": "update.json", "size": 100,
|
||||
"digest": "sha256:" + "a" * 64}
|
||||
rel = dict(draft(tag_name="v9.8.7", assets=draft()["assets"] + [old]), draft=False)
|
||||
p, log, upload = self.run_script([rel], "v9.8.7")
|
||||
self.assertNotEqual(p.returncode, 0)
|
||||
self.assertIn("already published", p.stderr)
|
||||
self.assertIsNone(upload)
|
||||
self.assertFalse(any("-X" in call for call in log))
|
||||
|
||||
def test_refuses_missing_or_unhashed_installers(self):
|
||||
assets = draft()["assets"][1:]
|
||||
assets[0] = dict(assets[0], digest=None)
|
||||
@@ -134,6 +144,17 @@ class PublishRelease(unittest.TestCase):
|
||||
self.assertIsNone(upload)
|
||||
self.assertFalse(any("-X" in call for call in log))
|
||||
|
||||
def test_refuses_a_prior_cancelled_installer_after_successful_rerun(self):
|
||||
gates = {"release_attempts": 2,
|
||||
"prior_bad_job": "build (macos-latest, dist, app/dist/*.dmg app/dist/*.zip)",
|
||||
"prior_bad_job_conclusion": "cancelled"}
|
||||
p, log, upload = self.run_script([draft()], "v9.8.7", gates=gates)
|
||||
self.assertNotEqual(p.returncode, 0)
|
||||
self.assertIn("attempt 1", p.stderr)
|
||||
self.assertIn("cancelled", p.stderr)
|
||||
self.assertIsNone(upload)
|
||||
self.assertFalse(any("-X" in call for call in log))
|
||||
|
||||
def test_refuses_missing_or_nonexact_gate_results(self):
|
||||
for gates in ({"omit_job": "e2e"}, {"main_sha": "c" * 40},
|
||||
{"decky_conclusion": "failure"}, {"release_conclusion": "failure"},
|
||||
|
||||
@@ -22,6 +22,7 @@ WORKFLOW = ROOT / ".github" / "workflows" / "release.yml"
|
||||
FAKEGH = ROOT / "tests" / "fakegh"
|
||||
STEP = "- name: Create the draft release"
|
||||
UPLOAD_STEP = "- name: Upload to the release"
|
||||
OUTPUTS_STEP = "- name: Check installer outputs before upload"
|
||||
|
||||
|
||||
def step_script(step=STEP):
|
||||
@@ -116,6 +117,28 @@ class DraftStep(unittest.TestCase):
|
||||
self.assertEqual(self.patches(log), [])
|
||||
|
||||
|
||||
@unittest.skipUnless(os.name != "nt" and shutil.which("bash"),
|
||||
"needs a POSIX bash; Actions uses Git Bash on Windows")
|
||||
class InstallerOutputsStep(unittest.TestCase):
|
||||
def run_step(self, files):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
for name in files:
|
||||
(root / name).write_bytes(b"installer")
|
||||
script = step_script(OUTPUTS_STEP).replace("${{ matrix.files }}", "*.dmg *.zip")
|
||||
return subprocess.run(["bash", "--noprofile", "--norc", "-eo", "pipefail", "-c", script],
|
||||
cwd=root, capture_output=True, text=True, timeout=10)
|
||||
|
||||
def test_requires_each_advertised_installer_type(self):
|
||||
result = self.run_step(["Frame-Control-mac.dmg"])
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("missing installer output matching *.zip", result.stderr)
|
||||
|
||||
def test_accepts_all_advertised_installer_types(self):
|
||||
result = self.run_step(["Frame-Control-mac.dmg", "Frame-Control-mac.zip"])
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
|
||||
@unittest.skipUnless(os.name != "nt" and shutil.which("bash") and shutil.which("jq"),
|
||||
"needs a POSIX bash and jq; Actions runs this step under bash")
|
||||
class UploadStep(unittest.TestCase):
|
||||
|
||||
+19
-1
@@ -267,6 +267,17 @@ class ServerGuards(unittest.TestCase):
|
||||
self.assertEqual(self.request("GET", "/api/titles/job?token=nope", headers={"X-Frame-UI": "1"})[0], 404)
|
||||
self.assertEqual(self.request("POST", "/api/titles", {"action": "list"})[0], 403)
|
||||
|
||||
def test_direct_local_transfer_is_desktop_only(self):
|
||||
status, payload = self.post("/api/transfer", {"action": "copy-local", "path": "/tmp/a.bin"})
|
||||
self.assertEqual(status, 403, payload)
|
||||
|
||||
def test_oversized_browser_upload_rejected_without_reading_body(self):
|
||||
status, _, payload = self.request("POST", "/api/upload", b"",
|
||||
{"X-Frame-UI": "1", "X-Filename": "large.bin",
|
||||
"Content-Length": str(27 * 1024 ** 3)})
|
||||
self.assertEqual(status, 413, payload)
|
||||
self.assertIn(b"limited to 8 GiB", payload)
|
||||
|
||||
def test_web_install_needs_the_app_page(self):
|
||||
# A website can only open frame-control:// links; it can't call these itself.
|
||||
link = {"url": "https://cdn.example.com/game.apk"}
|
||||
@@ -497,7 +508,10 @@ class ArtworkSettings(unittest.TestCase):
|
||||
time.sleep(0.05)
|
||||
key = {"X-Frame-UI": "1", "Content-Type": "application/json"}
|
||||
self.assertEqual(request("GET", "/api/settings/artwork")[0], 403)
|
||||
self.assertEqual(request("POST", "/api/settings/artwork", {"steamgriddb_api_key": "abc"})[0], 403)
|
||||
# The missing-key guard runs before reading a body. Leaving an
|
||||
# unread body on Windows can reset this test connection before
|
||||
# http.client receives the 403 response.
|
||||
self.assertEqual(request("POST", "/api/settings/artwork")[0], 403)
|
||||
status, payload = request("GET", "/api/settings/artwork", headers=key)
|
||||
self.assertEqual((status, json.loads(payload)["steamgriddb_configured"]), (200, False))
|
||||
status, payload = request("POST", "/api/settings/artwork", {"steamgriddb_api_key": "abc_1"}, key)
|
||||
@@ -575,6 +589,10 @@ class PageScripts(unittest.TestCase):
|
||||
r = subprocess.run(["node", str(ROOT / "tests" / "page" / "meta_rows.mjs")], capture_output=True, text=True)
|
||||
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
|
||||
|
||||
def test_large_local_file_uses_direct_copy(self):
|
||||
r = subprocess.run(["node", str(ROOT / "tests" / "page" / "large_transfer.mjs")], capture_output=True, text=True)
|
||||
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == "nt", "runs on the Frame (Linux); local-bin/ssh is a POSIX shell script")
|
||||
class LocalMode(unittest.TestCase):
|
||||
|
||||
@@ -95,6 +95,19 @@ class RealProcesses(unittest.TestCase):
|
||||
|
||||
|
||||
class Safety(unittest.TestCase):
|
||||
def test_locked_preflight_rejects_active_or_unknown_proximity_before_titles(self):
|
||||
for proximity in ([], [88.0625], [4.6875, 88.0625], [None]):
|
||||
with self.subTest(proximity=proximity), mock.patch.object(
|
||||
frame_smoke, 'remote', return_value=mock.Mock(
|
||||
returncode=0, stdout=json.dumps({
|
||||
'activity': 3, 'proximity': proximity, 'battery': [72]}))):
|
||||
with self.assertRaisesRegex(RuntimeError, 'proximity is active or unknown'):
|
||||
frame_smoke.preflight()
|
||||
with mock.patch.object(frame_smoke, 'remote', return_value=mock.Mock(
|
||||
returncode=0, stdout=json.dumps({
|
||||
'activity': 3, 'proximity': [4.6875], 'battery': [72]}))):
|
||||
self.assertEqual(frame_smoke.preflight()['proximity'], [4.6875])
|
||||
|
||||
def test_missing_x86_runtime_skips_launch_but_still_removes_title(self):
|
||||
smoke = frame_smoke.Smoke()
|
||||
gid = 'fc_smoke_x86_64'
|
||||
|
||||
+285
-15
@@ -4,6 +4,7 @@ Only Frame Control owned plugin folders can be updated or removed. The store
|
||||
digest validates transport, not plugin trust; store plugins run as the user.
|
||||
"""
|
||||
import base64
|
||||
import gzip
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
@@ -13,6 +14,7 @@ import re
|
||||
import secrets
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import frame_decky
|
||||
import framey_compatibility
|
||||
@@ -25,6 +27,21 @@ MAX_ZIP = 50 * 1024 * 1024
|
||||
MAX_UNPACKED = 150 * 1024 * 1024
|
||||
SAFE_FOLDER = re.compile(r'[A-Za-z0-9][A-Za-z0-9_.-]{0,99}\Z')
|
||||
SHA = re.compile(r'[0-9a-f]{64}\Z')
|
||||
FILESERVER_DIGEST = '7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0'
|
||||
FILESERVER_BINARIES = {
|
||||
'x86_64': '5e90efd1bf5163bef1686045f4714ad0e4fd0f54edb037fce8cdb06c58177bcb',
|
||||
'aarch64': '86b80021ebd62ed619eba065757c1122defa5a74b2c045aa44a0c6b4403a508a',
|
||||
}
|
||||
FILESERVER_ORIGINAL = 'd9eab8559c87c01736188bc676f3b4b44dee47cf952b6e571c770d1ea61733e9'
|
||||
LOCALSEND_DIGEST = 'e271ccb55eb23f918b99a69f79aec905df4c9b2ef7b23bbf4ec1c2d1a6f0c6b7'
|
||||
LOCALSEND_ORIGINAL = 'd448d03861d578685e6c70ea3ace25eb34fca99cb9d4f98bf51ba23aa5cae843'
|
||||
LOCALSEND_MAIN = 'db03f9201fe50b75dea99a7a05b54ec3f352d1bdfe174115b0e569154198d8ad'
|
||||
LOCALSEND_AARCH64 = 'e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae'
|
||||
NETWORK_SENTINEL_DIGEST = 'd3e07f40a0c800e72d15594e03c8c61e9bf3092524a578a0260520e735b971a5'
|
||||
NETWORK_SENTINEL_MAIN = '2baa390cb66dc267e8f04e61ba1edc43af75115ad5e453d308f6bf26ed70b6b9'
|
||||
BATTERY_TRACKER_DIGEST = '91c388d3c1b52079314045b79671ad55c767510658b6193f6936ed6cad383b94'
|
||||
BATTERY_TRACKER_MAIN = '2303ec70c262f1cbfe1c8c138770c6e7c3496827e748e679c94ca43ff9f28725'
|
||||
|
||||
VERSION = re.compile(r'[A-Za-z0-9][A-Za-z0-9._+\-]{0,39}\Z')
|
||||
|
||||
|
||||
@@ -70,6 +87,10 @@ def catalog():
|
||||
counts[item['id']] = counts.get(item['id'], 0) + 1
|
||||
unique = [item for item in result if counts[item['id']] == 1]
|
||||
for item in unique:
|
||||
if ((item['id'] == 60 and item['name'] == 'DeckyFileServer' and item['sha256'] == FILESERVER_DIGEST)
|
||||
or (item['id'] == 114 and item['name'] == 'Network Sentinel' and item['sha256'] == NETWORK_SENTINEL_DIGEST)
|
||||
or (item['id'] == 67 and item['name'] == 'Battery Tracker' and item['sha256'] == BATTERY_TRACKER_DIGEST)):
|
||||
item['root'] = False # Exact managed adaptation runs as the Frame user.
|
||||
item['compatibility'] = framey_compatibility.lookup(item['id'], item['sha256'], item['version'])
|
||||
return unique
|
||||
|
||||
@@ -88,6 +109,14 @@ def archive_info(data, expected_hash, expected_name):
|
||||
folder = manifests[0].split('/')[0]
|
||||
if not SAFE_FOLDER.fullmatch(folder):
|
||||
raise ValueError('Invalid plugin folder')
|
||||
fileserver = (expected_hash == FILESERVER_DIGEST and expected_name == 'DeckyFileServer'
|
||||
and folder == 'DeckyFileServer')
|
||||
localsend = (expected_hash == LOCALSEND_DIGEST and expected_name == 'Decky Localsend'
|
||||
and folder == 'decky-localsend')
|
||||
network_sentinel = (expected_hash == NETWORK_SENTINEL_DIGEST and expected_name == 'Network Sentinel'
|
||||
and folder == 'network-sentinel')
|
||||
battery_tracker = (expected_hash == BATTERY_TRACKER_DIGEST and expected_name == 'Battery Tracker'
|
||||
and folder == 'steam-deck-battery-tracker')
|
||||
seen = {}
|
||||
for entry in files:
|
||||
name = entry.filename
|
||||
@@ -117,7 +146,11 @@ def archive_info(data, expected_hash, expected_name):
|
||||
with archive.open(entry) as source:
|
||||
header = source.read(20)
|
||||
if header[:4] == b'\x7fELF' and int.from_bytes(header[18:20], 'little') != 183:
|
||||
raise ValueError('Plugin contains a non-ARM64 Linux binary: ' + name)
|
||||
if not ((fileserver and name == 'DeckyFileServer/bin/backend'
|
||||
and hashlib.sha256(archive.read(entry)).hexdigest() == FILESERVER_ORIGINAL)
|
||||
or (localsend and name == 'decky-localsend/bin/localsend-core'
|
||||
and hashlib.sha256(archive.read(entry)).hexdigest() == LOCALSEND_ORIGINAL)):
|
||||
raise ValueError('Plugin contains a non-ARM64 Linux binary: ' + name)
|
||||
if header[:2] == b'MZ':
|
||||
raise ValueError('Plugin contains a Windows binary: ' + name)
|
||||
manifest = json.loads(archive.read(manifests[0]))
|
||||
@@ -128,8 +161,16 @@ def archive_info(data, expected_hash, expected_name):
|
||||
flags = manifest.get('flags', [])
|
||||
if not isinstance(flags, list) or not all(isinstance(flag, str) for flag in flags):
|
||||
raise ValueError('Invalid plugin flags')
|
||||
if 'root' in flags or '_root' in flags:
|
||||
if ('root' in flags or '_root' in flags) and not (fileserver or network_sentinel or battery_tracker):
|
||||
raise ValueError('This plugin needs root and is not supported by the Frame user service')
|
||||
if localsend and hashlib.sha256(archive.read(folder+'/main.py')).hexdigest() != LOCALSEND_MAIN:
|
||||
raise ValueError('LocalSend Python source differs from the pinned archive')
|
||||
if network_sentinel and (flags != ['debug', '_root'] or manifest.get('api_version') != 1
|
||||
or hashlib.sha256(archive.read(folder+'/main.py')).hexdigest() != NETWORK_SENTINEL_MAIN):
|
||||
raise ValueError('Network Sentinel manifest or source differs from the pinned archive')
|
||||
if battery_tracker and (flags != ['_root'] or 'api_version' in manifest
|
||||
or hashlib.sha256(archive.read(folder+'/main.py')).hexdigest() != BATTERY_TRACKER_MAIN):
|
||||
raise ValueError('Battery Tracker manifest or source differs from the pinned archive')
|
||||
return folder
|
||||
|
||||
|
||||
@@ -141,8 +182,27 @@ PSUTIL_DIGESTS = {
|
||||
|
||||
|
||||
def apply_script(plugin_id, digest):
|
||||
"""Send verified offline dependencies only for the exact managed plugin."""
|
||||
if plugin_id != 89 or digest != DECKYSPY_DIGEST:
|
||||
"""Deliver verified offline dependencies only to exact managed archives."""
|
||||
if plugin_id == 125 and digest == LOCALSEND_DIGEST:
|
||||
packed = (Path(__file__).parent/'vendor/localsend-0.4.5/aarch64.gz').read_bytes()
|
||||
data = gzip.decompress(packed)
|
||||
if hashlib.sha256(data).hexdigest() != LOCALSEND_AARCH64:
|
||||
raise ValueError('Bundled LocalSend ARM64 helper SHA-256 mismatch')
|
||||
return ('FRAMEY_LOCALSEND_AARCH64 = '+repr(base64.b64encode(packed).decode('ascii'))+'\n'+APPLY)
|
||||
if plugin_id == 60 and digest == FILESERVER_DIGEST:
|
||||
assets = Path(__file__).parent/'vendor/deckyfileserver-1.2.0'
|
||||
payloads = {}
|
||||
for arch, expected in FILESERVER_BINARIES.items():
|
||||
packed = (assets/f'{arch}.gz').read_bytes()
|
||||
data = gzip.decompress(packed)
|
||||
if hashlib.sha256(data).hexdigest() != expected:
|
||||
raise ValueError('Bundled DeckyFileServer SHA-256 mismatch')
|
||||
payloads[arch] = base64.b64encode(packed).decode('ascii')
|
||||
# Python stdin, never shell arguments. The Frame rechecks hash and ELF machine.
|
||||
return ('FRAMEY_FILESERVER_BINARIES = '+repr(payloads)+'\n'
|
||||
+'FRAMEY_FILESERVER_HASHES = '+repr(FILESERVER_BINARIES)+'\n'+APPLY)
|
||||
if not ((plugin_id == 89 and digest == DECKYSPY_DIGEST)
|
||||
or (plugin_id == 114 and digest == NETWORK_SENTINEL_DIGEST)):
|
||||
return APPLY
|
||||
wheels = {}
|
||||
for arch, expected in PSUTIL_DIGESTS.items():
|
||||
@@ -151,13 +211,15 @@ def apply_script(plugin_id, digest):
|
||||
raise ValueError('Bundled psutil dependency SHA-256 mismatch')
|
||||
wheels[arch] = base64.b64encode(data).decode('ascii')
|
||||
# stdin is Python source, never shell arguments or a shared remote temp file.
|
||||
battery = (Path(__file__).parent/'framey_battery.py').read_text(encoding='utf-8')
|
||||
return ('FRAMEY_BACKEND_WHEELS = '+repr(wheels)+'\n'
|
||||
+'FRAMEY_BACKEND_BATTERY_SOURCE = '+repr(battery)+'\n'+APPLY)
|
||||
source = 'FRAMEY_BACKEND_WHEELS = '+repr(wheels)+'\n'
|
||||
if plugin_id == 89:
|
||||
battery = (Path(__file__).parent/'framey_battery.py').read_text(encoding='utf-8')
|
||||
source += 'FRAMEY_BACKEND_BATTERY_SOURCE = '+repr(battery)+'\n'
|
||||
return source+APPLY
|
||||
|
||||
|
||||
APPLY = r'''
|
||||
import base64,fcntl,hashlib,io,json,os,pathlib,platform,shutil,subprocess,sys,zipfile
|
||||
import base64,fcntl,gzip,hashlib,io,json,os,pathlib,platform,shutil,subprocess,sys,zipfile
|
||||
action,token,plugin_id,digest,folder,version,expected_installed = sys.argv[1:]
|
||||
base=pathlib.Path.home()/'.local/share/frame-control/decky/plugins'
|
||||
unit=pathlib.Path.home()/'.config/systemd/user/frame-control-decky.service'
|
||||
@@ -199,6 +261,17 @@ 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')
|
||||
fileserver=(int(plugin_id)==60 and digest=='7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0' and folder=='DeckyFileServer')
|
||||
localsend=(int(plugin_id)==125 and digest=='e271ccb55eb23f918b99a69f79aec905df4c9b2ef7b23bbf4ec1c2d1a6f0c6b7' and folder=='decky-localsend')
|
||||
network_sentinel=(int(plugin_id)==114 and digest=='d3e07f40a0c800e72d15594e03c8c61e9bf3092524a578a0260520e735b971a5' and folder=='network-sentinel')
|
||||
battery_tracker=(int(plugin_id)==67 and digest=='91c388d3c1b52079314045b79671ad55c767510658b6193f6936ed6cad383b94' and folder=='steam-deck-battery-tracker')
|
||||
manifest=json.loads(z.read(folder+'/plugin.json'))
|
||||
if not isinstance(manifest,dict): raise RuntimeError('Invalid plugin manifest')
|
||||
flags=manifest.get('flags',[])
|
||||
if not isinstance(flags,list) or not all(isinstance(flag,str) for flag in flags):
|
||||
raise RuntimeError('Invalid plugin flags')
|
||||
if ('root' in flags or '_root' in flags) and not (fileserver or network_sentinel or battery_tracker):
|
||||
raise RuntimeError('Root plugins are not supported by the Frame user service')
|
||||
seen={}
|
||||
for f in files:
|
||||
parts=f.filename.split('/')
|
||||
@@ -219,6 +292,15 @@ try:
|
||||
z.read(prior)!=z.read(f)):
|
||||
raise RuntimeError('Unsafe plugin archive')
|
||||
else: seen[f.filename]=f
|
||||
if not f.is_dir():
|
||||
with z.open(f) as source: header=source.read(20)
|
||||
if header[:4]==b'\x7fELF' and int.from_bytes(header[18:20],'little')!=183:
|
||||
if not ((fileserver and f.filename==folder+'/bin/backend' and
|
||||
hashlib.sha256(z.read(f)).hexdigest()=='d9eab8559c87c01736188bc676f3b4b44dee47cf952b6e571c770d1ea61733e9')
|
||||
or (localsend and f.filename==folder+'/bin/localsend-core' and
|
||||
hashlib.sha256(z.read(f)).hexdigest()=='d448d03861d578685e6c70ea3ace25eb34fca99cb9d4f98bf51ba23aa5cae843')):
|
||||
raise RuntimeError('Plugin contains a non-ARM64 Linux binary')
|
||||
if header[:2]==b'MZ': raise RuntimeError('Plugin contains a Windows binary')
|
||||
stage.mkdir(mode=0o700)
|
||||
z.extractall(stage)
|
||||
# zipfile drops execute bits. Legacy plugins also publish shebang
|
||||
@@ -234,6 +316,76 @@ try:
|
||||
unpacked=stage/folder
|
||||
if not unpacked.is_dir(): raise RuntimeError('Missing plugin folder')
|
||||
compat_shims=[]
|
||||
if localsend:
|
||||
helper=unpacked/'bin/localsend-core'
|
||||
source=unpacked/'main.py'
|
||||
if hashlib.sha256(helper.read_bytes()).hexdigest()!='d448d03861d578685e6c70ea3ace25eb34fca99cb9d4f98bf51ba23aa5cae843':
|
||||
raise RuntimeError('LocalSend original helper mismatch')
|
||||
if hashlib.sha256(source.read_bytes()).hexdigest()!='db03f9201fe50b75dea99a7a05b54ec3f352d1bdfe174115b0e569154198d8ad':
|
||||
raise RuntimeError('LocalSend Python source mismatch')
|
||||
if manifest.get('name')!='Decky Localsend' or manifest.get('api_version')!=1 or manifest.get('flags')!=[]:
|
||||
raise RuntimeError('LocalSend manifest mismatch')
|
||||
arch=platform.machine()
|
||||
if arch=='aarch64':
|
||||
packed=globals().get('FRAMEY_LOCALSEND_AARCH64')
|
||||
if not isinstance(packed,str): raise RuntimeError('Missing LocalSend ARM64 helper')
|
||||
native=gzip.decompress(base64.b64decode(packed,validate=True))
|
||||
if (hashlib.sha256(native).hexdigest()!='e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae'
|
||||
or native[:4]!=b'\x7fELF' or int.from_bytes(native[18:20],'little')!=183):
|
||||
raise RuntimeError('LocalSend ARM64 helper mismatch')
|
||||
helper.write_bytes(native)
|
||||
elif arch!='x86_64':
|
||||
raise RuntimeError('Unsupported LocalSend helper architecture')
|
||||
helper.chmod(0o700)
|
||||
compat_shims.append('localsend-digest-native')
|
||||
# GPL source and reproducible build recipe ship with Frame Control.
|
||||
# The exact official helper is x86_64; this plugin needs no root.
|
||||
if int(plugin_id)==60 and digest=='7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0' and folder=='DeckyFileServer':
|
||||
helper=unpacked/'bin/backend'
|
||||
source=unpacked/'main.py'
|
||||
if hashlib.sha256(helper.read_bytes()).hexdigest()!='d9eab8559c87c01736188bc676f3b4b44dee47cf952b6e571c770d1ea61733e9':
|
||||
raise RuntimeError('DeckyFileServer original helper mismatch')
|
||||
if hashlib.sha256(source.read_bytes()).hexdigest()!='4fb60745c083465d198d84e1abb0afdb59695d9eec3c873b0b35247676af05c3':
|
||||
raise RuntimeError('DeckyFileServer Python source mismatch')
|
||||
arch=platform.machine()
|
||||
machine={'aarch64':183,'x86_64':62}.get(arch)
|
||||
hashes=globals().get('FRAMEY_FILESERVER_HASHES',{})
|
||||
packed=globals().get('FRAMEY_FILESERVER_BINARIES',{}).get(arch)
|
||||
if not machine or not packed: raise RuntimeError('Missing native DeckyFileServer helper')
|
||||
native=gzip.decompress(base64.b64decode(packed,validate=True))
|
||||
if (hashlib.sha256(native).hexdigest()!=hashes.get(arch) or native[:4]!=b'\x7fELF'
|
||||
or int.from_bytes(native[18:20],'little')!=machine):
|
||||
raise RuntimeError('DeckyFileServer native helper mismatch')
|
||||
helper.write_bytes(native)
|
||||
helper.chmod(0o700)
|
||||
manifest_path=unpacked/'plugin.json'
|
||||
manifest=json.loads(manifest_path.read_text())
|
||||
if manifest.get('name')!='DeckyFileServer' or manifest.get('api_version')!=1 or manifest.get('flags')!=['_root']:
|
||||
raise RuntimeError('DeckyFileServer manifest mismatch')
|
||||
manifest['flags']=[]
|
||||
manifest['publish']['tags']=[tag for tag in manifest['publish']['tags'] if tag not in ('root','_root')]
|
||||
manifest_path.write_text(json.dumps(manifest)+'\n')
|
||||
text=source.read_text()
|
||||
# Go flag parsing stops at an empty positional argument. Filter
|
||||
# omitted boolean flags so -disablethumbnails actually takes effect.
|
||||
text=text.replace(' ],\n stdout=PIPE,',
|
||||
' ] if arg],\n stdout=PIPE,')
|
||||
text=text.replace(' [\n f"{decky.DECKY_PLUGIN_DIR}',
|
||||
' [arg for arg in [\n f"{decky.DECKY_PLUGIN_DIR}')
|
||||
# Hostname DNS is not guaranteed on Frame OS. A UDP connect
|
||||
# selects the kernel source address without sending any packet.
|
||||
text=text.replace(' return socket.gethostbyname(socket.gethostname())',
|
||||
' try:\n return socket.gethostbyname(socket.gethostname())\n'
|
||||
' except OSError:\n with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe:\n'
|
||||
' try:\n probe.connect(("192.0.2.1", 9))\n'
|
||||
' return probe.getsockname()[0]\n'
|
||||
' except OSError:\n return "127.0.0.1"')
|
||||
# Configure thumbnail policy before launching the helper.
|
||||
old=' if "server_running" in status:\n await Plugin.set_server_running(self, status["server_running"])\n if "disable_thumbnails" in status:\n await Plugin.set_disable_thumbnails(self, status["disable_thumbnails"])'
|
||||
new=' if "disable_thumbnails" in status:\n await Plugin.set_disable_thumbnails(self, status["disable_thumbnails"])\n if "server_running" in status:\n await Plugin.set_server_running(self, status["server_running"])'
|
||||
if old not in text: raise RuntimeError('DeckyFileServer start configuration changed')
|
||||
source.write_text(text.replace(old,new))
|
||||
compat_shims.append('deckyfileserver-user-native')
|
||||
# Simple Timer 1.0.0 has annotation-only task state, orphaned
|
||||
# replacement tasks and canceled deadlines that resume on restart.
|
||||
# Preserve its API1 instance convention and upstream license.
|
||||
@@ -347,10 +499,10 @@ try:
|
||||
' base = os.path.join(os.path.expanduser("~"), ".config")\n'
|
||||
' return os.path.join(base, app_name)\n',encoding='utf-8')
|
||||
compat_shims.append('shotty-click-app-dir')
|
||||
# DeckySpy's official archive omits its required psutil dependency.
|
||||
# The BSD wheels are packaged offline by Frame Control and selected
|
||||
# for the native loader; no pip, network or system path changes.
|
||||
if int(plugin_id)==89 and digest=='2d431a89826ed5cf3fd37258373eb3b46c841c39795aa8b25f83e59f4eee87c7' and folder=='decky-spy':
|
||||
# Both exact official archives omit psutil. Install the pinned BSD
|
||||
# native wheel in the plugin folder only; never use pip or system paths.
|
||||
deckyspy=(int(plugin_id)==89 and digest=='2d431a89826ed5cf3fd37258373eb3b46c841c39795aa8b25f83e59f4eee87c7' and folder=='decky-spy')
|
||||
if deckyspy or network_sentinel:
|
||||
arch=platform.machine()
|
||||
pins={
|
||||
'x86_64':'076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9',
|
||||
@@ -358,13 +510,13 @@ try:
|
||||
}
|
||||
encoded=globals().get('FRAMEY_BACKEND_WHEELS',{}).get(arch)
|
||||
if arch not in pins or not encoded:
|
||||
raise RuntimeError('Missing native psutil dependency for DeckySpy')
|
||||
raise RuntimeError('Missing native psutil dependency')
|
||||
wheel_data=base64.b64decode(encoded,validate=True)
|
||||
if hashlib.sha256(wheel_data).hexdigest()!=pins[arch]:
|
||||
raise RuntimeError('DeckySpy psutil dependency SHA-256 mismatch')
|
||||
raise RuntimeError('Native psutil dependency SHA-256 mismatch')
|
||||
modules=unpacked/'py_modules'
|
||||
for name in ('psutil','psutil-7.2.2.dist-info'):
|
||||
if (modules/name).exists(): raise RuntimeError('DeckySpy dependency collides with archive')
|
||||
if (modules/name).exists(): raise RuntimeError('Native psutil dependency collides with archive')
|
||||
with zipfile.ZipFile(io.BytesIO(wheel_data)) as wheel:
|
||||
for member in wheel.infolist():
|
||||
path=pathlib.PurePosixPath(member.filename)
|
||||
@@ -379,6 +531,124 @@ try:
|
||||
dest.parent.mkdir(parents=True,exist_ok=True)
|
||||
dest.write_bytes(payload)
|
||||
dest.chmod(0o600)
|
||||
if network_sentinel:
|
||||
manifest_path=unpacked/'plugin.json'
|
||||
manifest=json.loads(manifest_path.read_text())
|
||||
main=unpacked/'main.py'
|
||||
if (manifest.get('name')!='Network Sentinel' or manifest.get('api_version')!=1
|
||||
or manifest.get('flags')!=['debug','_root'] or
|
||||
hashlib.sha256(main.read_bytes()).hexdigest()!='2baa390cb66dc267e8f04e61ba1edc43af75115ad5e453d308f6bf26ed70b6b9'):
|
||||
raise RuntimeError('Network Sentinel manifest or source changed')
|
||||
manifest['flags']=['debug']
|
||||
manifest_path.write_text(json.dumps(manifest),encoding='utf-8')
|
||||
source=main.read_text(encoding='utf-8')
|
||||
old='decky.DECKY_SETTINGS_DIR'
|
||||
ping=' cmd = [\'ping\', \'-c\', str(count), \'-W\', \'2\', host]\n'
|
||||
if (source.count(old)!=2 or source.count(' async def _migration(self):')!=1
|
||||
or source.count(ping)!=1):
|
||||
raise RuntimeError('Network Sentinel settings or migration source changed')
|
||||
source=source.replace(old,'decky.DECKY_PLUGIN_SETTINGS_DIR')
|
||||
source=source.replace(ping,' jitter = 0\n'+ping)
|
||||
source=source.replace(' async def _migration(self):',
|
||||
' async def _migration(self):\n # Managed user service does not move legacy Decky paths.\n return\n')
|
||||
main.write_text(source,encoding='utf-8')
|
||||
compat_shims.append('network-sentinel-user-psutil')
|
||||
if battery_tracker:
|
||||
manifest_path=unpacked/'plugin.json'
|
||||
manifest=json.loads(manifest_path.read_text())
|
||||
main=unpacked/'main.py'
|
||||
if (manifest.get('name')!='Battery Tracker' or manifest.get('flags')!=['_root']
|
||||
or 'api_version' in manifest or
|
||||
hashlib.sha256(main.read_bytes()).hexdigest()!='2303ec70c262f1cbfe1c8c138770c6e7c3496827e748e679c94ca43ff9f28725'):
|
||||
raise RuntimeError('Battery Tracker manifest or source changed')
|
||||
manifest['flags']=[]
|
||||
manifest_path.write_text(json.dumps(manifest),encoding='utf-8')
|
||||
source=main.read_text(encoding='utf-8')
|
||||
split_start=source.index('def split_by_app(data):\n')
|
||||
split_end=source.index('\n\nclass Plugin:',split_start)
|
||||
source=source[:split_start]+"""def _framey_battery_path():
|
||||
# A normal user can read Frame's Battery-type supply; BAT1 remains first
|
||||
# on Steam Deck. The override is for a private, networkless runtime fixture.
|
||||
root = Path(os.environ.get('FRAMEY_BATTERY_ROOT', '/sys/class/power_supply'))
|
||||
if not root.is_absolute():
|
||||
raise ValueError('Battery supply root must be absolute')
|
||||
for candidate in [root / 'BAT1', *sorted(root.iterdir())]:
|
||||
try:
|
||||
if ((candidate / 'type').read_text().strip() == 'Battery' and
|
||||
all((candidate / field).is_file() for field in
|
||||
('capacity', 'status', 'voltage_now', 'current_now'))):
|
||||
return candidate
|
||||
except OSError:
|
||||
pass
|
||||
raise FileNotFoundError('No readable Battery-type power supply')
|
||||
|
||||
|
||||
def split_by_app(data):
|
||||
if not data:
|
||||
return []
|
||||
groups = []
|
||||
start = 0
|
||||
app = data[0][4]
|
||||
for index, row in enumerate(data[1:], 1):
|
||||
if row[4] != app:
|
||||
groups.append((start, index, app))
|
||||
start, app = index, row[4]
|
||||
groups.append((start, len(data), app))
|
||||
return groups
|
||||
"""+source[split_end:]
|
||||
old=' self.app = "Unknown"\n'
|
||||
if source.count(old)!=1: raise RuntimeError('Battery Tracker startup source changed')
|
||||
source=source.replace(old,old+' self._pending_samples = []\n')
|
||||
unload_start=source.index(' async def _unload(self):\n')
|
||||
unload_end=source.index(' async def set_app(',unload_start)
|
||||
source=source[:unload_start]+""" async def _unload(self):
|
||||
task = getattr(self, '_recorder_task', None)
|
||||
if task:
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
pending = getattr(self, '_pending_samples', [])
|
||||
if pending:
|
||||
self.cursor.executemany('insert into battery values (?, ?, ?, ?, ?)', pending)
|
||||
self.con.commit()
|
||||
pending.clear()
|
||||
if hasattr(self, 'con'):
|
||||
self.con.close()
|
||||
|
||||
"""+source[unload_end:]
|
||||
old=' ).fetchall()\n'
|
||||
if source.count(old)!=2: raise RuntimeError('Battery Tracker query source changed')
|
||||
query=' data = self.cursor.execute(\n "select * from battery where time > " + str(int(start_time))\n ).fetchall()\n'
|
||||
if source.count(query)!=1: raise RuntimeError('Battery Tracker history source changed')
|
||||
source=source.replace(query,query+' data.extend(row for row in self._pending_samples if row[0] > start_time)\n')
|
||||
recorder_start=source.index(' async def recorder(self):\n')
|
||||
source=source[:recorder_start]+""" async def recorder(self):
|
||||
battery = _framey_battery_path()
|
||||
decky_plugin.logger.info('recorder started')
|
||||
while True:
|
||||
try:
|
||||
volt = int((battery / 'voltage_now').read_text().strip())
|
||||
curr = int((battery / 'current_now').read_text().strip())
|
||||
cap = int((battery / 'capacity').read_text().strip())
|
||||
status = (battery / 'status').read_text().strip()
|
||||
state = -1 if status == 'Discharging' else 1 if status == 'Charging' else 0
|
||||
power = int(volt * curr * 10.0**-11)
|
||||
self._pending_samples.append((int(time.time()), cap, state, power, self.app))
|
||||
if len(self._pending_samples) > 10:
|
||||
self.cursor.executemany('insert into battery values (?, ?, ?, ?, ?)', self._pending_samples)
|
||||
self.con.commit()
|
||||
self._pending_samples.clear()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception:
|
||||
decky_plugin.logger.exception('recorder')
|
||||
await asyncio.sleep(5)
|
||||
"""
|
||||
main.write_text(source,encoding='utf-8')
|
||||
compat_shims.append('battery-tracker-user-supply')
|
||||
if deckyspy:
|
||||
compat=modules/'framey_battery.py'
|
||||
if compat.exists(): raise RuntimeError('DeckySpy battery module collides with archive')
|
||||
battery_source=globals().get('FRAMEY_BACKEND_BATTERY_SOURCE')
|
||||
|
||||
+10
-1
@@ -137,7 +137,16 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return self.reply(403, {'error': 'Forbidden'})
|
||||
try:
|
||||
size = int(self.headers.get('Content-Length', '0'))
|
||||
if not 0 < size <= 1024:
|
||||
if size > 1024:
|
||||
# Windows can reset a connection closed with an unread POST
|
||||
# body before the client receives our 400 response. Drain only
|
||||
# a bounded rejected body; never buffer an unbounded payload.
|
||||
if size <= 64 * 1024:
|
||||
self.rfile.read(size)
|
||||
else:
|
||||
self.close_connection = True
|
||||
raise ValueError('Invalid request size')
|
||||
if size <= 0:
|
||||
raise ValueError('Invalid request size')
|
||||
body = json.loads(self.rfile.read(size))
|
||||
if not isinstance(body, dict):
|
||||
|
||||
+118
-41
@@ -183,7 +183,7 @@
|
||||
"https://github.com/suchmememanyskill/SDH-CssLoader.git"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "Panel initialization succeeds; CSS themes patch Steam UI and cannot theme the isolated VR shell.",
|
||||
"reason": "Panel initialization succeeds; CSS themes patch Steam UI and cannot theme the isolated VR shell. Shared Toggle value rendering is repaired; other listed blockers remain.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -238,6 +238,11 @@
|
||||
"sha256": "99beaf1374af82408e6d056e0b0147e5fca544e1035fa7ab88419231697bc231",
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
},
|
||||
"toggleContract": {
|
||||
"verified": "local browser control fixture; official Pause Games callback regression",
|
||||
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
|
||||
"backendVerified": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -584,12 +589,13 @@
|
||||
"https://github.com/wynn1212/SDH-PauseGames.git"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "Settings and running-game list initialize in local Chromium with an explicit synthetic game; live Frame empty-list reads and lifetime registration/cleanup passed. Process pause/resume, automatic pausing and suspend integration remain unverified/unsupported; backend writes were denied.",
|
||||
"reason": "Settings and a synthetic running-game list render locally. Actual Decky 3.3.0 discovery, private settings writes/readback and exclusion-list persistence pass on amd64 and ARM64 Frame; standalone Toggle now honors its value prop. Live empty-list/lifetime checks passed earlier. Real process pause/resume, automatic pausing, suspend integration and worn VR remain unverified/unsupported.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame"
|
||||
"fake-Frame",
|
||||
"real Frame SSH"
|
||||
],
|
||||
"browser": {
|
||||
"id": 13,
|
||||
@@ -622,6 +628,11 @@
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
},
|
||||
"toggleContract": {
|
||||
"verified": "local browser control fixture; official Pause Games callback regression",
|
||||
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
|
||||
"backendVerified": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1661,7 +1672,7 @@
|
||||
"https://github.com/jurassicplayer/decky-autosuspend.git"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "Frontend audit failed: Cannot read properties of null (reading '6').",
|
||||
"reason": "Frontend audit failed: Cannot read properties of null (reading '6'). The exact frontend also contains SteamOS suspend and shutdown paths; threshold behavior and safe Frame operation are unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -1962,7 +1973,7 @@
|
||||
"https://github.com/jurassicplayer/decky-autoflatpaks.git"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "This plugin needs root and is not supported by the Frame user service.",
|
||||
"reason": "This plugin needs root and is not supported by the Frame user service. Shared Toggle value rendering is repaired; other listed blockers remain.",
|
||||
"blockInstall": true,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -2004,6 +2015,11 @@
|
||||
"archiveValidation": "This plugin needs root and is not supported by the Frame user service",
|
||||
"result": "blocked",
|
||||
"reason": "ValueError: Root plugins are not supported by the Frame user service"
|
||||
},
|
||||
"toggleContract": {
|
||||
"verified": "local browser control fixture; official Pause Games callback regression",
|
||||
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
|
||||
"backendVerified": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4332,13 +4348,15 @@
|
||||
"https://github.com/SteamDeckHomebrew/decky-plugin-template.git",
|
||||
"https://github.com/grimkor/DeckyFileServer"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "Plugin contains a non-ARM64 Linux binary: DeckyFileServer/bin/backend.",
|
||||
"blockInstall": true,
|
||||
"status": "partial",
|
||||
"reason": "Managed install supplies a native ARM64 helper and removes the exact archive's unnecessary root request. Real Decky 3.3.0 on Frame passes private read-only HTTPS listing/download and upload denial. Frontend address/picker limitations remain; use HTTPS and a preconfigured directory. Uploads, generated thumbnails and worn VR are unverified. Shared Toggle value rendering is repaired; other listed blockers remain.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame"
|
||||
"fake-Frame",
|
||||
"real Decky 3.3.0 amd64",
|
||||
"real Frame SSH"
|
||||
],
|
||||
"browser": {
|
||||
"id": 60,
|
||||
@@ -4357,7 +4375,7 @@
|
||||
},
|
||||
"backend": "Python + native payload",
|
||||
"hardware": "No specific hardware requirement detected statically; runtime requirements unverified.",
|
||||
"nativePort": "Native file-server backend needs ARM64 dependencies; root requirement also blocks the user service.",
|
||||
"nativePort": "GPL-3.0 helper rebuilt offline for ARM64 with vendored corresponding source. Actual Frame user service passes HTTPS listing/download of a private public fixture; uploads disabled. Exact managed digest only; other native/root archives remain blocked (docs/evidence/framey-fileserver-arm64-2026-10-27.md).",
|
||||
"uiSurfaces": [],
|
||||
"fakeFrame": {
|
||||
"id": 60,
|
||||
@@ -4365,6 +4383,20 @@
|
||||
"archiveValidation": "Plugin contains a non-ARM64 Linux binary: DeckyFileServer/bin/backend",
|
||||
"result": "blocked",
|
||||
"reason": "ValueError: Root plugins are not supported by the Frame user service"
|
||||
},
|
||||
"requiredShims": [
|
||||
"deckyfileserver-user-native"
|
||||
],
|
||||
"managedNative": {
|
||||
"shim": "deckyfileserver-user-native",
|
||||
"helperSha256": "86b80021ebd62ed619eba065757c1122defa5a74b2c045aa44a0c6b4403a508a",
|
||||
"sourceCommit": "dcf3f3ce5104d05811fa8c156c4f58632b84b313",
|
||||
"verification": "Real Decky discovery/public RPC and private read-only HTTPS fixture; docs/evidence/framey-fileserver-arm64-2026-10-27.md"
|
||||
},
|
||||
"toggleContract": {
|
||||
"verified": "local browser control fixture; official Pause Games callback regression",
|
||||
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
|
||||
"backendVerified": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -4791,7 +4823,7 @@
|
||||
"https://github.com/theCapypara/steamdeck-decky-syncthing.git"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "Plugin contains a non-ARM64 Linux binary: decky-syncthing/bin/decky-syncthing-watchdog.",
|
||||
"reason": "Plugin contains a non-ARM64 Linux binary: decky-syncthing/bin/decky-syncthing-watchdog. Shared Toggle value rendering is repaired; other listed blockers remain.",
|
||||
"blockInstall": true,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -4832,6 +4864,11 @@
|
||||
"archiveValidation": "Plugin contains a non-ARM64 Linux binary: decky-syncthing/bin/decky-syncthing-watchdog",
|
||||
"result": "blocked",
|
||||
"reason": "ValueError: Plugin contains a non-ARM64 Linux binary: decky-syncthing/bin/decky-syncthing-watchdog."
|
||||
},
|
||||
"toggleContract": {
|
||||
"verified": "local browser control fixture; official Pause Games callback regression",
|
||||
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
|
||||
"backendVerified": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -5139,13 +5176,18 @@
|
||||
"https://github.com/safijari/steam-deck-battery-tracker",
|
||||
"https://github.com/safijari/steam-deck-battery-tracker.git"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "This plugin needs root and is not supported by the Frame user service.",
|
||||
"blockInstall": true,
|
||||
"status": "partial",
|
||||
"reason": "Exact managed install removes the unnecessary root flag, selects a readable Battery-type power supply, and repairs empty history and per-app sample grouping. Actual Decky 3.3.0 amd64 recorded Frame-shaped private battery samples and returned history/power data. A locked real Frame read verified the required sysfs fields, not the managed plugin. Live ARM64 recording, long-term battery history, foreground-app labeling, panel controls and worn VR remain unverified; existing copies need update or reinstall.",
|
||||
"blockInstall": false,
|
||||
"requiredShims": [
|
||||
"battery-tracker-user-supply"
|
||||
],
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame"
|
||||
"fake-Frame",
|
||||
"isolated Decky amd64 backend",
|
||||
"real Frame read-only sysfs"
|
||||
],
|
||||
"browser": {
|
||||
"id": 67,
|
||||
@@ -5169,8 +5211,8 @@
|
||||
},
|
||||
"archiveValidation": {
|
||||
"id": 67,
|
||||
"accepted": false,
|
||||
"reason": "This plugin needs root and is not supported by the Frame user service"
|
||||
"accepted": true,
|
||||
"reason": "Exact digest-scoped user-service adaptation"
|
||||
},
|
||||
"backend": "Python",
|
||||
"hardware": "/sys/class/power_supply/BAT1/capacity; /sys/class/power_supply/BAT1/current_now; /sys/class/power_supply/BAT1/status; /sys/class/power_supply/BAT1/voltage_now",
|
||||
@@ -5180,9 +5222,8 @@
|
||||
"fakeFrame": {
|
||||
"id": 67,
|
||||
"sha256": "91c388d3c1b52079314045b79671ad55c767510658b6193f6936ed6cad383b94",
|
||||
"archiveValidation": "This plugin needs root and is not supported by the Frame user service",
|
||||
"result": "blocked",
|
||||
"reason": "ValueError: Root plugins are not supported by the Frame user service"
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -5817,7 +5858,7 @@
|
||||
"https://github.com/jurassicplayer/decky-autosuspend"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "Frontend audit failed: Cannot convert undefined or null to object.",
|
||||
"reason": "Frontend audit failed: Cannot convert undefined or null to object. Its primary trainer workflow uses a Steam game context menu and launch-option writes, with no target in the isolated Framey panel; no trainer launch was verified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -9830,7 +9871,7 @@
|
||||
"https://github.com/xXJSONDeruloXx/lsfg-vk"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "Initializer succeeds; runtime LSFG/Windows DLL downloads and Vulkan integration are not ARM64-validated.",
|
||||
"reason": "Initializer succeeds; runtime LSFG/Windows DLL downloads and Vulkan integration are not ARM64-validated. Shared Toggle value rendering is repaired; other listed blockers remain.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -9879,6 +9920,11 @@
|
||||
"evidence": "docs/evidence/framey-running-context-2026-10-26.json",
|
||||
"mode": "synthetic nonempty running/library replay; backend and network denied",
|
||||
"frontendReady": true
|
||||
},
|
||||
"toggleContract": {
|
||||
"verified": "local browser control fixture; official Pause Games callback regression",
|
||||
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
|
||||
"backendVerified": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -9927,13 +9973,17 @@
|
||||
"https://github.com/SteamDeckHomebrew/decky-frontend-lib",
|
||||
"https://github.com/SteamDeckHomebrew/decky-loader"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "This plugin needs root and is not supported by the Frame user service.",
|
||||
"blockInstall": true,
|
||||
"status": "partial",
|
||||
"reason": "Exact managed install removes the unnecessary root flag and adds pinned plugin-local psutil wheels. Actual Decky 3.3.0 amd64 in network isolation loads the backend and records a network sample; private settings and read-only RPCs pass. External ping/DNS, real Frame ARM64, Wi-Fi scan behavior, panel controls and worn VR remain unverified. Existing copies need update or reinstall.",
|
||||
"blockInstall": false,
|
||||
"requiredShims": [
|
||||
"network-sentinel-user-psutil"
|
||||
],
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame"
|
||||
"fake-Frame",
|
||||
"isolated Decky amd64 backend"
|
||||
],
|
||||
"browser": {
|
||||
"id": 114,
|
||||
@@ -9972,8 +10022,8 @@
|
||||
},
|
||||
"archiveValidation": {
|
||||
"id": 114,
|
||||
"accepted": false,
|
||||
"reason": "This plugin needs root and is not supported by the Frame user service"
|
||||
"accepted": true,
|
||||
"reason": "Exact digest-scoped user-service adaptation"
|
||||
},
|
||||
"backend": "Python",
|
||||
"hardware": "No specific hardware requirement detected statically; runtime requirements unverified.",
|
||||
@@ -9983,9 +10033,8 @@
|
||||
"fakeFrame": {
|
||||
"id": 114,
|
||||
"sha256": "d3e07f40a0c800e72d15594e03c8c61e9bf3092524a578a0260520e735b971a5",
|
||||
"archiveValidation": "This plugin needs root and is not supported by the Frame user service",
|
||||
"result": "blocked",
|
||||
"reason": "ValueError: This plugin needs root and is not supported by the Frame user service."
|
||||
"archiveValidation": "accepted",
|
||||
"result": "installed-removed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -10200,7 +10249,7 @@
|
||||
"https://github.com/wtlnetwork/muon.git"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "This plugin needs root and is not supported by the Frame user service.",
|
||||
"reason": "This plugin needs root and is not supported by the Frame user service. Shared Toggle value rendering is repaired; other listed blockers remain.",
|
||||
"blockInstall": true,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -10244,6 +10293,11 @@
|
||||
"archiveValidation": "This plugin needs root and is not supported by the Frame user service",
|
||||
"result": "blocked",
|
||||
"reason": "ValueError: Root plugins are not supported by the Frame user service"
|
||||
},
|
||||
"toggleContract": {
|
||||
"verified": "local browser control fixture; official Pause Games callback regression",
|
||||
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
|
||||
"backendVerified": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -11095,13 +11149,17 @@
|
||||
"https://github.com/moyoez/decky-localsend",
|
||||
"https://github.com/moyoez/decky-localsend.git"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "Plugin contains a non-ARM64 Linux binary: decky-localsend/bin/localsend-core.",
|
||||
"blockInstall": true,
|
||||
"status": "partial",
|
||||
"reason": "Exact managed 0.4.5 archive replaces its x86 helper with a pinned ARM64 build. Published Decky 3.3.0 ARM64 stopped status/configuration RPCs and a private real-Frame ARM64 helper marker transfer passed. The managed service, user-facing discovery/transfer, Steam routes and worn VR remain unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame"
|
||||
"exact archive and source hashes",
|
||||
"local Chromium initial panel",
|
||||
"host managed transaction",
|
||||
"fake Frame managed install/remove",
|
||||
"private x86 backend transfer",
|
||||
"published Decky 3.3.0 AMD64/ARM64 stopped status/configuration RPCs",
|
||||
"private real Frame ARM64 helper marker transfer"
|
||||
],
|
||||
"browser": {
|
||||
"id": 125,
|
||||
@@ -11127,7 +11185,7 @@
|
||||
},
|
||||
"backend": "Python + native payload",
|
||||
"hardware": "No specific hardware requirement detected statically; runtime requirements unverified.",
|
||||
"nativePort": "MIT LocalSend-Go 66edc509a28b9f877e7374504892dcbc653a4da4 reproducibly cross-built with Go 1.25.7/CGO_ENABLED=0 for ARM64; SHA-256 e79238f6051ce34f172f765ae918d3d902b819e27377989a55e5b1eba10e46f9. Help/official backend CLI flags and protocol 2.0 info endpoint passed on real Frame in an isolated network namespace. No file transfer or full plugin/CEF integration tested; exact source match to shipped helper unproven. Official ZIP stays x86_64 and blocked.",
|
||||
"nativePort": "Exact official x86 helper is reproducible from pinned MIT source; the managed installer replaces only the exact 0.4.5 digest with SHA-256 e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae on ARM64. The Frame rechecks original Python/helper bytes, replacement digest and ELF machine; x86 keeps the original helper. Local official x86 backend/proxy and real Frame rebuilt ARM64 helper each passed one private marker transfer. Published Decky 3.3.0 AMD64/ARM64 discovery and stopped status/configuration RPCs passed. The actual managed Decky service on Frame, network discovery, user-facing transfer, Steam routes and worn VR remain unverified (docs/evidence/framey-localsend-managed-2026-10-27.md).",
|
||||
"uiSurfaces": [
|
||||
"local/Steam routes",
|
||||
"toasts",
|
||||
@@ -11139,6 +11197,15 @@
|
||||
"archiveValidation": "Plugin contains a non-ARM64 Linux binary: decky-localsend/bin/localsend-core",
|
||||
"result": "blocked",
|
||||
"reason": "ValueError: Plugin contains a non-ARM64 Linux binary: decky-localsend/bin/localsend-core."
|
||||
},
|
||||
"requiredShims": [
|
||||
"localsend-digest-native"
|
||||
],
|
||||
"managedNative": {
|
||||
"shim": "localsend-digest-native",
|
||||
"helperSha256": "e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae",
|
||||
"sourceCommit": "ad28b8e9440e17e44b4735a6c4aa0512009cf333",
|
||||
"verification": "Host transaction and fake Frame managed install/remove; local original x86 backend private marker transfer. Published Decky ARM64 runtime and real Frame transfer pending (docs/evidence/framey-localsend-managed-2026-10-27.md)."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -11227,7 +11294,7 @@
|
||||
"https://github.com/casey/just"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "Plugin contains a non-ARM64 Linux binary: decky-launch-options/bin/hello. Frontend audit failed: Cannot convert undefined or null to object.",
|
||||
"reason": "Plugin contains a non-ARM64 Linux binary: decky-launch-options/bin/hello. Frontend audit failed: Cannot convert undefined or null to object. Shared Toggle value rendering is repaired; other listed blockers remain.",
|
||||
"blockInstall": true,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -11261,6 +11328,11 @@
|
||||
"archiveValidation": "Plugin contains a non-ARM64 Linux binary: decky-launch-options/bin/hello",
|
||||
"result": "blocked",
|
||||
"reason": "ValueError: Plugin contains a non-ARM64 Linux binary: decky-launch-options/bin/hello. Frontend audit failed: Cannot convert undefined or null to object."
|
||||
},
|
||||
"toggleContract": {
|
||||
"verified": "local browser control fixture; official Pause Games callback regression",
|
||||
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
|
||||
"backendVerified": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -11545,7 +11617,7 @@
|
||||
"https://github.com/tikh-iv/ShadowDeck.git"
|
||||
],
|
||||
"status": "broken",
|
||||
"reason": "This plugin needs root and is not supported by the Frame user service.",
|
||||
"reason": "This plugin needs root and is not supported by the Frame user service. Shared Toggle value rendering is repaired; other listed blockers remain.",
|
||||
"blockInstall": true,
|
||||
"verification": [
|
||||
"static archive",
|
||||
@@ -11583,6 +11655,11 @@
|
||||
"archiveValidation": "This plugin needs root and is not supported by the Frame user service",
|
||||
"result": "blocked",
|
||||
"reason": "ValueError: Root plugins are not supported by the Frame user service"
|
||||
},
|
||||
"toggleContract": {
|
||||
"verified": "local browser control fixture; official Pause Games callback regression",
|
||||
"evidence": "docs/evidence/framey-toggle-2026-10-27.md",
|
||||
"backendVerified": false
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+28
-6
@@ -56,6 +56,7 @@
|
||||
const pending = new Map();
|
||||
const routes = new Map();
|
||||
const routeListeners = new Set();
|
||||
const routeHistory = [];
|
||||
let currentRoute = null;
|
||||
let currentParams = {};
|
||||
let nextId = 0;
|
||||
@@ -219,6 +220,11 @@
|
||||
};
|
||||
const Field = ({label, description, children}) => R.createElement('label', null,
|
||||
R.createElement('strong', null, label), description && R.createElement('small', null, description), children);
|
||||
// DFL/@decky/ui Toggle uses value; ToggleField uses checked. Keep the
|
||||
// standalone control separate so persisted true settings render correctly.
|
||||
const Toggle = ({value, disabled, onChange}) => R.createElement('input', {
|
||||
type:'checkbox', checked:!!value, disabled:!!disabled,
|
||||
onChange:e=>{if(!disabled)onChange?.(e.target.checked);}});
|
||||
const ToggleField = ({label, description, checked, disabled, onChange}) => R.createElement(Field, {label, description},
|
||||
R.createElement('input', {type:'checkbox', checked:!!checked, disabled:!!disabled,
|
||||
onChange:e=>{if(!disabled)onChange?.(e.target.checked);}}));
|
||||
@@ -370,15 +376,31 @@
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function navigate(path) {
|
||||
if (path !== null && !localPath(path)) return false;
|
||||
const match = path === null ? null : activeRoute(path);
|
||||
if (path !== null && !match) return false;
|
||||
function showRoute(path, match) {
|
||||
currentRoute = path;
|
||||
currentParams = match?.params || {};
|
||||
for (const listener of routeListeners) listener(path);
|
||||
return true;
|
||||
}
|
||||
function navigate(path) {
|
||||
if (path !== null && !localPath(path)) return false;
|
||||
const match = path === null ? null : activeRoute(path);
|
||||
if (path !== null && !match) return false;
|
||||
if (path === null) routeHistory.length = 0;
|
||||
else if (path !== currentRoute) {
|
||||
routeHistory.push(currentRoute);
|
||||
if (routeHistory.length > 32) routeHistory.shift();
|
||||
}
|
||||
return showRoute(path, match);
|
||||
}
|
||||
function navigateBack() {
|
||||
while (routeHistory.length) {
|
||||
const previous = routeHistory.pop();
|
||||
const match = previous === null ? null : activeRoute(previous);
|
||||
if (previous === null || match) return showRoute(previous, match);
|
||||
}
|
||||
return navigate(null);
|
||||
}
|
||||
const routerHook = {
|
||||
addRoute(path, component) {
|
||||
if (!routePattern(path) ||
|
||||
@@ -478,7 +500,7 @@
|
||||
Focusable, ScrollPanel: passthrough('div'), ScrollPanelGroup: passthrough('div'),
|
||||
DialogButton: passthrough('button'), DialogButtonPrimary: passthrough('button'), ConfirmModal,
|
||||
DialogButtonSecondary: passthrough('button'), Button: ButtonItem, GamepadButton: passthrough('button'),
|
||||
Toggle: ToggleField, Spinner, SteamSpinner:Spinner, Tabs, SidebarNavigation,
|
||||
Toggle, Spinner, SteamSpinner:Spinner, Tabs, SidebarNavigation,
|
||||
SuspensefulImage:({alt='', ...props})=>R.createElement('img', {...props, alt}),
|
||||
Marquee:passthrough('span'), DialogBody:passthrough('div'), DialogBodyText:passthrough('p'),
|
||||
DialogHeader:passthrough('h2'), DialogSubHeader:passthrough('h3'), DialogFooter:passthrough('footer'),
|
||||
@@ -493,7 +515,7 @@
|
||||
joinClassNames:(...names)=>names.filter(Boolean).join(' '),
|
||||
sleep:ms=>new Promise(resolve=>setTimeout(resolve,Math.max(0,Number(ms)||0))),
|
||||
staticClasses: {Title:'framey-title'},
|
||||
Navigation: {Navigate:navigate, NavigateBack:()=>navigate(null), CloseSideMenus:()=>navigate(null),
|
||||
Navigation: {Navigate:navigate, NavigateBack:navigateBack, CloseSideMenus:()=>navigate(null),
|
||||
NavigateToExternalWeb: url => { if (/^https:\/\//.test(url)) window.open(url, '_blank', 'noopener'); }},
|
||||
showModal, showContextMenu:showModal,
|
||||
// Steam app, library, and store patches have no target inside this panel.
|
||||
|
||||
+12
-7
@@ -72,7 +72,8 @@ const byId=id=>document.getElementById(id);
|
||||
let latestStatus=null, activeExtension=null, recordBusy=false;
|
||||
async function api(path,body){const response=await fetch(path,{method:body?'POST':'GET',
|
||||
headers:{'X-Frame-UI':key,'Content-Type':'application/json'},body:body?JSON.stringify(body):undefined});
|
||||
const data=await response.json();if(!response.ok)throw Error(data.error||'Frame request failed');return data;}
|
||||
const data=await response.json();if(!response.ok){const error=Error(data.error||'Frame request failed');
|
||||
error.status=response.status;throw error;}return data;}
|
||||
async function refresh(){try{const status=latestStatus=await api('/api/status');
|
||||
byId('battery').textContent=status.battery?.percent==null?'Unavailable':`${status.battery.percent}%`;
|
||||
const free=status.disk?.home?.free;
|
||||
@@ -107,9 +108,8 @@ async function refreshGames(){const list=byId('games');try{const data=await api(
|
||||
row.append(name,quit);list.append(row);}if(!data.running.length)list.textContent='No games are running.';
|
||||
}catch(error){list.textContent=error.message;}}
|
||||
function compatibilityText(plugin){const c=plugin.compatibility;return c ? `${c.status === 'na' ? 'N/A' : c.status}: ${c.reason}${c.verification?.length ? ' Verified: '+c.verification.join(', ')+'.' : ''}` : 'Unverified on Frame';}
|
||||
// Some Chromium profiles deny sessionStorage. Keep the accepted job ID in
|
||||
// memory so this open panel can still reconnect instead of offering a second
|
||||
// install while the first one may be running.
|
||||
// Some Chromium profiles deny sessionStorage. Keep the reserved request ID in
|
||||
// memory so this open panel can still reconnect if the POST response is lost.
|
||||
let deckyPendingJob=null;
|
||||
function savedDeckyJob(){
|
||||
if(deckyPendingJob)return deckyPendingJob;
|
||||
@@ -124,6 +124,8 @@ function clearDeckyJob(){
|
||||
try{sessionStorage.removeItem('frameyPluginJob');}catch(_){}
|
||||
}
|
||||
function deckyJobPending(){return !!savedDeckyJob();}
|
||||
function newDeckyRequestId(){return Array.from(crypto.getRandomValues(new Uint8Array(16)),
|
||||
byte=>byte.toString(16).padStart(2,'0')).join('');}
|
||||
let deckyState=null, deckyStore=[], deckyStoreIndex=0, deckyStoreRequest=0,
|
||||
deckyStatusRequest=0,
|
||||
activeFrameyId=null, activeFrameyReceipt=null, frameySession=null, deckyBusy=false;
|
||||
@@ -200,7 +202,7 @@ async function followDeckyJob(job,action,name){
|
||||
catch(error){
|
||||
if(/no such job/i.test(error.message)){
|
||||
clearDeckyJob();
|
||||
throw Error('The app restarted and lost this job result. Refresh installed plugins to see what changed.');
|
||||
throw Error('The server has no record of this request. It may not have arrived, or the app may have restarted. Refresh installed plugins before trying again.');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -224,14 +226,17 @@ async function deckyChange(action,plugin){if(deckyBusy)return;
|
||||
if(panelClosed)closeFrameyPanel();
|
||||
deckyJobView(`${action} ${plugin.name}`,
|
||||
panelClosed?'Starting… Old plugin panel closed until the change finishes.':'Starting…',0);
|
||||
const requestId=newDeckyRequestId();
|
||||
rememberDeckyJob({job:requestId,action,name:plugin.name});
|
||||
try{const started=await api('/api/decky/plugin',{action,id:plugin.id,
|
||||
sha256:action==='remove'?undefined:plugin.sha256,acknowledged:true});
|
||||
sha256:action==='remove'?undefined:plugin.sha256,acknowledged:true,requestId});
|
||||
if(!started.job)throw Error('The server did not return a job ID');
|
||||
rememberDeckyJob({job:started.job,action,name:plugin.name});
|
||||
await followDeckyJob(started.job,action,plugin.name);
|
||||
clearDeckyJob();
|
||||
if(panelNote)byId('deckyJobStatus').textContent+=panelNote;
|
||||
}catch(error){deckyJobView(`${action} ${plugin.name}`,`Error: ${error.message}${panelNote}`,null,true);}
|
||||
}catch(error){if(error.status>=400&&error.status<500)clearDeckyJob();
|
||||
deckyJobView(`${action} ${plugin.name}`,`Error: ${error.message}${panelNote}`,null,true);}
|
||||
finally{deckyBusy=false;await refreshDecky();}
|
||||
}
|
||||
async function checkDeckyJob(){const saved=savedDeckyJob();
|
||||
|
||||
+8
-3
@@ -1686,6 +1686,7 @@
|
||||
<option value="vr">VR app</option>
|
||||
</select>
|
||||
<div class="progress" id="prog"><i></i></div>
|
||||
<div class="hint">The desktop app copies files from this computer directly, including files larger than 8 GiB. Browser uploads are limited to 8 GiB.</div>
|
||||
<div class="hint">Clipboard text, both ways, is in <a href="#pad">Type on the headset</a>.</div>
|
||||
</section>
|
||||
|
||||
@@ -3843,6 +3844,7 @@ window.addEventListener("drop", e => {
|
||||
function upload(file, mode, dev = window.connDevice, backupToken = null, progress = () => {}, signal = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (signal?.aborted) return reject(new Error("Import cancelled"));
|
||||
if (file.size > 8 * 1024 ** 3) return reject(new Error("Browser uploads are limited to 8 GiB; use the desktop app to copy larger files"));
|
||||
const xhr = new XMLHttpRequest();
|
||||
const abort = () => xhr.abort();
|
||||
const cleanup = () => signal?.removeEventListener("abort", abort);
|
||||
@@ -3875,6 +3877,9 @@ function upload(file, mode, dev = window.connDevice, backupToken = null, progres
|
||||
xhr.send(file);
|
||||
});
|
||||
}
|
||||
function copyToDownloads(file, path, dev) {
|
||||
return path ? api("/api/transfer", { action: "copy-local", path }, dev) : upload(file, "push", dev);
|
||||
}
|
||||
async function refreshMedia(selectId) {
|
||||
const data = await api("/api/media", { action: "list" });
|
||||
const selected = selectId || $("mediaFile").value;
|
||||
@@ -4001,7 +4006,7 @@ async function sendFilesOne(files, dirs, dev) {
|
||||
if (TITLE_EXT.test(f.name)) { await sideload(f, path); continue; }
|
||||
const apk = f.name.toLowerCase().endsWith(".apk");
|
||||
if (apk) await importAndroidFiles([f], dev);
|
||||
else await act(`Copy ${f.name} to ~/Downloads`, () => upload(f, "push", dev));
|
||||
else await act(`Copy ${f.name} to ~/Downloads`, () => copyToDownloads(f, path, dev));
|
||||
}
|
||||
$("fileInput").value = "";
|
||||
refresh();
|
||||
@@ -4137,7 +4142,7 @@ async function sideloadOne(file, path, isDir = false, dev = window.connDevice) {
|
||||
catch (e) {
|
||||
log(`${file.name}: ${e.message}`, "e");
|
||||
if (canPush && confirm(`${file.name}: ${e.message}\n\nCopy it to ~/Downloads instead?`))
|
||||
await act(`Copy ${file.name} to ~/Downloads`, () => upload(file, "push", dev));
|
||||
await act(`Copy ${file.name} to ~/Downloads`, () => copyToDownloads(file, path, dev));
|
||||
return;
|
||||
}
|
||||
// Its own fresh list, so the dialog can say whether this replaces an installed title.
|
||||
@@ -4146,7 +4151,7 @@ async function sideloadOne(file, path, isDir = false, dev = window.connDevice) {
|
||||
const choice = await confirmTitle(r.plan, canPush, installed);
|
||||
if (choice === "push") {
|
||||
api("/api/titles", { action: "discard", token: r.token }).catch(() => {});
|
||||
await act(`Copy ${file.name} to ~/Downloads`, () => upload(file, "push", dev));
|
||||
await act(`Copy ${file.name} to ~/Downloads`, () => copyToDownloads(file, path, dev));
|
||||
return;
|
||||
}
|
||||
if (!choice) { api("/api/titles", { action: "discard", token: r.token }).catch(() => {}); return; }
|
||||
|
||||
+56
-9
@@ -28,6 +28,7 @@ import shutil
|
||||
import signal
|
||||
import socket
|
||||
import socketserver
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -345,11 +346,16 @@ def backfill_art(apps=(), titles=(), meant=None):
|
||||
return True
|
||||
|
||||
|
||||
def _new_job_locked(label, **fields):
|
||||
def _prune_jobs_locked():
|
||||
now = time.time()
|
||||
for j in [j for j, v in _jobs.items() if v["done"] and now - v["time"] > JOB_TTL]:
|
||||
del _jobs[j]
|
||||
job = secrets.token_hex(8)
|
||||
|
||||
|
||||
def _new_job_locked(label, job_id=None, **fields):
|
||||
_prune_jobs_locked()
|
||||
now = time.time()
|
||||
job = job_id or secrets.token_hex(8)
|
||||
_jobs[job] = {"label": label, "done": False, "error": None, "message": None, "result": None, "time": now, **fields}
|
||||
return job
|
||||
|
||||
@@ -376,10 +382,18 @@ def _run_job(job, label, work, progress):
|
||||
_jobs[job].update(fields, done=True, time=time.time())
|
||||
|
||||
|
||||
def start_job(label, work, progress=False):
|
||||
def start_job(label, work, progress=False, *, request_id=None, request=None):
|
||||
"""Run work() in the background. It returns a dict with a "message"."""
|
||||
with _jobs_lock:
|
||||
job = _new_job_locked(label)
|
||||
if request_id is not None:
|
||||
_prune_jobs_locked()
|
||||
existing = _jobs.get(request_id)
|
||||
if existing:
|
||||
if existing.get('_request') != request:
|
||||
raise Failure('This plugin request ID was already used for a different change', 409)
|
||||
return {"message": f"{label}…", "job": request_id}
|
||||
job = _new_job_locked(label, job_id=request_id,
|
||||
**({'_request': request} if request_id is not None else {}))
|
||||
busy_thread(_run_job, job, label, work, progress).start()
|
||||
return {"message": f"{label}…", "job": job}
|
||||
|
||||
@@ -462,7 +476,7 @@ def queue_action(body):
|
||||
def job_status(query):
|
||||
with _jobs_lock:
|
||||
job = _jobs.get((parse_qs(query).get("id") or [""])[0])
|
||||
snapshot = job and {k: v for k, v in job.items() if k != "time"}
|
||||
snapshot = job and {k: v for k, v in job.items() if k not in ("time", "_request")}
|
||||
if not snapshot:
|
||||
raise Failure("no such job (the app may have restarted)", 404)
|
||||
return snapshot
|
||||
@@ -572,9 +586,15 @@ def decky_plugin(body):
|
||||
if body.get('acknowledged') is not True:
|
||||
raise Failure('Confirm that store plugins run code as your Frame user', 400)
|
||||
reviewed_hash = body.get('sha256')
|
||||
request_id = body.get('requestId')
|
||||
if request_id is not None and (not isinstance(request_id, str) or
|
||||
not re.fullmatch(r'[0-9a-f]{32}', request_id)):
|
||||
raise Failure('Invalid plugin request ID', 400)
|
||||
device = LINK.active_device()['id'] if LINK else FRAME
|
||||
return start_job('Decky plugin ' + action,
|
||||
lambda report: frame_decky_store.change(ssh, action, plugin_id, report,
|
||||
reviewed_hash=reviewed_hash), progress=True)
|
||||
reviewed_hash=reviewed_hash), progress=True,
|
||||
request_id=request_id, request=(device, action, plugin_id, reviewed_hash))
|
||||
|
||||
|
||||
def framey_method_action(body):
|
||||
@@ -3475,8 +3495,27 @@ def power_action(body):
|
||||
raise Failure(str(exc), 409) from exc
|
||||
|
||||
|
||||
def transfer_local(body):
|
||||
"""Copy a desktop-selected file without a second, size-limited HTTP upload to /tmp."""
|
||||
if os.environ.get("FRAME_CONTROL_APP") != "1":
|
||||
raise Failure("Direct file copy needs the Frame Control desktop app", 403)
|
||||
if body.get("action") != "copy-local" or not isinstance(body.get("path"), str):
|
||||
raise Failure("Choose a file on this computer", 400)
|
||||
path = Path(body["path"])
|
||||
if not path.is_absolute() or not path.name or path.name.startswith("."):
|
||||
raise Failure("Choose a regular file with a visible name", 400)
|
||||
try:
|
||||
info = path.stat()
|
||||
except OSError:
|
||||
raise Failure("That file is no longer available on this computer", 400) from None
|
||||
if not stat.S_ISREG(info.st_mode) or info.st_size <= 0:
|
||||
raise Failure("Choose a nonempty regular file", 400)
|
||||
return {"message": push_file(path)}
|
||||
|
||||
|
||||
POST = {
|
||||
"/api/steam-pipewire": steam_pipewire_action,
|
||||
"/api/transfer": transfer_local,
|
||||
"/api/power": power_action,
|
||||
"/api/android/backup": android_backup, "/api/backup": backup_action,
|
||||
"/api/decky": decky,
|
||||
@@ -3592,6 +3631,12 @@ def _next_chunk(chunks, timeout):
|
||||
def push_file(path, dest="Downloads/"):
|
||||
"""Copy a file to the Frame (as scripts/push.sh): rsync where both ends have it, else scp."""
|
||||
name = Path(path).name
|
||||
# A large file can legitimately take more than an hour over Wi-Fi. Keep a
|
||||
# finite deadline, but allow roughly one MiB/s plus startup time.
|
||||
try:
|
||||
timeout = max(3600, min(24 * 3600, int(Path(path).stat().st_size / (1 << 20)) + 600))
|
||||
except OSError:
|
||||
raise Failure(f"{name} is no longer available on this computer", 400) from None
|
||||
try:
|
||||
# Not on Windows: a Windows rsync (cwRsync, MSYS2) wouldn't take our POSIX -e quoting.
|
||||
if not frame_host.WINDOWS and shutil.which("rsync") and ssh("command -v rsync >/dev/null && echo yes || true").strip() == "yes":
|
||||
@@ -3599,7 +3644,7 @@ def push_file(path, dest="Downloads/"):
|
||||
else:
|
||||
# Modern scp uses SFTP, so the remote path isn't parsed by a shell.
|
||||
cmd = ["scp", *SSH[1:], "-r", str(path), f"{FRAME}:{dest}"]
|
||||
r = frame_host.run_ssh(cmd, capture_output=True, stdin=subprocess.DEVNULL, text=True, errors="replace", timeout=3600)
|
||||
r = frame_host.run_ssh(cmd, capture_output=True, stdin=subprocess.DEVNULL, text=True, errors="replace", timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
raise Failure(f"Copying {name} timed out")
|
||||
if r.returncode != 0:
|
||||
@@ -4051,8 +4096,10 @@ class Handler(BaseHTTPRequestHandler):
|
||||
length = int(self.headers.get("Content-Length") or 0)
|
||||
if not name or name.startswith("."):
|
||||
raise Failure("missing filename", 400)
|
||||
if length <= 0 or length > MAX_UPLOAD:
|
||||
raise Failure("empty or too-large upload", 400)
|
||||
if length <= 0:
|
||||
raise Failure("empty upload", 400)
|
||||
if length > MAX_UPLOAD:
|
||||
raise Failure("Browser uploads are limited to 8 GiB; use the desktop app to copy larger files", 413)
|
||||
if mode in ("apk", "apkinfo") and not name.lower().endswith(".apk"):
|
||||
raise Failure("APK install needs a .apk file", 400)
|
||||
backup = None
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google LLC nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+707
@@ -0,0 +1,707 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
||||
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2023, David Sturman
|
||||
Original Copyright (c) 2022-2023, Steam Deck Homebrew
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
# DeckyFileServer 1.2.0 native helper
|
||||
|
||||
The official store archive (`7dc2ecdcd05aa415d1ac90afaff28ecb17f77809a2bc7a77aee8e92505b976f0`)
|
||||
contains an x86_64 helper. Frame Control replaces only that exact archive's
|
||||
`bin/backend` with the native Linux helper here. No runtime download, compiler,
|
||||
root service or system package is used.
|
||||
|
||||
Upstream: https://github.com/grimkor/DeckyFileServer
|
||||
Pinned source commit: `dcf3f3ce5104d05811fa8c156c4f58632b84b313`.
|
||||
The source tarball fetched from GitHub has SHA-256
|
||||
`0854ca668b64598b0871ceda842912332b75e5c482afeea8a533e4cb38921639`.
|
||||
Its `main.py`, `plugin.json` and `package.json` match the store ZIP byte for byte.
|
||||
The published Go binary has no VCS revision metadata, so its source revision
|
||||
cannot be independently established from the binary.
|
||||
|
||||
`source.tar.gz` is the complete corresponding helper source, embedded resources,
|
||||
Go module metadata, vendored dependencies and their licenses, plus upstream
|
||||
`LICENSE`. SHA-256:
|
||||
`50d90a7328daa412bf963ec68bb4855a9ec0860a8475cdf7d2019e6c2507112d`.
|
||||
It is available in the same app bundle/repository as these binaries. Upstream is
|
||||
GPL-3.0; see `LICENSE.txt`. The statically linked Go runtime uses the BSD license
|
||||
in `GO-LICENSE.txt` (toolchain source: https://go.dev/dl/#go1.25.7).
|
||||
|
||||
The sole Go source change replaces the fixed `/tmp/deckyfileserver.log` path
|
||||
with `filepath.Join(os.Getenv("DECKY_PLUGIN_LOG_DIR"), "deckyfileserver.log")`,
|
||||
falling back to `os.TempDir()` when the environment variable is empty. This keeps
|
||||
managed and private test logs separate. The upstream HTTPS certificate/key are
|
||||
public embedded test material, unchanged; this is not a trusted TLS certificate.
|
||||
|
||||
Reproduce offline with Go **1.25.7** and Python 3.12+:
|
||||
|
||||
```sh
|
||||
python3 scripts/build-framey-fileserver.py --go /path/to/go1.25.7/bin/go --output /tmp/framey-fileserver-build
|
||||
```
|
||||
|
||||
The script extracts only this source archive, disables module downloads, uses
|
||||
`CGO_ENABLED=0`, `GOOS=linux`, `-mod=vendor`, `-trimpath`, `-buildvcs=false` and
|
||||
`-ldflags='-s -w -buildid='`, and emits deterministic gzip assets (mtime 0).
|
||||
|
||||
| Architecture | Uncompressed helper SHA-256 | Gzip SHA-256 |
|
||||
| --- | --- | --- |
|
||||
| x86_64 | `5e90efd1bf5163bef1686045f4714ad0e4fd0f54edb037fce8cdb06c58177bcb` | `84e49f049bbc475bc553ef52adc05dcd323e36c5f5ec75008558ae87f3a0e6d7` |
|
||||
| aarch64 | `86b80021ebd62ed619eba065757c1122defa5a74b2c045aa44a0c6b4403a508a` | `7ca1b8aebd4e8d2c8ce382ca561e1413ad8e581f85f419a1da777c87d86c4a19` |
|
||||
|
||||
The managed Python adaptation removes only this digest's `_root` flag, handles
|
||||
unavailable hostname DNS,
|
||||
filters omitted flags and applies thumbnail policy before server startup.
|
||||
The server still shares the selected directory on all interfaces using the
|
||||
upstream self-signed HTTPS server; it has no authentication. Select only a
|
||||
directory you intend to share. Uploads remain off by default. Protected system
|
||||
files remain subject to the ordinary Frame user's permissions.
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+27
@@ -0,0 +1,27 @@
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google LLC nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 MoeMagicMango
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
# Decky Localsend 0.4.5 native helper
|
||||
|
||||
The official Decky store ZIP SHA-256 is
|
||||
`e271ccb55eb23f918b99a69f79aec905df4c9b2ef7b23bbf4ec1c2d1a6f0c6b7`.
|
||||
Its Python backend matches upstream plugin tag `v0.45` byte for byte. That
|
||||
tag pins LocalSend-Go commit `ad28b8e9440e17e44b4735a6c4aa0512009cf333`.
|
||||
Building that source with Go 1.25.7, CGO disabled, `-trimpath`,
|
||||
`-buildvcs=false` and `-ldflags='-s -w'` reproduces the published x86_64
|
||||
helper **byte for byte**. The same source produced the ARM64 helper twice
|
||||
with identical hashes.
|
||||
|
||||
| Artifact | SHA-256 |
|
||||
| --- | --- |
|
||||
| Official x86_64 helper | `d448d03861d578685e6c70ea3ace25eb34fca99cb9d4f98bf51ba23aa5cae843` |
|
||||
| Rebuilt ARM64 helper | `e20363237e606ee65eae857792176d21a0bb8ba863489952728eaf2b13277dae` |
|
||||
| `aarch64.gz` | `2c4e24dd00d67c6d4f2125b42f1eedc0fe8bcedcbf804be0c7ae91e7b391d5f9` |
|
||||
| `source.tar.gz` | `f1f86292624b989d540689dbfad503bf15f3937d156978157b8dd418d54415d1` |
|
||||
|
||||
`source.tar.gz` is a Git archive of the exact LocalSend-Go commit, including
|
||||
`go.mod` and `go.sum`. Go modules are **not vendored**: reproducing the
|
||||
build requires access to checksum-verified dependencies or a populated module
|
||||
cache. Run the following to check both expected helper hashes:
|
||||
|
||||
```sh
|
||||
python3 scripts/build-framey-localsend.py \
|
||||
--go /path/to/go1.25.7/bin/go \
|
||||
--output /tmp/framey-localsend-build
|
||||
```
|
||||
The helper shipped to Frame needs no compiler, modules, root service or
|
||||
runtime download. `LICENSE.txt` is the upstream MIT license; `GO-LICENSE.txt`
|
||||
is the Go runtime BSD license. The official plugin ZIP retains its own
|
||||
BSD-3-Clause license unchanged.
|
||||
|
||||
Only the exact official ZIP can use this managed adaptation. The Frame
|
||||
transaction checks its original x86 helper and Python backend hashes,
|
||||
checks the replacement ARM64 SHA-256 and ELF machine locally and again on the
|
||||
headset, then records a managed receipt. Other native archives remain blocked.
|
||||
The official x86 backend and the rebuilt ARM64 helper each passed a single
|
||||
private no-route marker transfer. Published Decky 3.3.0 AMD64/ARM64 runtime
|
||||
checks discovered the plugin and read its stopped status and configuration.
|
||||
The actual managed Decky service on Frame, user-facing transfer, headset UI
|
||||
and worn use remain unverified. See
|
||||
`docs/evidence/framey-localsend-managed-2026-10-27.md` and its linked result.
|
||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in new issue
Block a user