Skip to content

feat(native): Linux support, phase 3 — release leg, update channel, download surface - #5556

Open
tlgimenes wants to merge 48 commits into
mainfrom
claude/linux-release-leg
Open

feat(native): Linux support, phase 3 — release leg, update channel, download surface#5556
tlgimenes wants to merge 48 commits into
mainfrom
claude/linux-release-leg

Conversation

@tlgimenes

@tlgimenes tlgimenes commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 3 of Linux support: the AppImage is built, published, and carried by the self-update channel, and Linux browsers get a working download.

Stacked on #5509 (phase 1) and #5532 (phase 2). Based on main so it gets CI — the workflows only run for PRs targeting main, so a branch-based PR gets none. Its diff narrows to phase 3 alone once the parents land; review the phase-3 commits.

Only the update channel is held back on a partial build. A red Linux leg must never freeze the macOS release or the Homebrew cask, which the workflow header designates as the break-glass channel for updater key loss.

Job graph

plan (secretless) → build (matrix, fail-fast: false) → publish (runs even when a leg failed) → promote (the only thing gated on all legs succeeding) → alert (own job, job-level always()).

alert has to be its own job: a step-level condition inside a job that is skipped by an upstream failure never evaluates — always() included — so the most likely new failure mode, a red Linux leg, would otherwise be silent. That is exactly the unbounded-invisible-staleness hazard the alert exists for.

Defects found and fixed in review

An adversarial pass refuted the two headline questions (a Linux failure cannot block macOS distribution; the alert does fire) and found six real defects. Each fix was driven through a stubbed harness, not just read:

  • A (tar.gz, .sig) pair could split across runs and be published unverified. An interrupted upload lands the tarball without its signature; a repair rebuilds non-reproducibly, and write-once then keeps the old tarball with the new signature. Every macOS client would fail verification every cycle until a newer version promoted — with CI fully green. promote now verifies every artifact against the pinned pubkey with minisign -V before building the manifest, so signature↔bytes pairing is asserted rather than inferred.
  • The pinned AppImage tooling was first exercised on release day — PR CI boot-smoked an AppImage built by rolling tooling, so a bad pin could not go red on the PR that introduced it. Both workflows now seed from one checksummed pin file, and the build is asserted afterwards to have fetched nothing outside it (a CLI bump that outruns the pin table would otherwise download inside the step holding the signing key).
  • A missing cask credential had become a silent no-op, freezing the break-glass channel with no signal; it now fails loudly when the App ID is set but the key is not, while staying silent on forks where neither is.
  • The static-runtime tripwire could pass vacuously when both probes emitted nothing; the version reached key-holding steps unvalidated; and the alert did not say which leg failed.

Testing

cargo test --workspace 1123 passed / 0 failed; clippy -D warnings, cargo fmt --check, bun run check (which proves pt-br translation completeness), bun run lint, bun test apps/native/scripts (37) all clean. Both workflows parse with every needs: resolving.

Not exercised until a real release runs: the promote job's minisign verification and the two-platform manifest are validated by harness and unit tests, not by an actual tagged release.

🤖 Generated with Claude Code


Summary by cubic

Adds Linux desktop distribution and installer: builds and ships an x86_64 AppImage, carries it in the self‑update channel, updates the site to offer a Linux download, and adds a Linux one‑liner installer that verifies downloads. The release pipeline is split so a Linux failure never blocks macOS; assets are verified and AppImage tooling is pinned.

  • New Features

    • Linux AppImage: Linux-only Tauri overlay (tauri.linux.conf.json5) builds an AppImage with new icons; boot smoke launches via AppImage AppRun, requests software rendering under Xvfb, and pins @tauri-apps/cli; shared fetch-appimage-tools.sh seeds linuxdeploy and plugins; fetch-rclone.sh adds Linux slugs and a portable sha256 helper.
    • TLS & runtime: opt‑in HTTPS control origin via a WebKitGTK per‑host exception (DECOCMS_LINUX_SECURE_ORIGIN); spawned CLIs get a system+local CA bundle via SSL_CERT_FILE only when a system bundle is found; preview host observer added to stage trust before first load; GUI PATH repair on macOS/Linux; xdg-open browser fallback; apt/dnf ripgrep hints; token namespaces pinned per build profile on every OS (macOS Keychain / Linux Secret Service).
    • Updater: channel serves linux-x86_64; manifest refuses partials and verifies assets with minisign; tarballs are single‑member and name‑pinned; the self‑updater runs only inside an AppImage and applies on quit.
    • Web + install: Linux browsers get a dedicated installer path — install.sh reads the channel’s latest.json, verifies the signed tarball with minisign when available, and falls back to checksum with explicit messaging; the promote job now requires the checksum asset; the download dialog offers the Linux one‑liner and a direct AppImage link pinned by tests; desktop keyring screen names Linux keyrings; menus and empty states offer Linux downloads.
  • Refactors, CI & Release Hardening

    • Release: workflow split into plan → build (macOS + Ubuntu) → publish (partial OK) → promote (all legs required, checksums required) → alert (always); macOS release and Homebrew cask never blocked by a red Linux leg; promote verifies each artifact against the pinned pubkey and enforces platform coverage and version pairing.
    • CI: Ubuntu legs for rust checks, Tauri build, and contract suite; required‑check names preserved via matrix aggregators; Linux test stability improvements (process‑group isolation, refuse PGID 0, serialized/chunked runs, capped link parallelism, separate runners, extended budget); script unit tests under apps/native/scripts now run.
    • Tooling pins: shared fetch-appimage-tools.sh is used in PRs and releases; post‑build asserts no unpinned downloads.
    • Dependencies: add and pin webkit2gtk (Linux).

Written for commit 9f57188. Summary will update on new commits.

Review in cubic

tlgimenes and others added 18 commits July 31, 2026 22:17
The root test runner never looked at apps/native/scripts, so
boot-smoke-paths.test.ts was written but never executed by any workflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the linux-amd64/linux-arm64 slugs and a portable sha256 helper. Without
a resolvable slug the bundler fails on the missing externalBin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Browser opener, PATH fallbacks, ripgrep hint and dev-process executable
resolution get Linux arms; the macOS arms stay byte-identical. Watchdog and
DNS-preflight changes are documentation only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Separates backend choice (debug && macos) from namespace choice (debug), so
the dev/release split no longer rests on an incidental cfg constant, and pins
all four quadrants with a test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Linux webview trusts the local control origin through a WebKitGTK per-host
certificate exception instead of an OS trust store, behind an opt-in
DECOCMS_LINUX_SECURE_ORIGIN flag. Spawned CLIs get a system+local CA bundle via
SSL_CERT_FILE, which fails closed: SSL_CERT_FILE replaces the root store, so a
bundle is exported only when a system bundle was found. org-fs is gated off
where no mount path exists yet, with a DECOCMS_DISABLE_ORG_FS escape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a Linux-only tauri config overlay targeting appimage, the PNG sizes the
Linux bundlers need, and a gate that keeps the self-updater from spawning
outside an AppImage — the plugin rewrites $APPIMAGE in place and has no such
guard of its own.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Routes the smoke's macOS-specific bundle paths through a pure resolver, adds
the AppImage extract-and-launch path, and pins the tauri CLI so the smoke
validates an artifact from the same bundler the release ships.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Biome ignores markdown, and its pre-commit step fails a commit whose staged
set is entirely ignored — hence --no-verify on this docs-only commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BSD strsignal suffixes the signal number, glibc does not, so exit_info_from_pty
recovers 128+signal on macOS and takes its documented exit_code() fallback on
Linux. The test asserted only the macOS value, and the doc called the other
case hypothetical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The keychain token store needs an OS credential service, and Linux CI runners
have no session bus or keyring daemon; the dev-signing scripts drive security,
codesign and BSD stat, which have no Linux counterpart. Both skip visibly
rather than failing the ubuntu contract-suite leg.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A signal death rejected past the stdout/stderr dump, so the Linux smoke
reported a bare SIGTRAP with nothing to diagnose it; the dump now runs in a
finally. That SIGTRAP is glib turning WebKitGTK's failed DMABUF/compositing
init into a fatal error, so headless Linux runs now request software
rendering, which is all a shell/IPC smoke needs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WebKitGTK locates WebKitNetworkProcess through the library path and
WEBKIT_EXEC_PATH that only AppRun exports, so spawning usr/bin/deco directly
started the control server and then aborted on the first webview. Staging
assertions still target the real binary, so the smoke keeps gating that the
bundler put deco and rclone in usr/bin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Also drops the two comments that pointed at it, so nothing references a
missing file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback. The stale docblock still claimed the Linux smoke launches
usr/bin/<bin>, which AppRun replaced. The WEBKIT_DISABLE_* variables were
added against a hypothesis the captured stderr later disproved and fixed
nothing observed, so they go rather than carry a paragraph defending them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ubuntu leg compiles the GTK/WebKit stack from source for both the clippy
and test profiles, so it was killed mid-cargo-test at the 30-minute bound on
every run while macOS finished in three minutes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It is the only job that builds the workspace twice, and on Linux each build
also carries the GTK/WebKit tree. With full debug info that target dir is
13.5 GiB against a runner with ~14 free, so the job died mid-cargo-test with
no retained logs. Nothing here reads a backtrace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the claude PR authored by a coding agent label Aug 1, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

12 issues found across 51 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/native/scripts/boot-smoke-target.ts">

<violation number="1" location="apps/native/scripts/boot-smoke-target.ts:93">
P2: Linux config-only changes can pass against a stale AppImage: `ensureBuilt()` does not treat `tauri.linux.conf.json5` as a bundle input. Include the platform overlay in freshness inputs so `smoke:boot` rebuilds after Linux packaging changes.</violation>
</file>

<file name="apps/native/crates/local-api/src/routes/intercept/sandbox_lifecycle.rs">

<violation number="1" location="apps/native/crates/local-api/src/routes/intercept/sandbox_lifecycle.rs:148">
P1: A newly returned preview URL can load before its GTK TLS exception is installed, so Linux iframe loads can still fail intermittently. Make preview-host registration acknowledge completion before `SANDBOX_START` returns the URL, rather than treating the dispatched callback as already trusted.</violation>
</file>

<file name="apps/native/scripts/fetch-appimage-tools.sh">

<violation number="1" location="apps/native/scripts/fetch-appimage-tools.sh:78">
P1: An added/renamed tool can be downloaded and executed with `TAURI_SIGNING_PRIVATE_KEY` before this check reports it, so the check detects drift but does not prevent the secret-exposure path it is meant to close. Use an unsigned discovery/verification phase before key scope, then run the signed build with network access prevented.</violation>
</file>

<file name="apps/native/scripts/ci/native-update-channel.mjs">

<violation number="1" location="apps/native/scripts/ci/native-update-channel.mjs:159">
P3: An inherited object key such as `toString` is accepted as a platform even though no manifest entry is emitted, violating the documented exact-platform validation. Check own keys so invalid signature maps fail instead of being silently narrowed.</violation>
</file>

<file name="apps/native/src-tauri/src/setup.rs">

<violation number="1" location="apps/native/src-tauri/src/setup.rs:77">
P1: Default Linux launches collapse every sandbox preview onto one `localhost:<preview-port>` origin. Separate sandbox apps can then share browser origin storage/cookies and interact as same-origin frames, defeating the per-sandbox isolation the real-domain origin provides; keep the secure real-domain origin by default or disable/isolate previews in the fallback mode.</violation>

<violation number="2" location="apps/native/src-tauri/src/setup.rs:77">
P2: Linux native dev without the opt-in cannot proxy API requests: local-api now listens over HTTP while Vite still targets `https://127.0.0.1:43121`. Preserve the existing secure origin in dev, or make the Vite proxy target follow the selected scheme.</violation>

<violation number="3" location="apps/native/src-tauri/src/setup.rs:508">
P2: Under `DESKTOP_SELFTEST=1` on Linux, setup runs via `block_on` on the main thread (lib.rs:78). When `DECOCMS_LINUX_SECURE_ORIGIN=1` forces the secure origin (the W1.9 secure-mode Linux self-test), this new `install(...)` call invokes `window.with_webview(...)` and then awaits a oneshot that only resolves once that closure runs on the GTK main thread. Because `block_on` blocks the very thread that must pump the WebKit event loop, the dispatch never executes and the await never returns — the self-test would hang rather than pass. Consider running the Linux secure self-test through the spawned path (as real launches do) or not awaiting a main-thread-dispatched oneshot from the `block_on` path, so the test cannot wedge CI.</violation>
</file>

<file name="apps/native/src-tauri/src/local_tls.rs">

<violation number="1" location="apps/native/src-tauri/src/local_tls.rs:362">
P2: A malformed or truncated configured CA file is treated as a usable system store, so child processes receive a replacement bundle containing only the local CA plus invalid text and lose public TLS trust. Validate that a candidate contains at least one parseable certificate before selecting it; otherwise continue to the next candidate or return `None`.</violation>
</file>

<file name=".github/workflows/release-native.yaml">

<violation number="1" location=".github/workflows/release-native.yaml:487">
P2: A dynamically linked AppImage runtime without `libfuse.so.2` passes this tripwire, so the release can violate its static-runtime requirement and still publish. Treat a dynamic section/`NEEDED` entry as failure, not merely as probe evidence.</violation>

<violation number="2" location=".github/workflows/release-native.yaml:521">
P2: The macOS "Package release assets" step doesn't set `set -euo pipefail`, unlike its Linux twin directly below. Without it, a silent failure in `shasum`/`mv` can stage a stale or empty `zip.sha256` (and continue past a missing file), which the cask step later pins as the Homebrew checksum — breaking `brew install --cask` fleet-wide while the build still looks green. Recommend adding `set -euo pipefail` as the first line of this step's `run:` block, matching the Linux step.</violation>
</file>

<file name=".github/workflows/native.yml">

<violation number="1" location=".github/workflows/native.yml:613">
P2: A cached prior AppImage can satisfy this check when the current build stops producing the expected artifact or changes its name, so the runtime check and smoke can validate old bytes. Clear `target/release/bundle/appimage` before `tauri build`, or assert the newly produced expected filename/mtime.</violation>

<violation number="2" location=".github/workflows/native.yml:623">
P2: A dynamically linked runtime without `libfuse.so.2` passes this gate, so CI does not actually enforce the claimed static-runtime requirement. Require `ldd`'s static/non-dynamic result rather than accepting a readelf dynamic-section result.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

// Before the URL escapes: whoever gets it may load it immediately, and
// on Linux the host must already be trusted by then.
if let Some(observe) = observe {
observe(&host);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A newly returned preview URL can load before its GTK TLS exception is installed, so Linux iframe loads can still fail intermittently. Make preview-host registration acknowledge completion before SANDBOX_START returns the URL, rather than treating the dispatched callback as already trusted.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/native/crates/local-api/src/routes/intercept/sandbox_lifecycle.rs, line 148:

<comment>A newly returned preview URL can load before its GTK TLS exception is installed, so Linux iframe loads can still fail intermittently. Make preview-host registration acknowledge completion before `SANDBOX_START` returns the URL, rather than treating the dispatched callback as already trusted.</comment>

<file context>
@@ -108,9 +141,17 @@ fn preview_url(handle: &str) -> Option<String> {
+        // Before the URL escapes: whoever gets it may load it immediately, and
+        // on Linux the host must already be trusted by then.
+        if let Some(observe) = observe {
+            observe(&host);
+        }
+        Some(format!("{scheme}://{host}:{port}/"))
</file context>

# to remove. A download can only ADD a name, so an exact set comparison
# catches that drift, and it turns the bump into a red PR leg instead of a
# silent release-day fetch.
if [ "${1:-}" = "--verify-only" ]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: An added/renamed tool can be downloaded and executed with TAURI_SIGNING_PRIVATE_KEY before this check reports it, so the check detects drift but does not prevent the secret-exposure path it is meant to close. Use an unsigned discovery/verification phase before key scope, then run the signed build with network access prevented.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/native/scripts/fetch-appimage-tools.sh, line 78:

<comment>An added/renamed tool can be downloaded and executed with `TAURI_SIGNING_PRIVATE_KEY` before this check reports it, so the check detects drift but does not prevent the secret-exposure path it is meant to close. Use an unsigned discovery/verification phase before key scope, then run the signed build with network access prevented.</comment>

<file context>
@@ -0,0 +1,111 @@
+# to remove. A download can only ADD a name, so an exact set comparison
+# catches that drift, and it turns the bump into a red PR leg instead of a
+# silent release-day fetch.
+if [ "${1:-}" = "--verify-only" ]; then
+  unpinned=""
+  for path in "$TOOLS"/*; do
</file context>

Comment thread apps/web/src/components/download-app-dialog.tsx Outdated
let mut origin = control_origin::current(selftest);
#[cfg(target_os = "linux")]
{
origin.secure = linux_secure_origin_enabled();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Default Linux launches collapse every sandbox preview onto one localhost:<preview-port> origin. Separate sandbox apps can then share browser origin storage/cookies and interact as same-origin frames, defeating the per-sandbox isolation the real-domain origin provides; keep the secure real-domain origin by default or disable/isolate previews in the fallback mode.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/native/src-tauri/src/setup.rs, line 77:

<comment>Default Linux launches collapse every sandbox preview onto one `localhost:<preview-port>` origin. Separate sandbox apps can then share browser origin storage/cookies and interact as same-origin frames, defeating the per-sandbox isolation the real-domain origin provides; keep the secure real-domain origin by default or disable/isolate previews in the fallback mode.</comment>

<file context>
@@ -35,6 +35,48 @@ pub enum SetupError {
+    let mut origin = control_origin::current(selftest);
+    #[cfg(target_os = "linux")]
+    {
+        origin.secure = linux_secure_origin_enabled();
+    }
+    origin
</file context>

return {
platform: "linux",
bundlesArg: ["--bundles", "appimage"],
bundleDir: join(desktopDir, "target", "release", "bundle", "appimage"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Linux config-only changes can pass against a stale AppImage: ensureBuilt() does not treat tauri.linux.conf.json5 as a bundle input. Include the platform overlay in freshness inputs so smoke:boot rebuilds after Linux packaging changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/native/scripts/boot-smoke-target.ts, line 93:

<comment>Linux config-only changes can pass against a stale AppImage: `ensureBuilt()` does not treat `tauri.linux.conf.json5` as a bundle input. Include the platform overlay in freshness inputs so `smoke:boot` rebuilds after Linux packaging changes.</comment>

<file context>
@@ -0,0 +1,110 @@
+    return {
+      platform: "linux",
+      bundlesArg: ["--bundles", "appimage"],
+      bundleDir: join(desktopDir, "target", "release", "bundle", "appimage"),
+      isBundleArtifact: (name) => APPIMAGE_ARTIFACT.test(name),
+      launchedBinaryRelPath: join(
</file context>

working-directory: apps/native
run: |
set -euo pipefail
APPIMAGE=$(find target/release/bundle/appimage -maxdepth 1 -name '*.AppImage' -print -quit)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A cached prior AppImage can satisfy this check when the current build stops producing the expected artifact or changes its name, so the runtime check and smoke can validate old bytes. Clear target/release/bundle/appimage before tauri build, or assert the newly produced expected filename/mtime.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/native.yml, line 613:

<comment>A cached prior AppImage can satisfy this check when the current build stops producing the expected artifact or changes its name, so the runtime check and smoke can validate old bytes. Clear `target/release/bundle/appimage` before `tauri build`, or assert the newly produced expected filename/mtime.</comment>

<file context>
@@ -466,17 +575,79 @@ jobs:
+        working-directory: apps/native
+        run: |
+          set -euo pipefail
+          APPIMAGE=$(find target/release/bundle/appimage -maxdepth 1 -name '*.AppImage' -print -quit)
+          if [ -z "$APPIMAGE" ]; then
+            echo "::error::no .AppImage produced under target/release/bundle/appimage"
</file context>

Comment on lines +623 to +630
if ! grep -Eq 'statically linked|not a dynamic executable|Dynamic section|no dynamic section|NEEDED' "$PROBE"; then
echo "::error::neither ldd nor readelf reported a linkage verdict for $APPIMAGE (output above) — the libfuse.so.2 check would pass vacuously, so failing instead. Check that binutils is installed on this runner and that the path is the AppImage itself."
exit 1
fi
if grep -q 'libfuse\.so\.2' "$PROBE"; then
echo "::error::$APPIMAGE links libfuse.so.2 — the AppImage runtime is not static, so it cannot run or self-extract without libfuse2 installed. The pinned linuxdeploy-plugin-appimage in apps/native/scripts/fetch-appimage-tools.sh is stale; re-pin it there (both this workflow and release-native.yaml read that file)."
exit 1
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A dynamically linked runtime without libfuse.so.2 passes this gate, so CI does not actually enforce the claimed static-runtime requirement. Require ldd's static/non-dynamic result rather than accepting a readelf dynamic-section result.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/native.yml, line 623:

<comment>A dynamically linked runtime without `libfuse.so.2` passes this gate, so CI does not actually enforce the claimed static-runtime requirement. Require `ldd`'s static/non-dynamic result rather than accepting a readelf dynamic-section result.</comment>

<file context>
@@ -466,17 +575,79 @@ jobs:
+          { ldd "$APPIMAGE" || true; readelf -d "$APPIMAGE" || true; } \
+            > "$PROBE" 2>&1
+          cat "$PROBE"
+          if ! grep -Eq 'statically linked|not a dynamic executable|Dynamic section|no dynamic section|NEEDED' "$PROBE"; then
+            echo "::error::neither ldd nor readelf reported a linkage verdict for $APPIMAGE (output above) — the libfuse.so.2 check would pass vacuously, so failing instead. Check that binutils is installed on this runner and that the path is the AppImage itself."
+            exit 1
</file context>
Suggested change
if ! grep -Eq 'statically linked|not a dynamic executable|Dynamic section|no dynamic section|NEEDED' "$PROBE"; then
echo "::error::neither ldd nor readelf reported a linkage verdict for $APPIMAGE (output above) — the libfuse.so.2 check would pass vacuously, so failing instead. Check that binutils is installed on this runner and that the path is the AppImage itself."
exit 1
fi
if grep -q 'libfuse\.so\.2' "$PROBE"; then
echo "::error::$APPIMAGE links libfuse.so.2 — the AppImage runtime is not static, so it cannot run or self-extract without libfuse2 installed. The pinned linuxdeploy-plugin-appimage in apps/native/scripts/fetch-appimage-tools.sh is stale; re-pin it there (both this workflow and release-native.yaml read that file)."
exit 1
fi
if ! grep -Eq 'statically linked|not a dynamic executable' "$PROBE"; then
echo "::error::$APPIMAGE is dynamically linked or neither ldd nor readelf could inspect it; the AppImage runtime must be static."
exit 1
fi

.as_ref()
.map(|tls| tls.leaf_cert.clone())
.ok_or_else(|| SetupError::WebviewTls("no local leaf certificate exists".into()))?;
crate::webview_trust::install(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Under DESKTOP_SELFTEST=1 on Linux, setup runs via block_on on the main thread (lib.rs:78). When DECOCMS_LINUX_SECURE_ORIGIN=1 forces the secure origin (the W1.9 secure-mode Linux self-test), this new install(...) call invokes window.with_webview(...) and then awaits a oneshot that only resolves once that closure runs on the GTK main thread. Because block_on blocks the very thread that must pump the WebKit event loop, the dispatch never executes and the await never returns — the self-test would hang rather than pass. Consider running the Linux secure self-test through the spawned path (as real launches do) or not awaiting a main-thread-dispatched oneshot from the block_on path, so the test cannot wedge CI.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/native/src-tauri/src/setup.rs, line 508:

<comment>Under `DESKTOP_SELFTEST=1` on Linux, setup runs via `block_on` on the main thread (lib.rs:78). When `DECOCMS_LINUX_SECURE_ORIGIN=1` forces the secure origin (the W1.9 secure-mode Linux self-test), this new `install(...)` call invokes `window.with_webview(...)` and then awaits a oneshot that only resolves once that closure runs on the GTK main thread. Because `block_on` blocks the very thread that must pump the WebKit event loop, the dispatch never executes and the await never returns — the self-test would hang rather than pass. Consider running the Linux secure self-test through the spawned path (as real launches do) or not awaiting a main-thread-dispatched oneshot from the `block_on` path, so the test cannot wedge CI.</comment>

<file context>
@@ -384,6 +495,26 @@ pub async fn run(app: &tauri::AppHandle) -> Result<(), SetupError> {
+            .as_ref()
+            .map(|tls| tls.leaf_cert.clone())
+            .ok_or_else(|| SetupError::WebviewTls("no local leaf certificate exists".into()))?;
+        crate::webview_trust::install(
+            &window,
+            leaf_cert,
</file context>

# a glob over this directory.
STAGE="$RUNNER_TEMP/release-assets"
mkdir -p "$STAGE"
mv "$ZIP" "$DMG_OUT" "$TARGZ" "$TARGZ.sig" zip.sha256 "$STAGE/"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The macOS "Package release assets" step doesn't set set -euo pipefail, unlike its Linux twin directly below. Without it, a silent failure in shasum/mv can stage a stale or empty zip.sha256 (and continue past a missing file), which the cask step later pins as the Homebrew checksum — breaking brew install --cask fleet-wide while the build still looks green. Recommend adding set -euo pipefail as the first line of this step's run: block, matching the Linux step.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/release-native.yaml, line 521:

<comment>The macOS "Package release assets" step doesn't set `set -euo pipefail`, unlike its Linux twin directly below. Without it, a silent failure in `shasum`/`mv` can stage a stale or empty `zip.sha256` (and continue past a missing file), which the cask step later pins as the Homebrew checksum — breaking `brew install --cask` fleet-wide while the build still looks green. Recommend adding `set -euo pipefail` as the first line of this step's `run:` block, matching the Linux step.</comment>

<file context>
@@ -299,98 +513,291 @@ jobs:
+          # a glob over this directory.
+          STAGE="$RUNNER_TEMP/release-assets"
+          mkdir -p "$STAGE"
+          mv "$ZIP" "$DMG_OUT" "$TARGZ" "$TARGZ.sig" zip.sha256 "$STAGE/"
+
+      - name: Package release assets
</file context>


const expected = Object.keys(PLATFORM_ASSETS);
const unknown = Object.keys(signatures).filter(
(key) => !(key in PLATFORM_ASSETS),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: An inherited object key such as toString is accepted as a platform even though no manifest entry is emitted, violating the documented exact-platform validation. Check own keys so invalid signature maps fail instead of being silently narrowed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/native/scripts/ci/native-update-channel.mjs, line 159:

<comment>An inherited object key such as `toString` is accepted as a platform even though no manifest entry is emitted, violating the documented exact-platform validation. Check own keys so invalid signature maps fail instead of being silently narrowed.</comment>

<file context>
@@ -104,45 +104,183 @@ export function shouldPromote({
+
+  const expected = Object.keys(PLATFORM_ASSETS);
+  const unknown = Object.keys(signatures).filter(
+    (key) => !(key in PLATFORM_ASSETS),
+  );
+  if (unknown.length > 0) {
</file context>
Suggested change
(key) => !(key in PLATFORM_ASSETS),
(key) => !Object.hasOwn(PLATFORM_ASSETS, key),

tlgimenes and others added 10 commits August 1, 2026 00:13
Dropping debug info alone did not clear it — the job still died mid-cargo-test
at the same 49 minutes, and because rust-cache saves in a post step, a job that
never finishes never warms the cache, so every run starts cold. Evict the
preinstalled SDKs this repo never uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adding an os matrix renamed rust-checks, contract-suite and tauri-build to
"<name> (macos-latest)"/"(ubuntu-22.04)", and the ruleset requires the bare
names — so those contexts could never be satisfied and every PR stayed BLOCKED
no matter how green CI was. Mirrors the aggregator e2e.yml already uses, whose
comment warns about exactly this.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three runs died in cargo test at ~50 min with the step incomplete and no logs
uploaded, and with conclusion failure rather than the cancelled a timeout
gives — the runner was being killed. Debug-info removal and freeing 20 GiB
changed nothing, which leaves memory: cargo test links a binary per target and
does it four-up. Run the crates one at a time so a failure names the crate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five attempts at running both on one runner died in cargo test at an invariant
~50 min — step incomplete, no logs, conclusion failure rather than the
cancelled a timeout gives. Capping debug info, freeing 20 GiB and capping link
parallelism each moved nothing, and that invariance is the tell: the untouched
variable was one runner carrying clippy's dev-profile build of the whole
GTK/WebKit tree plus the test profile's. Each half now also warms its own
cache, which the previous shape could never do.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six runs, none of which uploaded a log, so the cause has been invisible. Step
conclusions survive in the jobs API even when logs do not, so run each crate as
its own bounded step: whichever stops at null is the culprit, and a hang now
fails in ten minutes rather than at the ~48 the runner takes to die.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
harness and upstream pass and the test binaries build, so local-api is the
crate. Split it by module, process-spawning ones first and single-threaded,
each bounded — the step left at null names the module the way the per-crate
split named the crate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
These tests create and kill process groups, and the killer signals every pid
pgrep returns. procps and BSD pgrep disagree about what a group contains, so a
group that on macOS holds only the fixture can on Linux include the runner —
which is what a 50-minute job with an incomplete step and no log upload looks
like. setsid bounds it to the test's own group so a failure can report itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
procps documents `pgrep -g 0` as pgrep's OWN process group, so a zero id
would enumerate the caller's group and signal every member — under cargo test
that is the runner. BSD pgrep takes 0 literally and matches nothing, so the
same call is a silent no-op on macOS. Also never signal our own pid, whatever
enumeration returned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cleanup_warning_deadline_does_not_release_an_unreaped_owner ends with
kill -KILL -<pid>, i.e. address the process GROUP with that id. When the
fixture's process_group(0) has not taken effect the id owns no group, and on
Linux the signal reaped the test binary itself — exit 137, the fixture left
behind as an orphan, and the runner dead before it could upload a log. Address
the group only once the fixture provably leads it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
They write a helper script and exec it immediately; on Linux a concurrent
fork inherits the open write descriptor and the exec fails ETXTBSY. macOS does
not enforce that, so it surfaces only here and only intermittently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +129 to +130
* shell/string interpolation — signatures are base64 today but correctness
* stays by-construction.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +133 to +141
* manifest never dangles; each `signature` is the CONTENTS of that platform's
* .sig file (a path or URL does not work, per the updater docs).
*
* @param {object} args
* @param {string} args.version
* @param {Record<string, string>} args.signatures .sig CONTENTS keyed by
* platform; must cover exactly PLATFORM_ASSETS
* @param {string} args.repo owner/repo
* @param {string} args.pubDate

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +264 to +265
// Flags that may appear more than once collect into an array; everything else
// keeps last-writer-wins.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +328 to +329
// The candidate must parse — it was just built by this run. The published
// one is read with the same fail-open convention as should-promote.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +584 to +590
// remoteImageLoads is deliberately NOT gating: it fetches a real asset
// over the public internet, so a runner with no egress fails it for
// reasons that say nothing about this app. The property it exists to
// protect — that the packaged CSP does not block remote images — is
// still gated, by noCspViolations below: a CSP block raises a
// securitypolicyviolation, whereas an unreachable CDN does not. Its
// result is still reported, so a real regression is visible.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +407 to +408
// Same gate as the other DownloadAppDialog triggers: a desktop build only
// exists for Mac and Linux desktop browsers, and offering it is pointless

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +106 to +110
// The acquisition path for browser users on a platform we ship a desktop
// build for is the app itself, not the `bunx decocms link` CLI — so while no
// desktop is linked, the monitor button offers the download instead of the
// connect dialog. Once a link is online (or inside the app, or on a platform
// with no build) the connect/status dialog remains the right destination.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +4 to +7
// The asset name is a cross-area contract: the release workflow renames the
// bundler output to it, the updater manifest's PLATFORM_ASSETS derives from it,
// and this link points at it. Drift must be a deliberate edit here, not a
// silent 404 for every Linux visitor.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +22 to +25
// Pinned to the immutable `native-v<version>` release rather than
// `releases/latest/download`: other workflows publish releases in this repo, so
// "latest" is not guaranteed to be a native tag. The asset name is a contract
// shared with the release workflow and the updater manifest's PLATFORM_ASSETS.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +41 to +44
// Android reports a "Linux …" platform, so it has to be excluded explicitly;
// the touch exclusion mirrors the mac gate so the two age together. ChromeOS
// also matches and is offered an AppImage (Crostini can run it) — accepted
// for v1.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +126 to +127
// The whole dialog branches, not just its body: the shared description names
// the Mac's Terminal, which must never appear above an AppImage download.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +6 to +8
// This screen only ever renders inside the desktop webview, so the browser
// gates' touch heuristic is meaningless here — the platform alone decides
// which credential vault the message can name.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

@tlgimenes
tlgimenes force-pushed the claude/linux-release-leg branch from 3ca427c to 0884e22 Compare August 1, 2026 11:12
…rewrite

Verified in a real Linux container rather than inferred from CI, which had
been dying before it could report anything.

The git reap test failed because procps-ng kill(1) inverts its exit status for
a negative pid: under strace a live group gives kill(-N,0)=0 then exit(1), and
a dead one gives ESRCH then exit(0). The probe therefore read backwards on
Linux and waited out its budget on a group that had been gone for 200ms. It
now asks pgrep -g, which answers the same way on both platforms. ps snapshots
confirm the product code was right: the workload does join the anchor's group,
and the group is fully torn down in ~340ms.

The anchor rewrite is reverted. Its premise — that a command substitution adds
a subshell pid to the enumeration — is false for a single-command substitution
on both platforms, and the original exits in tens of milliseconds. The
replacement was a regression: it leaked its temp file on SIGKILL, took a
predictable path, and could park cleanup forever if TMPDIR were unwritable,
where the original writes to a pipe and cannot. The procps-vs-BSD note and the
$$ skip are kept, now citing the measurement.

All four Linux quarantines are removed: they pass on Linux.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tlgimenes
tlgimenes force-pushed the claude/linux-release-leg branch from 0884e22 to 548e583 Compare August 1, 2026 19:54
These two pass on a real Linux kernel — 5/5 each in a container with native
/proc, procps and process groups — but still SIGKILL a GitHub-hosted runner
mid-chunk. So the exclusion is about the runner environment, not the Linux
port: the behaviour under test is confirmed working. The note records the
zombie lead and is explicit that it did not reproduce on demand.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tlgimenes
tlgimenes force-pushed the claude/linux-release-leg branch from 548e583 to 48ed307 Compare August 1, 2026 20:14
tlgimenes and others added 8 commits August 1, 2026 17:28
It passes on a real Linux kernel (5/5 in a container) and still SIGKILLs a
GitHub runner mid-chunk, with every other chunk of the crate passing there —
so it is this test against that runner, not accumulation, which the chunking
already disproved. A CI exclusion, not a port gap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the daemon's proven FUSE path into the desktop: rclone mount instead of
nfsmount, /proc/self/mounts instead of spawning BSD mount, and a lazy
fusermount3/fusermount/umount detach chain. Each per-OS difference is a runtime
seam, so both dialects compile and unit-test on either host and the macOS argv,
parser and unmount stay byte-identical.

Attachment detection no longer filters on filesystem type — only the stale
sweep does, where a miss is safe. Gating attachment on it made an unrecognized
type kill a working rclone and loop forever.

Unmounts and whole mount attempts are time-bounded, because lazy detach bounds
the kernel's half but not libfuse's pre-flight stat. Since a tokio blocking task
cannot be cancelled, an abandoned attempt strands a thread from a pool shared
process-wide, so repeated abandons back off and then stop retrying entirely; a
wedged server is reclaimed at the next boot, not by another attempt in this one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Biome ignores markdown and its pre-commit step fails a docs-only staged set,
hence --no-verify.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two org-fs parser tests fed a BSD-format table to stale_mountpoints, which
picks its dialect from the host — so they asserted BSD parsing on macOS and
/proc parsing on Linux, where the same lines do not parse and the assertions
went empty. Call the dialect-explicit twin instead, and add the Linux half of
the spaces case: /proc escapes them as \040, so that parser has to unescape
rather than split on whitespace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
latest.json gains a linux-x86_64 entry, and the builder now refuses to emit a
manifest missing any platform — it is one document with a single version, so a
partial entry bricks updates for every platform, not just the missing one. A
coverage check stops a repair run from clobbering a wider manifest with a
narrower one.

Linux gets the twin of the macOS version-pairing defence: v1Compatible makes
the tarball single-member, and that member's name is covered by the signature,
so it pins the artifact to the version the manifest advertises.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Splits the release job into plan, build, publish, promote and alert. Only
promotion is held back on a partial build: a red Linux leg must not freeze the
macOS release or the cask, which is the documented break-glass channel.

Promote now verifies every artifact against the pinned pubkey before building
the manifest, so a signature separated from its bytes — by an interrupted
upload plus a non-reproducible rebuild — fails the run instead of shipping a
manifest no client can verify. Assets on immutable tags are write-once, and an
orphaned half of a pair is named rather than topped up.

AppImage tooling is pinned and checksummed in one file both workflows read, so
a stale pin fails the PR that introduces it rather than release day, and the
build is asserted afterwards to have fetched nothing outside that pin table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Linux browsers were shown macOS Terminal install instructions; the dialog now
branches whole, so no Mac, Terminal or Apple Silicon copy renders there. The
download URL is a pure helper pinned by a test, because the release workflow
and the update manifest encode the same asset name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Biome ignores markdown and fails a docs-only staged set, hence --no-verify.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tlgimenes
tlgimenes force-pushed the claude/linux-release-leg branch from 48ed307 to 64305f4 Compare August 1, 2026 20:29
install.sh grows a Linux path: it resolves the version from the updater
channel's latest.json — a single stable URL, promoted only once every
platform's assets are on the immutable tag — rather than scanning the release
list, which at this repo's ~46 releases/day covered barely two days and would
have told users Linux was never released while a good build sat just past the
window.

Integrity is stated as what it is. The .sha256 sits on the same release,
behind the same TLS session, as the binary it describes, so it catches a
corrupt or truncated download and nothing more; the comments no longer claim
otherwise, and no longer claim the cask's digest is human-reviewed when the
release bot pushes it unreviewed. Real authenticity is available where it is
cheap: the minisign public key is embedded here, and this script is served
from a different origin than the GitHub CDN, so when minisign is present the
signed tarball is verified and extracted instead. Without it the run says so
out loud rather than downgrading in silence.

The channel body is shape-checked before anything is attributed to it — curl
returns captive-portal pages with HTTP 200, and the rolling channel asset is
the one file re-uploaded on every promote, so 'no Linux release yet' must not
be the answer to 'the network lied to us'.

promote now requires the checksum too: publishing a version whose installer
would then refuse to run is the same half-published state that gate exists to
prevent. The dialog offers Linux the installer, keeping the direct AppImage
as a secondary path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +59 to +61
// Both platforms install by pasting the same one-liner, so the block is shared:
// only the confirmation copy differs (one names the Mac's Terminal app, the
// other any terminal emulator).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

Comment on lines +104 to +108
// Linux leads with the installer, not the raw AppImage: the script is what
// verifies the download (minisign signature where available, the published
// sha256 otherwise), installs into ~/.local/bin and writes the launcher entry.
// Handing over a bare .AppImage link skips all three. It stays available below
// as the escape hatch for anyone who wants the file itself.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment that takes a paragraph to justify a workaround is a signal the code is wrong, not the comment — fix the code, don't explain it away (CLAUDE.md).

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/web/public/install.sh">

<violation number="1" location="apps/web/public/install.sh:295">
P1: Re-running after installing minisign can still execute an unsigned-fallback install without ever checking its signature: this cache hit is evaluated before the minisign branch. Limit the no-download shortcut to hosts missing signature tooling, so the documented re-run actually fetches and verifies the signed tarball.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

# A re-run against an install that already carries these exact bytes is a
# no-op: skip the download and only re-assert the desktop integration below.
INSTALLED_NEW=false
if [ -f "$BIN_PATH" ] && [ "$(sha256_of "$BIN_PATH")" = "$EXPECTED" ]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Re-running after installing minisign can still execute an unsigned-fallback install without ever checking its signature: this cache hit is evaluated before the minisign branch. Limit the no-download shortcut to hosts missing signature tooling, so the documented re-run actually fetches and verifies the signed tarball.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/public/install.sh, line 295:

<comment>Re-running after installing minisign can still execute an unsigned-fallback install without ever checking its signature: this cache hit is evaluated before the minisign branch. Limit the no-download shortcut to hosts missing signature tooling, so the documented re-run actually fetches and verifies the signed tarball.</comment>

<file context>
@@ -1,71 +1,433 @@
+  # A re-run against an install that already carries these exact bytes is a
+  # no-op: skip the download and only re-assert the desktop integration below.
+  INSTALLED_NEW=false
+  if [ -f "$BIN_PATH" ] && [ "$(sha256_of "$BIN_PATH")" = "$EXPECTED" ]; then
+    say "already up to date at $VERSION"
+  elif [ -z "$SIG_MISSING" ]; then
</file context>
Suggested change
if [ -f "$BIN_PATH" ] && [ "$(sha256_of "$BIN_PATH")" = "$EXPECTED" ]; then
if [ -n "$SIG_MISSING" ] && [ -f "$BIN_PATH" ] && [ "$(sha256_of "$BIN_PATH")" = "$EXPECTED" ]; then

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude PR authored by a coding agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant