Skip to content

feat(helper)!: detach activations, freeze wire - #668

Draft
Alex Shabalin (alex-sparus) wants to merge 1 commit into
alex/fix-helper-build-idfrom
alex/fix-helper-runner
Draft

feat(helper)!: detach activations, freeze wire#668
Alex Shabalin (alex-sparus) wants to merge 1 commit into
alex/fix-helper-build-idfrom
alex/fix-helper-runner

Conversation

@alex-sparus

@alex-sparus Alex Shabalin (alex-sparus) commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Problem: SMAppService unregister kills the helper process, and the helper was the process running the root activation — so replacing the helper on upgrade risked interrupting a root mutation in flight. And whatever wire surface ships is a forever commitment: every future GUI must be able to talk to a helper of any older build.

Solution: the helper only authenticates and admits. The activation runs in a forked no_std supervisor in its own session that survives unregister (the shipped plist gains AbandonProcessGroup, asserted by a test against the real resource), so terminating a helper interrupts nothing. The single activation slot is an exclusive flock on a never-unlinked lock file, shared with the password path's root executor, so "an activation is running" has one source of truth across helper generations and both paths. The cross-build wire is kept minimal — a frozen Status exchange plus three typed refusals, byte-pinned by golden tests — while TryActivate stays same-build-only and free to change. Connections close after the reply.

Stacked on #667.

Test Plan

  • cargo test green: golden wire fixtures, flock admission/release (incl. unwind paths), real fork+supervise integration tests (exit codes, signal deaths, straggler cannot inherit the lock, capped log tail, profile-update warning)
  • Platform behavior verified by hand on macOS 26.6.2: a detached child of an SMAppService daemon survives unregister, launchctl asuser keeps working from the orphan, and a full real nix-darwin activation started before unregister ran to completion (exit 0) after it

Docs

  • No docs update needed

Prior review

peer_auth.rs (710 lines) is byte-identical to the version Scott approved in #619. Everything else here is the redesign that replaced #619's single-slot state machine — the detached runner, the flock slot, and the shrunken frozen wire — and is the part that needs a real review.

@alex-sparus

Alex Shabalin (alex-sparus) commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by jj-ryu.

@darkmatter

darkmatter Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🎨 Storybook preview

Open Storybook preview

Updated for 7b637c2


⚠️ Detected UI changes (5)

These stories' HTML snapshots changed. I've added screenshots + links to the changed stories below. Review them carefully then accept the changes to regenerate baselines and include them in this PR:

Flows/Evolve › Playground

Flows/Evolve › Playground

Flows/Evolve › 1. Begin (idle)

Flows/Evolve › 1. Begin (idle)

Flows/Evolve › 2. Evolving (progress)

Flows/Evolve › 2. Evolving (progress)

Flows/Evolve › Evolving With Error Event

Flows/Evolve › Evolving With Error Event

Flows/Evolve › 3. Review (changes generated)

Flows/Evolve › 3. Review (changes generated)


Accept UI changes

  • Click here to accept these changes

Alternatively, you can run bun run test:update-snapshots locally to re-generate the baselines and then push the changes to this PR.

What does this do?

The screenshots above show UI changes detected by the Storybook
snapshot tests run on this PR. Each image is the rendered output of
a Storybook story from the code in this PR branch; the snapshot
test compared it against the committed baseline in
__snapshots__/ and flagged the difference.

Checking the box tells the darkmatter[bot] to regenerate the
baselines from this PR's current code and commit them directly to
this branch. The new baselines become the source of truth for
future runs — only accept after confirming the visual changes are
intentional.

Comparison baseline: the committed __snapshots__/ files on this
PR branch (carried forward from develop). Accept updates them in
place on this branch.

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor
Warnings
⚠️

No Linear issue ID found in this PR's title, description, or branch name (expected something like ENG-123). Add one so this work is traceable in Linear, or add #no-linear to the PR description to acknowledge it's intentionally untracked.

⚠️ PR is marked WIP / draft — do not merge until ready for review.
⚠️ ❗ Big PR (4103 lines changed). Consider splitting it into smaller, focused changes.

📋 PR Overview

Lines changed 4103 (+3295 / -808)
Files 2 added, 10 modified, 0 deleted
Draft / WIP yes
Has Test Plan yes
Linear issue no
No Test Plan Needed no
New UI components no
New Storybook stories no
New Rust modules no
New TS source files no
New tests no
package.json touched no
Cargo.toml touched yes
Infra / CI touched no

🔬 Coverage

Report Lines Statements Functions Branches
apps/native/coverage/coverage-summary.json 35.6% 35.2% 30.5% 29.5%

Generated by 🚫 dangerJS against 7b637c2

self.current.lock().unwrap_or_else(PoisonError::into_inner)
}

/// The `Status` answer, from live observation: this process's own memory

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This calculation is surprisingly (to me, at least) complex, and I have general concerns about if there's any way for "status" to get persistently out of sync and therefore block the product behavior -- for example, due to a crash / restart / ungraceful termination of a process.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

With the help of AI:

There's no persisted status anywhere to get out of sync: every Status answer is recomputed live from one kernel flock probe, and the kernel releases a flock on any process death, crash and SIGKILL included. So a dead runner reads Idle on the next probe with zero cleanup code, and a restarted helper only loses the display-only details (it still reports Activating while the runner lives). "Blocked forever" would need the lock held forever, which needs a live process actually holding it — i.e. a real (or genuinely hung) activation, which Busy then reports truthfully.

The complexity that's visible here is one mutex for one narrow race (a status probe's momentary LOCK_SH turning a concurrent admission into a phantom Busy); the flock itself is the simplification — crash-consistency comes from the kernel instead of from code we'd have to get right.

Problem: SMAppService unregister kills the helper process, and the
helper was the process running the root activation — so replacing the
helper on upgrade risked interrupting a root mutation in flight. And
whatever wire surface ships is a forever commitment: every future GUI
must be able to talk to a helper of any older build.

Solution: the helper only authenticates and admits. The activation
runs in a forked no_std runner in its own session that survives
unregister (the shipped plist gains AbandonProcessGroup, asserted by a
test against the real resource), so terminating a helper interrupts
nothing. The single activation slot is an exclusive flock on a
never-unlinked lock file, shared with the password path's root
executor, so "an activation is running" has one source of truth across
helper generations and both paths. The cross-build wire is kept
minimal — a frozen Status exchange plus three typed refusals,
byte-pinned by golden tests — while TryActivate stays same-build-only
and free to change. Connections close after the reply.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants