Skip to content

feat: support iOS Simulator launch environment - #3000

Open
csark0812 wants to merge 1 commit into
callstack:mainfrom
csark0812:chris/agent/launch-environment
Open

csark0812 wants to merge 1 commit into
callstack:mainfrom
csark0812:chris/agent/launch-environment

Conversation

@csark0812

@csark0812 csark0812 commented Sep 27, 2026 •

Copy link
Copy Markdown

Summary

  • add launchEnvironment to the typed app-open API and carry it through CLI, MCP, daemon, and runtime contracts
  • add repeatable --launch-env KEY=VALUE parsing with structured validation and duplicate-key rejection
  • translate caller keys to SIMCTL_CHILD_ variables only at the iOS Simulator host boundary
  • reject physical iOS, macOS, other unsupported leaves, and bare URL launches with structured errors
  • redact launch environment values from diagnostics and prove App Clip URL plus launch-argument composition in provider-backed tests

Validation

  • pnpm check:affected --run
  • all locally runnable checks passed, including package tarball and clean-install entrypoint validation
  • GitHub-authoritative Apple runner and live simulator lanes are intentionally not claimed by local validation

Based on upstream 0.21.15 / 16e0757.

Review in cubic

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

13 issues found across 32 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="test/integration/provider-scenarios/ios-world.ts">

<violation number="1" location="test/integration/provider-scenarios/ios-world.ts:258">
P3: `recordLaunchEnvironment` copies only two hardcoded keys out of `envPatch`, so the final `assert.deepEqual(launchEnvironments, [...])` in the test proves only a subset of the translated environment: any additional `SIMCTL_CHILD_*` key the daemon passes is dropped silently and the assertion still passes. The declared record type (`Record<string, string | undefined>`) already reflects this partial capture. Capture the whole patch (`{ ...options.envPatch }`) so the later deepEqual verifies the exact env composition, and it also removes the undefined-valued keys from the record shape.</violation>
</file>

<file name="packages/contracts/src/application-lifecycle-interaction.ts">

<violation number="1" location="packages/contracts/src/application-lifecycle-interaction.ts:209">
P2: `assertLaunchEnvironmentSupport` gates on `resolveDeviceAppleOs(device) === 'ios'`, which excludes iPadOS simulators (`appleOs: 'ipados'`). iPad Simulator launches are iOS Simulator app launches — discovery classifies iPad models as `'ipados'` (inventory-classification.ts:62) and `openIosApp` accepts any `kind === 'simulator'` — so `--launch-env` is rejected with "supported only for iOS Simulator" on exactly the devices the platform layer supports. Use `isIosFamily(device) && device.kind === 'simulator'` to match the `launchConsole` gate (also `resolveDeviceAppleOs` falls back to `'ios'` for non-Apple legacy records, making the check both too narrow and accidentally permissive).</violation>

<violation number="2" location="packages/contracts/src/application-lifecycle-interaction.ts:267">
P2: This forwards `launchEnvironment` into the separate `runtimeLaunchUrl` follow-up, so an app launch with `--launch-env` succeeds but its URL follow-up is rejected as a configured bare URL open. Clear `launchEnvironment` in the follow-up execution, matching the Apple lifecycle path.</violation>
</file>

<file name="packages/kernel/src/redaction.ts">

<violation number="1" location="packages/kernel/src/redaction.ts:2">
P3: The key-level match discards the entire `launchEnvironment` map (and the whole `launchEnvironmentEntries` array) as `[REDACTED]`, hiding benign keys and values (e.g., `MODE: 'test'`) that are the most useful diagnostic context. The stated intent is to redact values, and only values are sensitive. Redact each map value (via redactString) while preserving keys, e.g. `launchEnvironment: { _XCAppClipURL: '[REDACTED]' }`, to keep diagnostics actionable without leaking secrets.</violation>

<violation number="2" location="packages/kernel/src/redaction.ts:2">
P2: Only SENSITIVE_KEY_RE was extended; SENSITIVE_ASSIGNMENT_RE (free-text `KEY=VALUE` redaction) was not, so launch-environment values leak when they appear inside unstructured strings rather than under the `launchEnvironment`/`launchEnvironmentEntries` keys. Verified: `redactDiagnosticData({ message: 'invalid --launch-env entry _XCAppClipURL=' + secret })` and `argv: ['--launch-env', 'SOME_VAR=' + secret]` both return the secret verbatim (URL values get only their query dropped; non-URL values unchanged). The flag is documented as "values are treated as sensitive", and the `SIMCTL_CHILD_`-prefixed entries produced at the iOS Simulator host boundary benefit from this only when the key itself contains a sensitive keyword. If any error message, argv echo, or recorded command line carries these entries, the secret crosses the diagnostic boundary. Redact at the source by passing each raw entry/value through redaction (or registering them in host-kit's scope-sensitive-values set) before they are embedded in strings.</violation>
</file>

<file name="website/docs/docs/commands.md">

<violation number="1" location="website/docs/docs/commands.md:83">
P2: This cross-platform statement incorrectly says bare URL opens cannot accept launch arguments: Android forwards `--launch-args` to `am start` for bare deep links. Qualify the restriction to iOS Simulator, where `simctl openurl` is used.</violation>
</file>

<file name="src/commands/management/app.ts">

<violation number="1" location="src/commands/management/app.ts:36">
P2: NUL-containing values pass this string-only check, then make Node reject the simulator process spawn as `COMMAND_FAILED` instead of reporting invalid input. Reject `\0` in values in both `readLaunchEnvironment` and `parseLaunchEnvironmentEntries`.</violation>

<violation number="2" location="src/commands/management/app.ts:39">
P2: `__proto__` is accepted but assignment to these normal objects invokes the inherited setter and silently drops the requested environment variable. Build both maps with `Object.create(null)` or define each property explicitly so every accepted key is preserved.</violation>
</file>

<file name="packages/platform-apple/src/core/__tests__/apps.test.ts">

<violation number="1" location="packages/platform-apple/src/core/__tests__/apps.test.ts:343">
P3: The new launchEnvironment tests only cover the plain launch path. Two other code paths in openIosApp also carry the env patch and are untested: `launchIosSimulatorApp` routes through `runIosSimulatorConsoleLaunch` when `launchConsole` is set (app-launch.ts:292-297, which also forwards envPatch at line 380), and the launch-before-openurl path when `url` is given with launchEnvironment (app-launch.ts:91, `shouldLaunchAppBeforeUrl` includes `launchEnvironment !== undefined`, forwarding it at line 97). A regression in env forwarding through either path would pass the suite silently. Add tests for `launchConsole` + `launchEnvironment` and for `url` + `launchEnvironment` on the simulator, mirroring the existing `launchArgs` + `url` tests.</violation>
</file>

<file name="test/integration/provider-scenarios/ios-lifecycle.test.ts">

<violation number="1" location="test/integration/provider-scenarios/ios-lifecycle.test.ts:71">
P3: This redaction assertion cannot fail in this fixture, so it does not prove the claimed "redact launch environment values from diagnostics" behavior. The env value reaches the app only through `envPatch` (subprocess environment, never argv; see `iosSimulatorLaunchEnvironmentPatch` in packages/platform-apple/src/core/app-launch.ts, which applies `SIMCTL_CHILD_*` via envPatch), the scripted appleTool returns empty stderr, and the open result data echoes only `appBundleId`-style fields. `provider-test` would appear in `response.json` only if the daemon echoed launchEnvironment back, which nothing in this flow does — the assertion passes even without redaction. Either assert against a channel that actually carries the value (e.g., serialized tool diagnostics/`appleTool.calls`), or delete the assertion and rely on the diagnostics-layer redaction tests.</violation>

<violation number="2" location="test/integration/provider-scenarios/ios-lifecycle.test.ts:72">
P3: The redaction proof checks only the `MODE` value (`provider-test`); the other sensitive value in this step, `_XCAppClipURL: 'https://example.com/clip?id=42'`, is never asserted absent. If diagnostics leak only the App Clip URL, this test still passes despite the PR's claim that launch-environment values are redacted. Assert both values are absent from `response.json`.</violation>
</file>

<file name="packages/platform-apple/src/core/app-launch.ts">

<violation number="1" location="packages/platform-apple/src/core/app-launch.ts:75">
P2: This guard accepts non-iOS simulator leaves such as tvOS and visionOS, so `--launch-env` can launch them instead of returning the documented unsupported-operation error. Gate the option on the resolved Apple OS being `ios` as the shared lifecycle contract does.</violation>

<violation number="2" location="packages/platform-apple/src/core/app-launch.ts:75">
P3: This early guard already throws for every non-simulator device before the URL, deep-link, and direct-app branches, so the three identical `--launch-env is supported only for iOS Simulator` blocks added inside those branches are unreachable dead code. They also duplicate the same four-line message four times, so future message edits can drift. Remove the three branch-local blocks (the explicit-url, deep-link, and direct-app non-simulator checks) and keep the single early guard.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

): void {
if (
launchEnvironment !== undefined &&
!(resolveDeviceAppleOs(device) === 'ios' && device.kind === 'simulator')

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: assertLaunchEnvironmentSupport gates on resolveDeviceAppleOs(device) === 'ios', which excludes iPadOS simulators (appleOs: 'ipados'). iPad Simulator launches are iOS Simulator app launches — discovery classifies iPad models as 'ipados' (inventory-classification.ts:62) and openIosApp accepts any kind === 'simulator' — so --launch-env is rejected with "supported only for iOS Simulator" on exactly the devices the platform layer supports. Use isIosFamily(device) && device.kind === 'simulator' to match the launchConsole gate (also resolveDeviceAppleOs falls back to 'ios' for non-Apple legacy records, making the check both too narrow and accidentally permissive).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/contracts/src/application-lifecycle-interaction.ts, line 209:

<comment>`assertLaunchEnvironmentSupport` gates on `resolveDeviceAppleOs(device) === 'ios'`, which excludes iPadOS simulators (`appleOs: 'ipados'`). iPad Simulator launches are iOS Simulator app launches — discovery classifies iPad models as `'ipados'` (inventory-classification.ts:62) and `openIosApp` accepts any `kind === 'simulator'` — so `--launch-env` is rejected with "supported only for iOS Simulator" on exactly the devices the platform layer supports. Use `isIosFamily(device) && device.kind === 'simulator'` to match the `launchConsole` gate (also `resolveDeviceAppleOs` falls back to `'ios'` for non-Apple legacy records, making the check both too narrow and accidentally permissive).</comment>

<file context>
@@ -194,6 +197,22 @@ function assertOpenDeviceSupport(
+): void {
+  if (
+    launchEnvironment !== undefined &&
+    !(resolveDeviceAppleOs(device) === 'ios' && device.kind === 'simulator')
+  ) {
+    throw new AppError(
</file context>
Fix with cubic

@@ -1,5 +1,5 @@
const SENSITIVE_KEY_RE =
/(token|secret|password|authorization|cookie|api[_-]?key|access[_-]?key|private[_-]?key|user[_-]?code|device[_-]?code|refresh[_-]?credential)/i;
/(token|secret|password|authorization|cookie|api[_-]?key|access[_-]?key|private[_-]?key|user[_-]?code|device[_-]?code|refresh[_-]?credential|launch[_-]?environment)/i;

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Only SENSITIVE_KEY_RE was extended; SENSITIVE_ASSIGNMENT_RE (free-text KEY=VALUE redaction) was not, so launch-environment values leak when they appear inside unstructured strings rather than under the launchEnvironment/launchEnvironmentEntries keys. Verified: redactDiagnosticData({ message: 'invalid --launch-env entry _XCAppClipURL=' + secret }) and argv: ['--launch-env', 'SOME_VAR=' + secret] both return the secret verbatim (URL values get only their query dropped; non-URL values unchanged). The flag is documented as "values are treated as sensitive", and the SIMCTL_CHILD_-prefixed entries produced at the iOS Simulator host boundary benefit from this only when the key itself contains a sensitive keyword. If any error message, argv echo, or recorded command line carries these entries, the secret crosses the diagnostic boundary. Redact at the source by passing each raw entry/value through redaction (or registering them in host-kit's scope-sensitive-values set) before they are embedded in strings.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/kernel/src/redaction.ts, line 2:

<comment>Only SENSITIVE_KEY_RE was extended; SENSITIVE_ASSIGNMENT_RE (free-text `KEY=VALUE` redaction) was not, so launch-environment values leak when they appear inside unstructured strings rather than under the `launchEnvironment`/`launchEnvironmentEntries` keys. Verified: `redactDiagnosticData({ message: 'invalid --launch-env entry _XCAppClipURL=' + secret })` and `argv: ['--launch-env', 'SOME_VAR=' + secret]` both return the secret verbatim (URL values get only their query dropped; non-URL values unchanged). The flag is documented as "values are treated as sensitive", and the `SIMCTL_CHILD_`-prefixed entries produced at the iOS Simulator host boundary benefit from this only when the key itself contains a sensitive keyword. If any error message, argv echo, or recorded command line carries these entries, the secret crosses the diagnostic boundary. Redact at the source by passing each raw entry/value through redaction (or registering them in host-kit's scope-sensitive-values set) before they are embedded in strings.</comment>

<file context>
@@ -1,5 +1,5 @@
 const SENSITIVE_KEY_RE =
-  /(token|secret|password|authorization|cookie|api[_-]?key|access[_-]?key|private[_-]?key|user[_-]?code|device[_-]?code|refresh[_-]?credential)/i;
+  /(token|secret|password|authorization|cookie|api[_-]?key|access[_-]?key|private[_-]?key|user[_-]?code|device[_-]?code|refresh[_-]?credential|launch[_-]?environment)/i;
 const SECRET_TOKEN_RE =
   /\b(?:bearer\s+[a-z0-9._-]+|adc_(?:agent|live|refresh|cli)_[a-z0-9._-]+)\b/gi;
</file context>
Fix with cubic

- `open <app> --launch-console <path>` captures launch-time stdout/stderr for direct iOS simulator app launches. It is not valid for URL opens or
non-simulator targets.
- `open <app> --launch-env KEY=VALUE` sets one child-process environment variable for an iOS Simulator app launch. Repeat the flag for multiple variables. Use child names such as `_XCAppClipURL`; do not add the `SIMCTL_CHILD_` transport prefix. Duplicate or empty keys are rejected, values are redacted from diagnostics, and physical iOS devices, macOS, and other platforms report `UNSUPPORTED_OPERATION`.
- Launch environment can be combined with repeatable `--launch-args`. With `open <app> <url>`, agent-device launches the app with both settings before opening the URL. A bare `open <url>` cannot accept launch arguments or environment because `simctl openurl` does not configure an app process.

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This cross-platform statement incorrectly says bare URL opens cannot accept launch arguments: Android forwards --launch-args to am start for bare deep links. Qualify the restriction to iOS Simulator, where simctl openurl is used.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At website/docs/docs/commands.md, line 83:

<comment>This cross-platform statement incorrectly says bare URL opens cannot accept launch arguments: Android forwards `--launch-args` to `am start` for bare deep links. Qualify the restriction to iOS Simulator, where `simctl openurl` is used.</comment>

<file context>
@@ -79,6 +79,8 @@ agent-device fold open
 - `open <app> --launch-console <path>` captures launch-time stdout/stderr for direct iOS simulator app launches. It is not valid for URL opens or
   non-simulator targets.
+- `open <app> --launch-env KEY=VALUE` sets one child-process environment variable for an iOS Simulator app launch. Repeat the flag for multiple variables. Use child names such as `_XCAppClipURL`; do not add the `SIMCTL_CHILD_` transport prefix. Duplicate or empty keys are rejected, values are redacted from diagnostics, and physical iOS devices, macOS, and other platforms report `UNSUPPORTED_OPERATION`.
+- Launch environment can be combined with repeatable `--launch-args`. With `open <app> <url>`, agent-device launches the app with both settings before opening the URL. A bare `open <url>` cannot accept launch arguments or environment because `simctl openurl` does not configure an app process.
 - `open --platform macos --surface app|frontmost-app|desktop|menubar` selects the macOS session surface explicitly. `app` is the default when an app argument is provided.
 - `back` now defaults to app-owned back navigation. On Apple targets that means visible in-app back UI only. On Android this currently maps to the same back keyevent because Android routes in-app back through that platform event.
</file context>
Fix with cubic

if (typeof entry !== 'string') {
throw new AppError('INVALID_ARGS', `launchEnvironment value for ${key} must be a string.`);
}
result[key] = entry;

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: __proto__ is accepted but assignment to these normal objects invokes the inherited setter and silently drops the requested environment variable. Build both maps with Object.create(null) or define each property explicitly so every accepted key is preserved.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/commands/management/app.ts, line 39:

<comment>`__proto__` is accepted but assignment to these normal objects invokes the inherited setter and silently drops the requested environment variable. Build both maps with `Object.create(null)` or define each property explicitly so every accepted key is preserved.</comment>

<file context>
@@ -21,6 +21,58 @@ import { defineCommandFacet } from '../family/types.ts';
+    if (typeof entry !== 'string') {
+      throw new AppError('INVALID_ARGS', `launchEnvironment value for ${key} must be a string.`);
+    }
+    result[key] = entry;
+  }
+  return Object.freeze(result);
</file context>
Fix with cubic

const result: Record<string, string> = {};
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
assertLaunchEnvironmentKey(key);
if (typeof entry !== 'string') {

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: NUL-containing values pass this string-only check, then make Node reject the simulator process spawn as COMMAND_FAILED instead of reporting invalid input. Reject \0 in values in both readLaunchEnvironment and parseLaunchEnvironmentEntries.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/commands/management/app.ts, line 36:

<comment>NUL-containing values pass this string-only check, then make Node reject the simulator process spawn as `COMMAND_FAILED` instead of reporting invalid input. Reject `\0` in values in both `readLaunchEnvironment` and `parseLaunchEnvironmentEntries`.</comment>

<file context>
@@ -21,6 +21,58 @@ import { defineCommandFacet } from '../family/types.ts';
+  const result: Record<string, string> = {};
+  for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
+    assertLaunchEnvironmentKey(key);
+    if (typeof entry !== 'string') {
+      throw new AppError('INVALID_ARGS', `launchEnvironment value for ${key} must be a string.`);
+    }
</file context>
Fix with cubic

@@ -1,5 +1,5 @@
const SENSITIVE_KEY_RE =
/(token|secret|password|authorization|cookie|api[_-]?key|access[_-]?key|private[_-]?key|user[_-]?code|device[_-]?code|refresh[_-]?credential)/i;
/(token|secret|password|authorization|cookie|api[_-]?key|access[_-]?key|private[_-]?key|user[_-]?code|device[_-]?code|refresh[_-]?credential|launch[_-]?environment)/i;

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The key-level match discards the entire launchEnvironment map (and the whole launchEnvironmentEntries array) as [REDACTED], hiding benign keys and values (e.g., MODE: 'test') that are the most useful diagnostic context. The stated intent is to redact values, and only values are sensitive. Redact each map value (via redactString) while preserving keys, e.g. launchEnvironment: { _XCAppClipURL: '[REDACTED]' }, to keep diagnostics actionable without leaking secrets.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/kernel/src/redaction.ts, line 2:

<comment>The key-level match discards the entire `launchEnvironment` map (and the whole `launchEnvironmentEntries` array) as `[REDACTED]`, hiding benign keys and values (e.g., `MODE: 'test'`) that are the most useful diagnostic context. The stated intent is to redact values, and only values are sensitive. Redact each map value (via redactString) while preserving keys, e.g. `launchEnvironment: { _XCAppClipURL: '[REDACTED]' }`, to keep diagnostics actionable without leaking secrets.</comment>

<file context>
@@ -1,5 +1,5 @@
 const SENSITIVE_KEY_RE =
-  /(token|secret|password|authorization|cookie|api[_-]?key|access[_-]?key|private[_-]?key|user[_-]?code|device[_-]?code|refresh[_-]?credential)/i;
+  /(token|secret|password|authorization|cookie|api[_-]?key|access[_-]?key|private[_-]?key|user[_-]?code|device[_-]?code|refresh[_-]?credential|launch[_-]?environment)/i;
 const SECRET_TOKEN_RE =
   /\b(?:bearer\s+[a-z0-9._-]+|adc_(?:agent|live|refresh|cli)_[a-z0-9._-]+)\b/gi;
</file context>
Fix with cubic

);
});

test('openIosApp translates launch environment keys for the iOS simulator child process', async () => {

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The new launchEnvironment tests only cover the plain launch path. Two other code paths in openIosApp also carry the env patch and are untested: launchIosSimulatorApp routes through runIosSimulatorConsoleLaunch when launchConsole is set (app-launch.ts:292-297, which also forwards envPatch at line 380), and the launch-before-openurl path when url is given with launchEnvironment (app-launch.ts:91, shouldLaunchAppBeforeUrl includes launchEnvironment !== undefined, forwarding it at line 97). A regression in env forwarding through either path would pass the suite silently. Add tests for launchConsole + launchEnvironment and for url + launchEnvironment on the simulator, mirroring the existing launchArgs + url tests.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/platform-apple/src/core/__tests__/apps.test.ts, line 343:

<comment>The new launchEnvironment tests only cover the plain launch path. Two other code paths in openIosApp also carry the env patch and are untested: `launchIosSimulatorApp` routes through `runIosSimulatorConsoleLaunch` when `launchConsole` is set (app-launch.ts:292-297, which also forwards envPatch at line 380), and the launch-before-openurl path when `url` is given with launchEnvironment (app-launch.ts:91, `shouldLaunchAppBeforeUrl` includes `launchEnvironment !== undefined`, forwarding it at line 97). A regression in env forwarding through either path would pass the suite silently. Add tests for `launchConsole` + `launchEnvironment` and for `url` + `launchEnvironment` on the simulator, mirroring the existing `launchArgs` + `url` tests.</comment>

<file context>
@@ -340,6 +340,28 @@ test('openIosApp emits a clean simctl launch when launchArgs is an empty array',
   );
 });
 
+test('openIosApp translates launch environment keys for the iOS simulator child process', async () => {
+  mockEnsureBootedSimulator.mockResolvedValue();
+  mockRunCmd.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 });
</file context>
Fix with cubic

},
},
expectData: { appBundleId: 'com.apple.Preferences' },
assert: (response) => {

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This redaction assertion cannot fail in this fixture, so it does not prove the claimed "redact launch environment values from diagnostics" behavior. The env value reaches the app only through envPatch (subprocess environment, never argv; see iosSimulatorLaunchEnvironmentPatch in packages/platform-apple/src/core/app-launch.ts, which applies SIMCTL_CHILD_* via envPatch), the scripted appleTool returns empty stderr, and the open result data echoes only appBundleId-style fields. provider-test would appear in response.json only if the daemon echoed launchEnvironment back, which nothing in this flow does — the assertion passes even without redaction. Either assert against a channel that actually carries the value (e.g., serialized tool diagnostics/appleTool.calls), or delete the assertion and rely on the diagnostics-layer redaction tests.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/integration/provider-scenarios/ios-lifecycle.test.ts, line 71:

<comment>This redaction assertion cannot fail in this fixture, so it does not prove the claimed "redact launch environment values from diagnostics" behavior. The env value reaches the app only through `envPatch` (subprocess environment, never argv; see `iosSimulatorLaunchEnvironmentPatch` in packages/platform-apple/src/core/app-launch.ts, which applies `SIMCTL_CHILD_*` via envPatch), the scripted appleTool returns empty stderr, and the open result data echoes only `appBundleId`-style fields. `provider-test` would appear in `response.json` only if the daemon echoed launchEnvironment back, which nothing in this flow does — the assertion passes even without redaction. Either assert against a channel that actually carries the value (e.g., serialized tool diagnostics/`appleTool.calls`), or delete the assertion and rely on the diagnostics-layer redaction tests.</comment>

<file context>
@@ -49,6 +56,22 @@ test('Provider-backed integration iOS Settings flow uses scripted simctl and run
+            },
+          },
+          expectData: { appBundleId: 'com.apple.Preferences' },
+          assert: (response) => {
+            assert.doesNotMatch(JSON.stringify(response.json), /provider-test/);
+          },
</file context>
Fix with cubic

await openMacOsApp(device, app, options);
return;
}
if (launchEnvironment !== undefined && device.kind !== 'simulator') {

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This early guard already throws for every non-simulator device before the URL, deep-link, and direct-app branches, so the three identical --launch-env is supported only for iOS Simulator blocks added inside those branches are unreachable dead code. They also duplicate the same four-line message four times, so future message edits can drift. Remove the three branch-local blocks (the explicit-url, deep-link, and direct-app non-simulator checks) and keep the single early guard.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/platform-apple/src/core/app-launch.ts, line 75:

<comment>This early guard already throws for every non-simulator device before the URL, deep-link, and direct-app branches, so the three identical `--launch-env is supported only for iOS Simulator` blocks added inside those branches are unreachable dead code. They also duplicate the same four-line message four times, so future message edits can drift. Remove the three branch-local blocks (the explicit-url, deep-link, and direct-app non-simulator checks) and keep the single early guard.</comment>

<file context>
@@ -64,6 +72,12 @@ export async function openIosApp(
     await openMacOsApp(device, app, options);
     return;
   }
+  if (launchEnvironment !== undefined && device.kind !== 'simulator') {
+    throw new AppError(
+      'UNSUPPORTED_OPERATION',
</file context>
Fix with cubic

},
expectData: { appBundleId: 'com.apple.Preferences' },
assert: (response) => {
assert.doesNotMatch(JSON.stringify(response.json), /provider-test/);

@cubic-dev-ai cubic-dev-ai Bot Sep 27, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The redaction proof checks only the MODE value (provider-test); the other sensitive value in this step, _XCAppClipURL: 'https://example.com/clip?id=42', is never asserted absent. If diagnostics leak only the App Clip URL, this test still passes despite the PR's claim that launch-environment values are redacted. Assert both values are absent from response.json.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/integration/provider-scenarios/ios-lifecycle.test.ts, line 72:

<comment>The redaction proof checks only the `MODE` value (`provider-test`); the other sensitive value in this step, `_XCAppClipURL: 'https://example.com/clip?id=42'`, is never asserted absent. If diagnostics leak only the App Clip URL, this test still passes despite the PR's claim that launch-environment values are redacted. Assert both values are absent from `response.json`.</comment>

<file context>
@@ -49,6 +56,22 @@ test('Provider-backed integration iOS Settings flow uses scripted simctl and run
+          },
+          expectData: { appBundleId: 'com.apple.Preferences' },
+          assert: (response) => {
+            assert.doesNotMatch(JSON.stringify(response.json), /provider-test/);
+          },
+        },
</file context>
Suggested change
assert.doesNotMatch(JSON.stringify(response.json), /provider-test/);
assert.doesNotMatch(
JSON.stringify(response.json),
/provider-test|https:\/\/example\.com\/clip/,
);
Fix with cubic

@thymikee

Copy link
Copy Markdown
Member

Reviewed at d11abc3. This has findings that need to be fixed before merge.

assertLaunchEnvironmentSupport (packages/contracts/src/application-lifecycle-interaction.ts#L208) admits a device only when resolveDeviceAppleOs(device) === 'ios', but discovery classifies iPad simulators as appleOs 'ipados' (packages/platform-apple/src/inventory-classification.ts#L62). An iPad simulator user hits UNSUPPORTED_OPERATION even though the simctl path serves iPad simulators fine. The kernel already has the right predicate for this: isHandheldAppleSimulator (packages/kernel/src/device.ts#L120-L126). The openIosApp guard (app-launch.ts#L75) only checks kind === 'simulator' and would admit tvOS and visionOS simulators, so the two gates disagree with each other. One predicate should decide which leaf supports launch environment: use isHandheldAppleSimulator in both assertLaunchEnvironmentSupport and the openIosApp guard, and add an iPad simulator success row to the contracts test table.

The gate at application-lifecycle-interaction.ts#L325 reads only device facts. A Limrun iOS device is built as {platform:'apple', appleOs:'ios', kind:'simulator'} (packages/provider-limrun/src/device.ts#L22-L26), so it passes the gate. openDirectApplication then calls the Limrun interactor, whose open(app, {url}) (packages/provider-limrun/src/ios.ts#L184) never reads launchEnvironment, so the app launches without it and the request still returns success. A caller asking for launch environment on a Limrun cloud iOS simulator gets a silent success with the environment missing, even though the PR body and docs say this case fails closed. Launch environment should be accepted only by the owner that applies it: reject a defined launchEnvironment with UNSUPPORTED_OPERATION in openDirectApplication/bindDirectApplicationLifecycle, which covers every direct and provider owner in one place, and add a Limrun lifecycle test that expects the typed error.

This PR changes a device-facing simctl launch path (packages/platform-apple/src/core/app-launch.ts#L292), and the PR body says live simulator lanes were not claimed; every test here stubs runXcrun or the simctl provider, so nothing shows that a real simctl actually delivers the variables to the app process. Can we get a live run on a booted local iPhone simulator, and after f1 an iPad simulator too: agent-device open <bundle> --platform ios --launch-env AD_PROBE=<nonce> --launch-console <path> --debug, plus open <bundle> <url> --launch-env ...? Proof would be the app observing the nonce in ProcessInfo.environment, and the nonce absent from the request diagnostics ndjson.

Not blocking, can be taken or left: recording with --launch-env drops the flag silently on replay since sanitizeFlags only copies recorded keys (src/daemon/session-action-recorder.ts#L365); the three guards in app-launch.ts#L115/138/170 are unreachable now that line 75 throws first, and the message literal is duplicated instead of using one constant like LAUNCH_CONSOLE_IOS_SIMULATOR_ONLY_MESSAGE; three separate sites (Apple lifecycle.ts#L223, Android lifecycle.ts#L141-155, contracts openDirectApplication#L351-356) each keep their own launch-only field strip list and only the Apple one learned about launchEnvironment; the ios-world.ts#L1127 test helper only checks two hard-coded keys and the doesNotMatch assertion can't fail since open responses never echo flags, and app.test.ts checks .toThrow(regex) without asserting the INVALID_ARGS code; app.ts#L36 builds env objects as plain {} so a __proto__ key is silently dropped and a NUL byte in a value fails at spawn instead of INVALID_ARGS; and command-flags.ts#L28 excludes launchEnvironmentEntries inline instead of via DaemonExcludedCliFlag, adding a one-line subpath for something that could be a plain alias.

Could this be simpler by admitting launch environment only in the owner that applies it: the Apple lifecycle's local handheld-simulator path using isHandheldAppleSimulator, plus one rejection in openDirectApplication? With that, the device-fact gate in contracts and the four guards in openIosApp could go, along with the one-line contracts subpath, collapsing to one message constant. The host-kit envPatch field looks justified on its own, since without it a provider would receive the daemon's whole environment. Nothing needs to change first, since bindDirectApplicationLifecycle and isHandheldAppleSimulator both already exist.

No live simulator run was done, so whether simctl actually delivers SIMCTL_CHILD_ vars to the app, and whether it restarts an already-running app without --relaunch, is unverified (launchArgs has this same open question). I did not trace whether MCP batch steps carrying launchEnvironment go through readLaunchEnvironment validation. I found no diagnostic that emits req.flags, so the redaction change looks defensive, but I could not enumerate every structured diagnostic producer. The behavior of out-of-tree appleToolProvider implementations that receive envPatch but don't handle it is unknown.

CI shows one check and it's green, but this is a cross-repo PR and the Apple runner and live simulator lanes that would exercise app-launch.ts did not run.

The launch-env gate needs to become owner-scoped: use isHandheldAppleSimulator so iPad simulators pass, and reject the environment in openDirectApplication so Limrun and other provider owners fail closed, then back it with a live simulator run showing the app actually receives the variable.

@thymikee

Copy link
Copy Markdown
Member

One direction note on top of the review above. The overall shape looks right: launchEnvironment sits next to launchArgs and goes through the same admission, so we'd like to keep this feature.

Could you add one or two sentences to the PR body on why environment variables are needed here, rather than launch arguments? --launch-args already reaches the iOS app process, and -Key value arguments land in UserDefaults, which covers many feature-flag and test-mode cases. If your app (or the App Clip flow) reads ProcessInfo.processInfo.environment, that is a good reason, and it matches how XCUITest users pass config through launchEnvironment.

For Android, please don't add a second path in this PR. --launch-args already forwards to am start, so intent extras (--es KEY VALUE) cover that case. A short line in the --launch-env help and docs that points Android users to --launch-args would be enough. It should keep the failure clear instead of a bare "iOS Simulator only".

This branch has not been deployed

No deployments
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