Skip to content

fix(open): refuse --launch-console on a provider-owned device before the relaunch close - #3938

Merged
thymikee merged 1 commit into
mainfrom
fix/open-admission-before-device
Oct 28, 2026
Merged

thymikee merged 1 commit into
mainfrom
fix/open-admission-before-device

Conversation

@thymikee

@thymikee thymikee commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Summary

open <app> --relaunch --launch-console <path> against a provider-owned iOS simulator (Limrun, TestMu) closed the running app and only then failed. A provider simulator has the same device shape as a local one, so the existing shape check let the request through.

The provider branch of the lifecycle binding (packages/contracts/src/application-lifecycle-interaction.ts) now refuses --launch-console before it resolves an interactor, so no close or launch runs. The refusal is UNSUPPORTED_OPERATION with details.reason: launch-console-owner-unadmitted, details.deviceId, and a hint to drop the flag or use a local iOS simulator. The flag help and docs now say "local iOS simulator". The provider scenario fixture is built with bindDirectApplicationLifecycle, as production provider runtimes are, and a new scenario proves the refusal leaves the fake device with no close and no open.

Scope change: this replaces the earlier version of the PR (owner admission called at each lifecycle entry, 12 files, +332/−54). That version kept the older Apple and Vega refusals beside the new check, and preparation steps still ran before it. Moving one admission step ahead of prepareApplicationOpen was also prototyped (69 files, +384/−136); it adds a lifecycle operation every runtime owner, including provider plugins, must implement. The failure only occurs on provider-owned devices, so the fix stays at that one branch. Local iOS and Android open paths are unchanged.

Validation

  • Tested SHA: 51a56a6b9f (rebased onto main at 61ce711a1c).
  • pnpm check:affected --run: all runnable checks passed (exactCommit=true; format, lint, typecheck, layering, fallow, build, vitest-related, command-docs, docs-links, docs-prose).
  • Live Limrun iOS simulator, same change before the rebase (f40fdfd9cf): open host.exp.Exponent succeeded; open host.exp.Exponent --relaunch --launch-console <path> --json returned UNSUPPORTED_OPERATION, reason: launch-console-owner-unadmitted, the Limrun deviceId, and the hint; a following snapshot still showed Expo Go in the foreground. The instance was torn down.
  • Not run: local iOS simulator and Android emulator, whose open paths this change does not touch.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.56 MB 5.56 MB +285 B
Package (unpacked) 5.56 MB 5.56 MB +285 B
Package (download) 1.69 MB 1.69 MB +67 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 36.8 ms 36.2 ms -0.5 ms
CLI --help 75.7 ms 75.0 ms -0.7 ms

@thymikee

Copy link
Copy Markdown
Member Author

The code looks right at 37dc3c4, but the live evidence for the dispatch-order change is still missing. All 16 checks pass at this commit, and there are no conflicts.

This PR changes app-lifecycle dispatch order in the Android and Apple lifecycle.ts, so it needs a device run at this SHA. First, on a local iOS simulator, agent-device open <bundle> --platform ios --relaunch --launch-console <path> should succeed, and <path> should contain launch output. That shows admission still admits the one supported owner after the device is rebound. Second, on an Android emulator with the app in the foreground, open <pkg> --relaunch --launch-console <path> --json should return UNSUPPORTED_OPERATION with details.reason set to launch-console-owner-unadmitted. A following appstate or snapshot should show the app still in the foreground. Please record the device, SHA and outputs.

Not blocking, and you can take or leave these: (1) The daemon calls prepareApplicationOpen and clearRuntimeHints (session-open-prepare.ts:161) before assertOpenAdmitted runs. So open <app> --launch-console <path> on a shut-down Android emulator boots it and then refuses. When stored runtime hints are being removed, it clears them and then refuses. On a cold-booting iOS target, prewarmRunnerCache fires before the refusal. The rule should be that no open-admission refusal follows any lifecycle operation call. Otherwise, narrow the PR claim to "before any operation inside openApplication". (2) openIosApp (app-launch.ts:69) and the Vega interactor still refuse launch-console with their own checks. LAUNCH_CONSOLE_IOS_SIMULATOR_ONLY_MESSAGE survives only for them, so those branches and the constant can go, along with the tests that pin them. (3) The providerScenarioApplicationLifecycle fixture (provider-device-runtime.fixtures.ts:199) hand-rolls openApplication without assertOpenAdmitted. ios-lifecycle.test.ts:86 therefore still expects a provider-bound iOS simulator --launch-console open to succeed, while the production provider owners now refuse it. That fixture should use the same direct binding as the daemon harness, or expect the typed refusal. (4) Provider-backed iOS simulators now get UNSUPPORTED_OPERATION instead of silently ignoring --launch-console. The flag help (flag-definitions-target.ts:366) and the docs in commands.md:101, client-api.md:70 and debugging-profiling.md:151 should say "local iOS simulator".

Could launch-only options be admitted once, before prepareApplicationOpen? The binding or the owner's lifecycle could expose an admission step that the daemon calls with the positionals and execution. Or the Apple local simulator could declare launch-console support as an owner fact, which the daemon admits with the existing facts. Then the three manual assertOpenAdmitted calls, the openIosApp and Vega duplicates and the old message constant could all be deleted. This would first need OpenApplicationPreparationInput, or a new admission operation on ApplicationLifecycleRuntimeOperations, to carry the positionals and the launch-only execution fields. No ADR looks needed. If that is out of scope, the minimum is the deletions in note (2) and a narrower PR claim.

I did not run tests or check:affected locally; I judged regression validity from the pre-change code in the diff. I treated host.androidApplications.clearRuntimeHints as a device write from its name and the PR wording, and did not trace it to the adb level. I also did not check whether Apple ensureAppleReady on a physical device has side effects beyond readiness.

Before merge, this needs the two device runs above and an answer on admitting before prepareApplicationOpen and removing the per-backend launch-console checks.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-28 15:21 UTC

@thymikee

Copy link
Copy Markdown
Member Author

Thanks for the update. The code at 0a4b925 reads well, and all 19 checks pass. There are no conflicts. The device runs I asked for in the 37dc3c4 review (#3938 (comment)) are still missing, so this is not ready to merge yet.

This PR moves the open-admission refusal ahead of the close, runner and runtime-hint steps in packages/platform-apple/src/lifecycle.ts:126 and packages/platform-android/src/lifecycle.ts:112. That changes app-lifecycle dispatch on live devices, and the PR body says it is not device-facing. Nothing yet shows that the one supported owner (a local iOS simulator) is still admitted after rebinding, or that an Android refusal leaves the foreground app untouched. Please record the device, the SHA and the outputs of two runs. First, on a local iOS simulator, agent-device open <bundle> --platform ios --relaunch --launch-console <path> should succeed, and <path> should contain launch output. Second, on an Android emulator with the app in the foreground, open <pkg> --relaunch --launch-console <path> --json should return UNSUPPORTED_OPERATION with details.reason launch-console-owner-unadmitted and details.deviceId. A following appstate or snapshot should still show <pkg> in the foreground.

Could admission run once, before prepareApplicationOpen, instead of at each lifecycle? Either ApplicationLifecycleRuntimeOperations gets an admitOpen(positionals, execution) operation that session-open-prepare calls first, or prepareApplicationOpen receives the positionals and launch-only execution fields and runs assertOpenAdmitted itself. Then the manual calls in the Android, Apple and direct lifecycles and in both test fixtures can go, along with the openIosApp :69 and Vega :24 launch-console refusals, LAUNCH_CONSOLE_IOS_SIMULATOR_ONLY_MESSAGE, its facade export, and the tests that pin them. This delta adds a fifth manual call site, and the earlier question is still open. The smaller alternative is those deletions plus narrowing the PR claim to "before any operation inside openApplication". Either way, that input or a new admission operation has to carry the positionals and launch-only fields first. No ADR is needed.

Not blocking, and fine to take or leave: the new scenario test in provider-device-runtime.fixtures.ts:214 uses a hand-written openApplication that copies assertOpenAdmitted and a close-then-open. Production provider runtimes (provider-limrun/src/lifecycle.ts:29, provider-webdriver/src/lifecycle.ts:19) go through bindDirectApplicationLifecycle and openDirectApplication, so removing the admission there would leave this scenario green. Building the fixture with bindDirectApplicationLifecycle({ binding, closeBeforeRelaunch: true, ... }) would fix that.

I ran no tests or device commands myself. I judged the fixture from the fixture and provider sources. I did not find the committed mutation test that the PR body describes, and I did not reproduce it. I also did not trace whether a shut-down Android emulator boots during prepareApplicationOpen before the refusal. That part relies on the earlier reading of session-open-prepare.ts:161. Before merge, please add the two device runs at the final head and settle the admission placement above.

…the relaunch close

A provider-owned iOS simulator has the same device shape as a local one, so
open --relaunch --launch-console passed the existing shape check, closed the
running app, and only then failed. The provider branch of the lifecycle
binding now refuses launch-console with UNSUPPORTED_OPERATION and the typed
reason launch-console-owner-unadmitted before it resolves an interactor, so
no close or launch runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee
thymikee force-pushed the fix/open-admission-before-device branch from e8b9aa6 to 51a56a6 Compare October 28, 2026 13:17
@vercel

vercel Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
agent-device-website Ignored Ignored Preview Oct 28, 2026 1:18pm UTC

Request Review

@thymikee thymikee changed the title fix(contracts): refuse launch-only open options before the lifecycle touches the device fix(open): refuse --launch-console on a provider-owned device before the relaunch close Oct 28, 2026
@thymikee

Copy link
Copy Markdown
Member Author

The code looks good at 51a56a6. The earlier concerns are fixed: the hand-rolled fixture is gone, the docs now say "local", and the refusal now comes before the relaunch close, so a provider-owned device is no longer closed and reopened first.

Not blocking, and you can take or leave these: the debugging workflow help in src/commands/schema/cli-help-workflows.ts still says "--launch-console is only for direct iOS simulator app launches" without "local", and two tests pin that wording (cli-help-first-screen.test.ts:68, cli-help-workflows.test.ts:19), so say "local iOS simulator" there and update both assertions; also, the check in packages/contracts/src/application-lifecycle-interaction.ts:90 fires before the shape checks, so a provider open --launch-console x with no app target or with a URL now returns UNSUPPORTED_OPERATION instead of INVALID_ARGS, and no test covers those two cases, so either accept it or note it in the PR body.

All 21 checks pass, with none failing or pending, and there are no conflicts.

On evidence: I could not see the raw Limrun run output behind the PR body's live-validation claim. The tested commit f40fdfd9cf has the same patch-id as head, so the run covers this change, but I could not re-run it or confirm the device and outputs. I did not run TestMu or WebDriver devices live. They share the same binder and bindDirectApplicationLifecycle route as Limrun, so the refusal takes the same code path. I also did not rerun check:affected or the unit and scenario tests, and I judged the regression by reading the old route, where openDirectApplication closes and opens when the refusal is absent.

The remaining step is for a maintainer to accept the recorded Limrun run as the device evidence.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 28, 2026
@thymikee
thymikee merged commit 3537dc5 into main Oct 28, 2026
21 checks passed
@thymikee
thymikee deleted the fix/open-admission-before-device branch October 28, 2026 15:17
thymikee added a commit that referenced this pull request Oct 28, 2026
* origin/main: (76 commits)
  fix(open): refuse --launch-console on a provider-owned device before the relaunch close (#3938)
  fix(test-app): keep the inert surface control above the settings fold (#3993)
  fix(fill): redact the read-back of a sensitive fill (#4014)
  fix(apple): read the runner cache toolchain key from Xcode's own files (#3995)
  docs: point stable test suites to e2e (#4010)
  docs(mutation): state the file-name rule that marks test support code (#4011)
  refactor(kernel): read the Action Button leaf from isHandheldAppleDevice (#3988)
  fix(ios): match alert labels through the OS prompt strings of the preferred languages (#4006)
  fix(mutation): measure each kernel against the tests that import it (#3994)
  fix(webdriver): avoid unused pre-tap focus geometry reads (#3997)
  refactor(apple): share coordinate drag execution (#4003)
  fix(apple): type a text-entry-trait element as a text input in the simulator AX tree (#4008)
  refactor(contracts): own the unsupported-interactor stub both platforms duplicated (#4002)
  refactor: reuse canonical device identity projections (#3996)
  refactor(apple): share simulator status-bar diagnostics (#3998)
  refactor(runtime): share host command runner construction (#3999)
  refactor(android): centralize session error encoding (#4000)
  chore: mark 0.21.33 as released
  chore(gates): detect new native duplication with jscpd (#3980)
  fix(android): publish the Compose role on its clickable parent instead of a fake child (#3983)
  ...
thymikee added a commit to janjankowski-cs/agent-device that referenced this pull request Oct 28, 2026
* base-3865: (76 commits)
  fix(open): refuse --launch-console on a provider-owned device before the relaunch close (callstack#3938)
  fix(test-app): keep the inert surface control above the settings fold (callstack#3993)
  fix(fill): redact the read-back of a sensitive fill (callstack#4014)
  fix(apple): read the runner cache toolchain key from Xcode's own files (callstack#3995)
  docs: point stable test suites to e2e (callstack#4010)
  docs(mutation): state the file-name rule that marks test support code (callstack#4011)
  refactor(kernel): read the Action Button leaf from isHandheldAppleDevice (callstack#3988)
  fix(ios): match alert labels through the OS prompt strings of the preferred languages (callstack#4006)
  fix(mutation): measure each kernel against the tests that import it (callstack#3994)
  fix(webdriver): avoid unused pre-tap focus geometry reads (callstack#3997)
  refactor(apple): share coordinate drag execution (callstack#4003)
  fix(apple): type a text-entry-trait element as a text input in the simulator AX tree (callstack#4008)
  refactor(contracts): own the unsupported-interactor stub both platforms duplicated (callstack#4002)
  refactor: reuse canonical device identity projections (callstack#3996)
  refactor(apple): share simulator status-bar diagnostics (callstack#3998)
  refactor(runtime): share host command runner construction (callstack#3999)
  refactor(android): centralize session error encoding (callstack#4000)
  chore: mark 0.21.33 as released
  chore(gates): detect new native duplication with jscpd (callstack#3980)
  fix(android): publish the Compose role on its clickable parent instead of a fake child (callstack#3983)
  ...
thymikee added a commit that referenced this pull request Oct 28, 2026
…4028)

The debugging workflow help still said direct iOS simulator app launches,
while open's flag help and the docs say local since #3938, where a
provider-owned simulator refuses the flag.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant