feat(linux): bring desktop support up to date - #71
Conversation
Add the versioned Aiden Remote API, secure local and Tailscale pairing, resumable chat streams, and workspace, file, Git, model, schedule, and attachment operations. Extract shared application services and cover authorization, idempotency, replay, revocation, opaque handles, and transport behavior.
Expose pairing, connection status, approved roots, device revocation, and local or Tailscale controls in Electron settings. Introduce Aiden On The Go during onboarding, synchronize appearance tokens, and cover the settings and end-to-end lifecycle.
Add the SwiftUI iPhone and iPad companion with secure pairing, adaptive workspace and chat navigation, resumable SSE streaming, files, Git, scheduled tasks, App Intents, Live Activities, voice input, native Markdown, and Aiden appearance. Include physical-device tests, signing metadata, TestFlight workflows, ASC monitoring, release-policy checks, and required upstream notices.
Record the Aiden Remote API, threat model, phased Aiden On The Go implementation and verification history, TestFlight status, and remaining release work. Remove the obsolete CLAUDE.md instructions while retaining the active migration plan and required attribution history.
Persist bounded per-provider model visibility, apply it across Mac and Telegram selection surfaces, and keep existing hidden selections executable. Normalize custom provider PNG/SVG artwork on the Mac and project only bounded PNG data through the remote model catalog.
Render Mac-normalized custom provider artwork, filter hidden models while preserving current hidden selections, remove the model-row Aiden ghost, and replace the speech status sentence with a reduced-motion-safe animated waveform.
Remove the opaque bottom safe-area pane and reserve scroll-content clearance so chat rows flow beneath the Liquid Glass action while the final row remains reachable.
Advance app, tests, and widget build metadata to 8 while retaining the pre-release 0.1.0 marketing version.
Record the exact VALID build resource, internal tester assignment, external NOT_APPLICABLE state, physical-device verification, and release scope.
update .papercuts/troubleshooting.md as part of the reviewed Aiden On The Go and Remote Access update.
update docs/aiden-remote-api-v1.md as part of the reviewed Aiden On The Go and Remote Access update.
update docs/plans/README.md as part of the reviewed Aiden On The Go and Remote Access update.
remove docs/plans/aiden-remote-multi-instance-hardening-plan.md as part of the reviewed Aiden On The Go and Remote Access update.
update docs/security/aiden-remote-threat-model.md as part of the reviewed Aiden On The Go and Remote Access update.
update docs/testing/aiden-on-the-go/phase-12.md as part of the reviewed Aiden On The Go and Remote Access update.
update docs/testing/aiden-on-the-go/phase-6.md as part of the reviewed Aiden On The Go and Remote Access update.
update ios/ASC_CLI.md as part of the reviewed Aiden On The Go and Remote Access update.
update ios/AidenOnTheGo.xcodeproj/project.pbxproj as part of the reviewed Aiden On The Go and Remote Access update.
update ios/AidenOnTheGo/ContentView.swift as part of the reviewed Aiden On The Go and Remote Access update.
update ios/AidenOnTheGo/Features/Remote/AidenChatFeature.swift as part of the reviewed Aiden On The Go and Remote Access update.
update ios/AidenOnTheGo/Features/Remote/AidenPairingView.swift as part of the reviewed Aiden On The Go and Remote Access update.
There was a problem hiding this comment.
Important
One definite bug in the Linux subagent cleanup path (an unbounded reap loop that can hang the helper), a registration gap that leaves six new policy-contract test suites never executed, and one Linux metadata limitation worth resolving before Fedora is declared fully supported. Details inline.
Reviewed changes
- Linux build, package, and release pipeline — new CI jobs build, install, verify, and smoke the AppImage/deb/rpm artifacts on x64/arm64 Ubuntu plus a Fedora container RPM job; the release workflow gains
linux-releaseand the publisher now requires both architectures' Linux assets before publishing. - Host capability gates — a main-owned
hostPlatformCapabilities()policy (bots, Computer Use, Apple Foundation Models, accessibility paste, dock icon, native share) is projected to the renderer and enforced across IPC registration, chat/bot paths, remote pairing/state, and Telegram bindings so Apple-only surfaces neither advertise nor execute on Linux. - Fail-closed Linux secret storage — a
secure-storagewrapper refuses Electron's reversiblebasic_textbackend with an actionable message, and keyless→keyless provider transitions no longer require a keyring; every formersafeStorageconsumer funnels through the wrapper. - Offline models.dev policy — live-app model reads and provider handlers use only the bundled snapshot; the foreground catalog-refresh action and the device-local cache read path are removed.
- Native helper portability — shared
aiden-platform.h(SHA-256, getrandom, renameat2 shims), Linux xattr and st_mtim identity handling in the file mutator, 7-field run-store tokens, subreaper cleanup, and/bin/shsubagent shells, each with Linux-native adversarial tests. - Platform window, menu, and shortcut behavior — platform-split window options and menu templates, Ctrl-based accelerator projection, the Wayland GlobalShortcuts portal, clipboard-only dictation delivery, Linux editor/file-manager discovery, and background keep-alive for an enabled Remote Access listener.
- Docs and companion copy —
docs/linux.md, the support plan, README/AGENTS updates, and a mechanical "Mac" → "desktop" copy sweep across the iOS/Android companions with matching native test updates.
ℹ️ Live models.dev refresh is removed cross-platform, not only on Linux
main/handlers/providers.ts's providers:updateCatalogs no longer refreshes models.dev, modelsCatalog reads only the bundled snapshot, and Settings → Providers loses the Update model catalogs action on macOS too, since the handlers and UI are shared. The Linux-focused PR description doesn't call out this macOS-facing removal, but AGENTS.md and the new models-dev-live-app-policy.test.ts were updated consistently, so it reads as a deliberate policy change — worth confirming that's the intent. Two follow-on effects: previously fetched device-local catalog data is now silently ignored, and main/services/models-dev-cache.ts has no remaining live importers (dead code that can be removed or kept for the refresh tooling).
DeepSeek Pro | 𝕏
| * descendants. Reap every child that stayed in the occupied process group | ||
| * so a zombie cannot make kill(-group, 0) report a false cleanup failure. */ | ||
| for (;;) { | ||
| pid_t reaped = waitpid(-group, &ignored_status, 0); |
There was a problem hiding this comment.
cleanup_group's Linux reap loop blocks with no deadline. waitpid(-group, …, 0) waits for every adopted descendant to change state, so a single D-state descendant (e.g. a subagent command touching a hung FUSE/NFS mount) or a grandchild that escaped the group before the SIGKILL and rejoined it afterwards hangs the helper forever — the 1000 ms confirmation deadline below is never reached and no response is ever sent. On macOS only the direct child is waited on, so this widens that bounded contract to every descendant; I reproduced the hang with a compiled harness (grandchild rejoin after the kill: timeout 3 … → exit 124).
Technical details
# Bound the Linux subreaper reap loop in cleanup_group
## Affected sites
- native/subagent-shell-runner/main.c:295-300 — the `for (;;)` `waitpid(-group, &status, 0)` loop runs before the 1000 ms `process_group_exists` confirmation (lines 303-305). A group member that survives `SIGKILL` (uninterruptible D-state, or a process that left the group before the kill and rejoined the still-existing pgid afterwards) blocks it indefinitely.
- main/services/subagents/subagent-shell-runner-io.ts:234 — the main-process watchdog (`timeoutMs + 2_500`) eventually SIGKILLs the hung helper, so the turn fails after the full command timeout instead of completing cleanup; the helper never sends its `OUTCOME_CLEANUP_UNCONFIRMED` response.
## Required outcome
- The reap phase must be bounded by the same deadline as the confirmation poll: reap with `WNOHANG` only while `monotonic_ms() < deadline`, then fall through to the existing `waitpid(direct_child)` + `process_group_exists` check so cleanup either confirms or reports unconfirmed within ~1 s, matching the macOS contract.
## Suggested approach (optional)
- Replace the loop with `while (monotonic_ms() < deadline) { pid_t r = waitpid(-group, &ignored_status, WNOHANG); if (r > 0) continue; if (r < 0 && errno == ECHILD) break; if (r < 0 && errno == EINTR) continue; usleep(10000); }` and keep the existing deadline poll. If practical, add a Linux-only regression fixture mirroring the compiled repro (escaped-then-rejoined grandchild or paused child).| "dist": "node scripts/run-macos-distribution.mjs", | ||
| "dist:linux": "npm run build && electron-builder --linux AppImage deb rpm --config.forceCodeSigning=false --config.directories.output=release/linux-distribution --publish never", | ||
| "test:linux-native": "node scripts/build-worktree-remover.mjs && node scripts/build-worktree-remover.mjs --test && node --test scripts/worktree-remover.test.mjs && node scripts/build-bot-inbox-writer.mjs && node scripts/build-bot-inbox-writer.mjs --test && node --test scripts/bot-inbox-writer.test.mjs && node scripts/build-subagent-run-store.mjs && node scripts/build-subagent-run-store.mjs --test && node --test scripts/subagent-run-store.test.mjs && tsx --test main/services/subagents/subagent-run-store-io.test.ts && node scripts/build-subagent-file-mutator.mjs && node scripts/build-subagent-file-mutator.mjs --test && node --test scripts/subagent-file-mutator.test.mjs && node scripts/build-subagent-shell-runner.mjs && node scripts/build-subagent-shell-runner.mjs --test && tsx --test main/services/subagents/subagent-shell-runner-io.test.ts", | ||
| "test:linux-contracts": "tsx --test main/application-lifecycle-core.test.ts main/desktop-cli-core.test.ts main/services/application-menu-core.test.ts main/services/computer-use/platform.test.ts main/services/external-editors.test.ts main/services/profile-share-files.test.ts main/services/provider-key-policy.test.ts main/services/secure-storage-core.test.ts main/windows/main-window-options.test.ts main/windows/pill-window-platform.test.ts renderer/components/environment-subagents-contract.test.ts renderer/lib/command-system-core.test.ts renderer/shared/keybindings.test.ts && node --test scripts/native-c-build-core.test.mjs scripts/configure-electron-fuses.test.mjs scripts/verify-linux-package.test.mjs", |
There was a problem hiding this comment.
Six of the new platform-policy contract tests are not registered in any npm script, so they never run in npm test or either CI job. main/services/host-platform-capabilities.test.ts, main/services/dictation-platform.test.ts, main/handlers/bots-platform-contract.test.ts, main/services/aiden-remote-platform-contract.test.ts, main/services/models-dev-live-app-policy.test.ts, and main/services/telegram/telegram-bot-binding-platform.test.ts pin load-bearing gates (bots IPC registration, remote Bot routes, the models.dev offline policy, Apple Foundation Models gates) via source matching — unregistered, a regression in any of those would go uncaught. Repo convention requires new test files to be registered in the relevant test/test:coverage scripts.
Technical details
# Register the unexecuted platform-policy test files
## Affected sites
- package.json:129 — `test:linux-contracts` omits the six files above (checked against the entire `scripts` object; each filename matches no script).
- package.json — `pretest` does not chain `test:linux-contracts`, so even the registered Linux policy tests only run in the new Linux CI job, never in the macOS `npm test` flow.
## Required outcome
- Add the six files to `test:linux-contracts` (they are platform-independent source-reading tests) so the Linux CI job executes them, and consider adding that script to the general test flow so macOS CI exercises the cross-platform policy contracts too.| int value_result = | ||
| linux_xattr_value(source, name, &value, &value_length); | ||
| if (value_result != 1 || | ||
| fsetxattr(destination, name, value, value_length, 0) != 0) { |
There was a problem hiding this comment.
On enforcing-SELinux systems this copy step makes the whole transaction fail: when the source carries a security.* xattr that differs from the staged file's default label, fsetxattr here returns EPERM for the unprivileged helper, linux_copy_xattrs returns -1, and every commit onto such a file (non-default chcon labels, security.capability setcap binaries) aborts with RESULT_IO. That's fail-closed — no silent drift — but it means file mutation is unavailable for exactly these files on Fedora/RHEL, which this PR lists as supported targets; the Fedora CI tests won't catch it because test files inherit their directory's default label.
Technical details
# Handle non-copyable security.* xattrs on SELinux hosts
## Affected sites
- native/subagent-file-mutator/main.c:404-419 — `linux_copy_xattrs` calls `fsetxattr(destination, name, value, value_length, 0)` for any mismatched xattr name, including `security.selinux`/`security.capability`, which requires `CAP_MAC_ADMIN`/`CAP_SETFCAP` and fails for the unprivileged helper.
## Required outcome
- File mutation keeps working on enforcing-SELinux hosts for files with non-default security labels, while the staged copy still preserves user-visible xattrs and never silently accepts drift.
## Suggested approach (optional)
- Treat `security.*` and `trusted.*` namespaces as non-copyable in the Linux path (skip them in copy and exclude them from the match set, mirroring how the macOS provenance field is handled), or document the limitation explicitly in `docs/linux.md` before declaring Fedora fully supported.
## Open questions for the human (optional)
- Is excluding the security namespace acceptable, given the destination file receives its own default security label anyway?There was a problem hiding this comment.
ℹ️ The incremental delta is clean — no new issues. The two blocking findings from the prior review (the unbounded subagent cleanup reap loop and the unregistered platform-policy test files) were re-verified against 2cc31db and still stand; the review comments remain open on the PR.
Reviewed changes (delta since the review of 09d5890)
- 0.36.1 Pi extension sync — the attended-chat Ask User Question composer, native todo, BTW, and Advisor extensions are merged in;
registerBtwHandlers()andinitializeAdvisorRuntime()are wired unconditionally inregisterHandlers(), which matches their platform-neutral design. - Linux CI extension gates — both Ubuntu and Fedora jobs now run
npm run test:pi-extensions, pinned by a newcheck-ci-policy.test.mjsassertion (exactly two occurrences). - Version and catalog sync — package version bumps to 0.36.1 and the bundled model catalog is refreshed.
- Linux test-seam fix —
bot-inbox-writer.test.mjsnow tolerates the intentional early-stdin-closeEPIPEon Linux while still failing on any other stdin error. - Plan bookkeeping — plan index rows for the rpiv integrations and the advisor plan moves to
completed/.
DeepSeek Pro | 𝕏
There was a problem hiding this comment.
ℹ️ The incremental delta is clean — no new issues. Commit 901110a is CI-only, so the two blocking findings and the SELinux note from the initial review remain open in their threads (no
native/orpackage.jsonpaths changed since 2cc31db). The hosted Fedora acceptance job is still running as of this review — first live validation of the new handoff.
Reviewed changes (delta since the review of 2cc31db)
- Baseline-verified RPM handoff — the Ubuntu x64 leg now writes
rpm.sha256and uploads the RPM as a run-scoped artifact; the Fedora job gainedneeds: linuxand downloads, checksums, installs, and re-verifies that RPM instead of rebuilding it (droppingrpm-buildandlibxcrypt-compatfrom the container). - Force teardown GUI smokes — both
ci.ymlandrelease.ymlsmokes switched fromtimeout --kill-after=5s/ exit 124 totimeout --signal=KILL/ exit 137, removing the graceful-shutdown window inside the headless session; I verified on this runner that GNUtimeoutgroup-kills its process group and exits 137. - CI policy coverage —
scripts/check-ci-policy.test.mjsgained two source-pinning tests for the artifact handoff (checksum, artifact name,needs: linux, no-rebuild assertions) and the KILL smoke semantics; they run viatest:brandingand pass 5/5 locally.
DeepSeek Pro | 𝕏
There was a problem hiding this comment.
ℹ️ The incremental delta is clean — no new issues. Commit f2916769 tightens one Playwright assertion to the exact shipped dictation fallback copy, which I verified against
renderer/components/settings/dictation-shortcut-settings.tsx:246(theTextspan is a single text node, soexact: truematches). The two blocking findings and the SELinux note from the initial review remain open in their threads — re-verified unchanged at this head (package.jsonstill registers none of the six policy-contract test files, and bothnative/sites are byte-identical).
Reviewed changes (delta since the review of 901110a)
- Exact dictation-fallback e2e assertion —
tests/e2e/settings-model-picker.spec.tsreplaces the loose/copies the transcript to the clipboard/uregex (which never matched the shipped copy) with an exact-match assertion on "Completed transcripts are copied to the clipboard so you can paste them into any app.", so the Linux voice-settings smoke now both passes and fails loudly if the copy drifts.
DeepSeek Pro | 𝕏
…-support # Conflicts: # android/app/src/main/java/sbtbiswas/AidenOnTheGo/features/scheduled/AidenScheduledTasksScreen.kt # docs/plans/README.md # ios/AidenOnTheGo/Features/Remote/AidenScheduledTasksView.swift
There was a problem hiding this comment.
ℹ️ The incremental delta is clean — no new issues. The merge of
origin/main(the chat-native Scheduled Tasks line from PR #73) resolved without behavioral regressions: the two blocking findings and the SELinux note from the initial review remain open in their threads and were re-verified unchanged at this head.
Reviewed changes (delta since the review of f2916769)
- Main feature-line merge —
origin/mainis merged in; the reviewable surface is the conflict resolutions, which adapt copy and shared types rather than code paths. - Android confirmation rename —
requiresMacConfirmationis renamed torequiresDesktopConfirmationconsistently acrossAidenChat.kt,AidenChatDetailScreen.kt, andAidenChatTest.kt; no stale callers remain. - iOS desktop copy sweep completion — every scheduled-task and approval string now says "paired desktop" (a repo-wide grep finds zero remaining "your Mac" in either companion);
scripts/check-ios-shipping-target.test.mjspins the new copy and passes 12/12 locally. - Remote approval gate reconciliation —
main/services/aiden-remote-streams.tsadopts main'sapprovalIsHostOnly(scheduled-task approvals stay remotely allow-able whencanAllow); the file's only divergence from main is the host-only error copy "from the Aiden desktop app.". - Shared assistant union merge —
renderer/shared/assistant.tskeeps the branch's/bin/shSubagentShellApprovalShellunion and itsisSubagentShellApprovalShellguard alongside main's newScheduledTaskApprovalDetailsandisScheduledTaskApprovalDetails. - Docs — the plans index gains the scheduled-tasks-experience row, and the archived Linux plan now notes Scheduled Tasks are platform-neutral on Linux.
Verification this run: npm run type-check is clean (after refreshing a stale node_modules missing bonjour-service, which is declared in package.json), npm run test:linux-contracts passes 20/20, scripts/check-ci-policy.test.mjs passes 5/5, no conflict markers exist anywhere in the tree, and the host capability gates in llm-client.ts and the remote handlers survive the merge.
DeepSeek Pro | 𝕏
There was a problem hiding this comment.
ℹ️ The incremental delta is clean — no new issues. Commit 5b380a30 replaces the
selectText()+press("Backspace")clearing sequence in the scheduled-tasks e2e smoke with a singlefill("")— Playwright's canonical deterministic clear, independent of focus state and platform keyboard-selection semantics. The assertions that follow (toHaveValue("")andtoHaveCount(0)on "No matching tasks") already pin the cleared state, and the samefill("")idiom is used earlier in the same spec. The two blocking findings and the SELinux note from the initial review remain open in their threads: this commit touches onlytests/e2e/assistant-scheduled-profile.spec.ts, so thenative/andpackage.jsonsites are unchanged.
Reviewed changes (delta since the review of 0e414b4b)
- Deterministic e2e search clearing —
tests/e2e/assistant-scheduled-profile.spec.tsswaps the select-text-and-backspace keystroke pair forfill("")when clearing the scheduled-task search box, removing a platform-dependent keyboard selection step from the Linux smoke path.
DeepSeek Pro | 𝕏
There was a problem hiding this comment.
ℹ️ The incremental delta is clean — no new issues. Commit 6a397578 hardens two test flows and adds one small user-facing control. The two blocking findings and the SELinux note from the initial review remain open in their threads and were re-verified unchanged at this head (the commit touches no
native/orpackage.jsonpaths).
Reviewed changes (delta since the review of 5b380a30)
- Watcher registration-race baseline —
bot-skill-content-watcher.test.tsextractswaitForWatcherBaseline(), a 75 ms drain of Darwin's already-queued directory notification afterfs.watchregistration, and applies it before every observed mutation; watcher event timeouts rise from 1 s to a shared 5 s constant, and the assertions (lease aborted, unrelated-file count stays 0, warm snapshot refreshes to"After") remain exact and fail-able. - Accessible clear action for scheduled-task search —
scheduled-tasks-view.tsxreplaces the plain search wrapper with a real "Clear scheduled task search" icon button that empties the query and restores focus to the input; the input keeps itsaria-labeland gainspr-9clearance, and the button follows the houseiconOnly/transparentidiom (same shape asproviders-settings.tsx). - Real-user-path e2e assertion —
assistant-scheduled-profile.spec.tsclicks the new clear button instead of synthesizing keystrokes, pinning the cleared value, the restored input focus, and the button's unmount. - Papercut records — two new entries document the Darwin watcher race and the
fill("")/clear()select-and-delete limitation that motivated both fixes.
DeepSeek Pro | 𝕏

Summary
main, including Bots, Web Search, diagnostics, Generative UI, Parakeet, Remote Access, subagents, Model Pad, Pi Ask User Question/Todo/BTW/Advisor, the current provider/model catalog, and chat-native Scheduled TasksIntentional Linux tradeoffs
Validation
Verification on the final branch included:
npm run type-checknpm run type-check:e2enpm run lintnpm run buildnpm run testnpm run test:scheduled(98 passing)npm run test:pi-extensions(127 passing)npm run test:bots(431 passing)npm run test:linux-contracts(94 passing; one expected host-only skip)npm run test:model-catalogHosted verification:
origin/mainatf87e9e7f64400cbb7bee3335fb4f876e53efa970is an ancestor of the PR head