fix(host-kit): one async process-identity reader and one identity-checked stop escalation

Merged
TH
4h ago
+1923-674

Design

  • Identity reader: readProcessIdentities(pids, timeoutMs) and readProcessIdentityFacts(pid) answer {startTime, command, zombie} off the event loop. They use two concurrent ps reads on POSIX and one CIM query on Windows. A pid that gets no answer reads as all-null, never as "absent".
  • Identity rule: judgeProcessIdentity(expected, facts) returns exited, reassigned, ours or unverified, and it is the whole rule for whether host facts name a recorded process.
    • ours only when every recorded claim (start time and/or command) was read and matches.
    • reassigned for a contradicted start time, or for a command mismatch when no start time was recorded.
    • unverified for everything else, including one confirmed claim beside an unread one.
    • The host-kit stop, the app-log inspect and the screen-recording marker evidence all use it.
  • Caching: only the process's own identity is cached (readCurrentOwnerIdentity, memoized once readable). A foreign identity is never cached. A process-lock wait re-reads a live owner's or guard holder's identity at most once per second for the same claim, and checks kill(pid,0) on every poll.
  • Escalation: within host-kit, stopProcessAndConfirmExit is the single TERM→KILL path. It can open with SIGINT. It requires an ours verdict before the graceful signal and again before SIGKILL; anything else retains the process. Grace periods are set by each caller: reaper 1.5 s / 1 s, emulator 1.5 s / 1.5 s, app-log 2 s / 2 s. Two ladders outside host-kit keep their own escalation, and both check identity: the screen-recording set ladder (SIGINT, then SIGTERM, then SIGKILL, re-proving each marker per rung) and daemon-process.ts stopDaemonProcess.

Summary

Fixes 2026-11-10 audit findings CONTR2-1, SRC2-2, CMD-3, CMD-4, CMD-6 and PROV2-5 (call site only).

  • No synchronous ps on the daemon event loop: device-claim, lock-wait, recording, app-log and browser-fleet paths no longer block on it.
  • Reclaim guard (CMD-4): the guard file now records its holder. A guard whose holder is dead or whose pid was reused is reaped under a per-token marker. ADR 0030 is amended.
  • One host-kit escalation (CMD-6): stopPidsWithEscalation is removed.
  • Behavior changes:
    • The reaper summary reports unconfirmed and survived instead of the mislabelled ownershipLost. A record whose pid is proven to belong to another lifetime is forgotten without a signal.
    • A record or marker whose recorded lifetime is not fully confirmed is retained, not signaled. That covers an unread start time or command, and a recorded start time shown running another command.
    • stopCompanionProcess (react-native client-companion-tunnel.ts) now gets identity-unverified in two cases: a recorded start time with a command mismatch, and a companion whose command line the host hides (the #3540 elevated case). It throws through companionStopFailure and keeps its state; it does not signal.
    • The Android emulator focus helper (osascript) is best effort. A failure to start it emits android_emulator_focus_return_failed and never fails the launch.
  • Deferred: ARUN2-1 and ARUN2-5 change runner and device teardown timing, so they need a device run.

30 files changed.

Validation

  • Tested SHA 778aa6e (check:affected --run, exactCommit=true): format, lint, typecheck, layering, fallow and build pass. Focused host-kit, app-log, screen-recording, emulator and react-native companion tests pass (522).
  • integration-node (local): 2 smoke-daemon-clean tests fail because the container's PID 1 does not reap the killed daemon, which stays a zombie. The daemon stop code is unchanged here.
  • Device runs: two live runs were posted. The iOS simulator record stop and app-log stop, and the Android cold-boot abort, at bd15e4a (issuecomment-6777371512). The Android cold-boot cancel and the -avd token check at 9f48ee2 (issuecomment-6778854131). Both predate the stricter identity rule in 778aa6e.
  • CI obligations: the full unit suite and the Android lane, because the emulator terminate path uses the shared escalation.
  • Risk: the reaper forgets records whose pid is proven reused; an elevated companion on Windows is retained and reported, never signaled.

Generated by Claude Code


<a href="https://backend.blacksmith.sh/track/enable-autofix?expires=1796921043&installation_model_id=447842&pr_number=4359&ref=codesmith_pr_footer&repository=callstack%2Fagent-device&return_to=https%3A%2F%2Fgithub.com%2Fcallstack%2Fagent-device%2Fpull%2F4359&signature=f672fb3630ba26d7e6468b7356f6af3c59cf9f56801a9473879c025372e1f33b"&gt;&lt;picture&gt;&lt;source media="(prefers-color-scheme: dark)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"&gt;&lt;source media="(prefers-color-scheme: light)" srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"&gt;&lt;img alt="Autofix with [code]smith" src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png" width="190" height="28"&gt;
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.

<a href="https://www.cubic.dev/action/auto-fix/pr/callstack/agent-device/4359?returnTo=https%3A%2F%2Fgithub.com%2Fcallstack%2Fagent-device%2Fpull%2F4359&source=description" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true">&lt;source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"&gt;&lt;source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"&gt;&lt;img alt="Turn on auto-fix" src="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"&gt;