From 3f63bd48a76b05876d3602f6daa936509c362cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 30 Apr 2026 10:38:50 -0400 Subject: [PATCH] fix: clarify remote device devtools guidance --- skills/react-devtools/SKILL.md | 6 +- src/__tests__/cli-react-devtools.test.ts | 71 +++++++++++-------- src/cli/commands/react-devtools.ts | 15 +++- src/utils/__tests__/args.test.ts | 22 ++++++ src/utils/command-schema.ts | 34 +++++---- .../suites/agent-device-smoke-suite.ts | 40 +++++++++++ website/docs/docs/commands.md | 20 +++--- 7 files changed, 154 insertions(+), 54 deletions(-) diff --git a/skills/react-devtools/SKILL.md b/skills/react-devtools/SKILL.md index b58539f8..9ad2cec7 100644 --- a/skills/react-devtools/SKILL.md +++ b/skills/react-devtools/SKILL.md @@ -11,7 +11,7 @@ Router for React Native internals. Private setup before using this skill: agent-device --version ``` -Require `agent-device >= 0.14.0`; older CLIs lack these help topics. If older, run `npm install -g agent-device@latest`, recheck, then continue. If you cannot upgrade, stop and tell the user. Do not include version/upgrade commands in final plans. +Require `agent-device >= 0.14.2`; older CLIs lack the remote/cloud React DevTools guidance. If older, run `npm install -g agent-device@latest`, recheck, then continue. If you cannot upgrade, stop and tell the user. Do not include version/upgrade commands in final plans. Read current CLI guidance: @@ -21,6 +21,8 @@ agent-device help react-devtools Use `agent-device react-devtools ...` for component tree, props, state, hooks, render ownership, performance profiling, slow components, or rerenders. It dynamically runs pinned `agent-react-devtools@0.4.0`. Use normal `agent-device` commands for visible UI, refs, screenshots, logs, network, or device-level perf. +Remote sessions: Android cloud devices work through the CLI-managed service tunnel when the remote profile includes DevTools tunnel settings. Remote iOS cloud devices are not available for React DevTools in the current cloud service; use normal device evidence or local iOS when React internals are required. + Core loop: ```bash @@ -36,4 +38,4 @@ agent-device react-devtools profile rerenders --limit 5 Rules: -Keep reads bounded with `--depth`/`find`, treat `@c` refs as reload-local, profile only the investigated interaction, and run the same command in remote Android sessions; the CLI manages the needed local service tunnel. +Keep reads bounded with `--depth`/`find`, treat `@c` refs as reload-local, profile only the investigated interaction, and use the same command in supported remote Android sessions; the CLI manages the needed local service tunnel. diff --git a/src/__tests__/cli-react-devtools.test.ts b/src/__tests__/cli-react-devtools.test.ts index aa6c6af8..208291f8 100644 --- a/src/__tests__/cli-react-devtools.test.ts +++ b/src/__tests__/cli-react-devtools.test.ts @@ -1,6 +1,6 @@ +import assert from 'node:assert/strict'; import fs from 'node:fs'; import { afterEach, test, vi } from 'vitest'; -import assert from 'node:assert/strict'; vi.mock('../utils/exec.ts', () => ({ runCmdStreaming: vi.fn(), @@ -26,16 +26,15 @@ type ReactDevtoolsOptions = NonNullable; const remoteBridgeScope = { - metroProxyBaseUrl: 'https://bridge.example.test', + metroProxyBaseUrl: 'https://cloud.example.test', metroBearerToken: 'token', tenant: 'tenant-1', runId: 'run-1', leaseId: 'lease-1', } as const; -const remoteBridgeBackends = [ +const remoteAndroidBridgeBackends = [ { label: 'Android', leaseBackend: 'android-instance' }, - { label: 'iOS', leaseBackend: 'ios-instance' }, ] as const; afterEach(() => { @@ -110,7 +109,7 @@ function assertRemoteCompanionStarted(env: NodeJS.ProcessEnv): void { assert.deepEqual(vi.mocked(ensureReactDevtoolsCompanion).mock.calls[0]?.[0], { projectRoot: '/tmp/project', stateDir: '/tmp/agent-device-state', - serverBaseUrl: 'https://bridge.example.test', + serverBaseUrl: 'https://cloud.example.test', bearerToken: 'token', bridgeScope: { tenantId: 'tenant-1', @@ -134,7 +133,7 @@ function assertRemoteCompanionStarted(env: NodeJS.ProcessEnv): void { }); } -for (const { label, leaseBackend } of remoteBridgeBackends) { +for (const { label, leaseBackend } of remoteAndroidBridgeBackends) { test(`react-devtools starts remote ${label} companion around passthrough command`, async () => { const env = { ...process.env }; mockRemoteCompanionSuccess(); @@ -168,25 +167,41 @@ test('react-devtools skips companion when remote bridge backend is missing', asy await runStatusWithoutCompanion(remoteBridgeScope); }); -for (const { label, leaseBackend } of remoteBridgeBackends) { - test(`react-devtools fails clearly when remote ${label} bridge scope is incomplete`, async () => { - await assert.rejects( - () => - runReactDevtoolsCommand(['status'], { - stateDir: '/tmp/agent-device-state', - session: 'default', - flags: { - leaseBackend, - metroProxyBaseUrl: 'https://bridge.example.test', - tenant: 'tenant-1', - runId: 'run-1', - leaseId: 'lease-1', - }, - }), - /react-devtools remote bridge requires metroBearerToken/, - ); - - assert.equal(vi.mocked(runCmdStreaming).mock.calls.length, 0); - assertNoRemoteCompanion(); - }); -} +test('react-devtools fails clearly for remote iOS service sessions', async () => { + await assert.rejects( + () => + runReactDevtoolsCommand(['status'], { + stateDir: '/tmp/agent-device-state', + session: 'default', + flags: { + leaseBackend: 'ios-instance', + ...remoteBridgeScope, + }, + }), + /react-devtools is not available for remote iOS devices/, + ); + + assert.equal(vi.mocked(runCmdStreaming).mock.calls.length, 0); + assertNoRemoteCompanion(); +}); + +test('react-devtools fails clearly when remote Android service tunnel scope is incomplete', async () => { + await assert.rejects( + () => + runReactDevtoolsCommand(['status'], { + stateDir: '/tmp/agent-device-state', + session: 'default', + flags: { + leaseBackend: 'android-instance', + metroProxyBaseUrl: remoteBridgeScope.metroProxyBaseUrl, + tenant: 'tenant-1', + runId: 'run-1', + leaseId: 'lease-1', + }, + }), + /react-devtools remote service tunnel requires metroBearerToken/, + ); + + assert.equal(vi.mocked(runCmdStreaming).mock.calls.length, 0); + assertNoRemoteCompanion(); +}); diff --git a/src/cli/commands/react-devtools.ts b/src/cli/commands/react-devtools.ts index 1a46df74..c89c65fd 100644 --- a/src/cli/commands/react-devtools.ts +++ b/src/cli/commands/react-devtools.ts @@ -52,6 +52,10 @@ function isRemoteBridgeBackend(leaseBackend: CliFlags['leaseBackend']): boolean return leaseBackend === 'android-instance' || leaseBackend === 'ios-instance'; } +function isRemoteIosBridgeBackend(leaseBackend: CliFlags['leaseBackend']): boolean { + return leaseBackend === 'ios-instance'; +} + function readRemoteBridgeField( missing: string[], field: string, @@ -66,6 +70,15 @@ function resolveRemoteBridgeConfig( flags: ReactDevtoolsCommandOptions['flags'], ): RemoteBridgeConfig | null { if (!flags?.metroProxyBaseUrl || !isRemoteBridgeBackend(flags.leaseBackend)) return null; + if (isRemoteIosBridgeBackend(flags.leaseBackend)) { + throw new AppError( + 'UNSUPPORTED_OPERATION', + 'react-devtools is not available for remote iOS devices in the current cloud service.', + { + leaseBackend: flags.leaseBackend, + }, + ); + } const missing: string[] = []; const config = { serverBaseUrl: readRemoteBridgeField(missing, 'metroProxyBaseUrl', flags.metroProxyBaseUrl), @@ -77,7 +90,7 @@ function resolveRemoteBridgeConfig( if (missing.length > 0) { throw new AppError( 'INVALID_ARGS', - `react-devtools remote bridge requires ${missing.join(', ')}.`, + `react-devtools remote service tunnel requires ${missing.join(', ')}.`, { missing }, ); } diff --git a/src/utils/__tests__/args.test.ts b/src/utils/__tests__/args.test.ts index fd8f86b2..7b03e861 100644 --- a/src/utils/__tests__/args.test.ts +++ b/src/utils/__tests__/args.test.ts @@ -872,6 +872,8 @@ test('usageForCommand resolves workflow help topic', () => { assert.match(help, /if no URL is provided but a target\/app name is provided, open that target/); assert.match(help, /do not split clear\/restart/); assert.match(help, /do not write network log headers/); + assert.match(help, /Remote profiles own device lifecycle/); + assert.match(help, /Runner-backed commands can be unavailable for remote devices/); assert.match(help, /agent-device open exp:\/\/127\.0\.0\.1:8081 --platform ios/); assert.match(help, /agent-device open "Expo Go" exp:\/\/127\.0\.0\.1:8081 --platform ios/); assert.match(help, /direct URL open can report success while leaving the runner\/shell focused/); @@ -902,6 +904,10 @@ test('usageForCommand resolves remote help topic', () => { assert.match(help, /Script flow, per-command config/); assert.match(help, /same --remote-config to every operational command/); assert.match(help, /install-from-source --github-actions-artifact org\/repo:artifact/); + assert.match(help, /Do not run boot or ensure-simulator when a remote\/cloud service/); + assert.match(help, /alert and iOS keyboard dismiss can be unavailable remotely/); + assert.match(help, /For remote Android React DevTools, run agent-device react-devtools normally/); + assert.match(help, /Remote iOS is not available for React DevTools/); }); test('usageForCommand resolves macos help topic', () => { @@ -948,6 +954,8 @@ test('usageForCommand resolves react-devtools help topic', () => { assert.match(help, /@c refs reset after reload\/remount/); assert.match(help, /isolated --state-dir/); assert.match(help, /local service tunnel/); + assert.match(help, /Remote Android sessions run normally through agent-device react-devtools/); + assert.match(help, /Remote iOS is not available for React DevTools/); }); test('apps defaults to --all filter and allows overrides', () => { @@ -1243,6 +1251,20 @@ test('keyboard command usage is documented', () => { if (help === null) throw new Error('Expected command help text'); assert.match(help, /keyboard \[status\|get\|dismiss\]/); assert.match(help, /Inspect Android keyboard visibility\/type or dismiss the device keyboard/); + assert.match(help, /runner-backed paths may be unavailable for remote devices/); +}); + +test('remote-inapplicable command usage is marked', () => { + const bootHelp = usageForCommand('boot'); + const ensureHelp = usageForCommand('ensure-simulator'); + const alertHelp = usageForCommand('alert'); + if (bootHelp === null || ensureHelp === null || alertHelp === null) { + throw new Error('Expected command help text'); + } + + assert.match(bootHelp, /not for remote\/cloud device profiles/); + assert.match(ensureHelp, /not for remote\/cloud device profiles/); + assert.match(alertHelp, /may be unavailable for remote devices/); }); test('rotate command usage is documented', () => { diff --git a/src/utils/command-schema.ts b/src/utils/command-schema.ts index 5cd14a23..40b33111 100644 --- a/src/utils/command-schema.ts +++ b/src/utils/command-schema.ts @@ -212,7 +212,7 @@ const ENVIRONMENT_LINES = [ }, { label: 'AGENT_DEVICE_CLOUD_BASE_URL', - description: 'Bridge/control-plane API origin for cloud auth and /api-keys', + description: 'Cloud/control-plane API origin for auth and /api-keys', }, ] as const; @@ -333,7 +333,9 @@ Validation and evidence: Android animations: settings animations off/on, not animations disable/restore. Debug logs: logs clear --restart, logs mark, reproduce, then logs path; do not split clear/restart into separate stop/start commands. Network headers: network dump --include headers; do not write network log headers. - Remote config: connect --remote-config ./remote-config.json, open, snapshot, disconnect. + Remote/cloud devices: connect --remote-config ./remote-config.json, open, snapshot, disconnect. + Remote profiles own device lifecycle. Do not plan local-only setup commands such as boot or ensure-simulator when the device is provided by a remote service; use connect/open and follow the service error if a device is unavailable. + Runner-backed commands can be unavailable for remote devices. If alert or keyboard returns unsupported/no alert remotely, inspect with snapshot -i and use visible UI controls instead of retrying the runner command. macOS menu bar: open ... --platform macos --surface menubar; snapshot -i --platform macos --surface menubar. React Native dev loop: @@ -459,7 +461,7 @@ Rules: @c refs reset after reload/remount. After reload, wait --connected and inspect again. Keep the profile window narrow; unrelated navigation makes render data noisy. For cross-platform validation with explicit device selectors, prefer isolated --state-dir and restart react-devtools between platforms. - Remote bridge sessions (Android and iOS) run normally through agent-device react-devtools; the CLI manages the needed local service tunnel. Expo support depends on the SDK's bundled React Native runtime. + Remote Android sessions run normally through agent-device react-devtools when the profile includes DevTools tunnel settings. The CLI manages the local service tunnel. Remote iOS is not available for React DevTools in the current cloud service; use normal device evidence or local iOS when React internals are required. Expo support depends on the SDK's bundled React Native runtime. Example: agent-device react-devtools status @@ -496,7 +498,9 @@ Rules: For self-contained scripts, pass the same --remote-config to every operational command, including disconnect; a preceding connect is optional but not required. For remote artifact installs, use install-from-source or install-from-source --github-actions-artifact org/repo:artifact; do not download CI artifacts locally first. After connect, let the active remote connection supply runtime hints. - For remote Android and iOS bridge React DevTools, run agent-device react-devtools normally. The CLI opens the needed local service tunnel for the DevTools daemon and cleans it up when the command exits. + Remote profiles own device boot/provisioning. Do not run boot or ensure-simulator when a remote/cloud service provides the device; start with connect/open and report the service error if no device is leased. + Runner-backed helpers such as alert and iOS keyboard dismiss can be unavailable remotely. When they report unsupported or no alert, use snapshot -i plus visible refs/selectors instead of looping on the helper. + For remote Android React DevTools, run agent-device react-devtools normally. The CLI opens the needed local service tunnel for the DevTools daemon and cleans it up when the command exits. Remote iOS is not available for React DevTools in the current cloud service. Use --debug when remote connection or transport errors need diagnostic ids and remote log hints.`, }, macos: { @@ -821,7 +825,7 @@ const FLAG_DEFINITIONS: readonly FlagDefinition[] = [ names: ['--proxy-base-url'], type: 'string', usageLabel: '--proxy-base-url ', - usageDescription: 'metro prepare: optional bridge origin for remote Metro access', + usageDescription: 'metro prepare: optional remote service origin for Metro access', }, { key: 'metroBearerToken', @@ -829,7 +833,7 @@ const FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'string', usageLabel: '--bearer-token ', usageDescription: - 'metro prepare: host bridge bearer token (prefer AGENT_DEVICE_PROXY_TOKEN or AGENT_DEVICE_METRO_BEARER_TOKEN)', + 'metro prepare: remote service bearer token (prefer AGENT_DEVICE_PROXY_TOKEN or AGENT_DEVICE_METRO_BEARER_TOKEN)', }, { key: 'metroPreparePort', @@ -868,7 +872,7 @@ const FLAG_DEFINITIONS: readonly FlagDefinition[] = [ type: 'int', min: 1, usageLabel: '--probe-timeout-ms ', - usageDescription: 'metro prepare: timeout for /status and proxy bridge calls', + usageDescription: 'metro prepare: timeout for /status and remote service calls', }, { key: 'metroRuntimeFile', @@ -1423,7 +1427,8 @@ export const GLOBAL_FLAG_KEYS = new Set([ const COMMAND_SCHEMAS: Record = { boot: { - helpDescription: 'Ensure target device/simulator is booted and ready', + helpDescription: + 'Ensure local target device/simulator is booted and ready (not for remote/cloud device profiles)', summary: 'Boot target device/simulator', positionalArgs: [], allowedFlags: ['headless'], @@ -1439,7 +1444,7 @@ const COMMAND_SCHEMAS: Record = { usageOverride: 'connect --remote-config [--tenant ] [--run-id ] [--lease-backend ] [--force] [--no-login]', helpDescription: - 'Connect to a remote daemon, authenticate when needed, and save remote session state. AGENT_DEVICE_CLOUD_BASE_URL is the bridge/control-plane API origin; use AGENT_DEVICE_DAEMON_AUTH_TOKEN=adc_live_... for CI/service-token automation.', + 'Connect to a remote daemon, authenticate when needed, and save remote session state. AGENT_DEVICE_CLOUD_BASE_URL is the cloud/control-plane API origin; use AGENT_DEVICE_DAEMON_AUTH_TOKEN=adc_live_... for CI/service-token automation.', summary: 'Connect to remote daemon', positionalArgs: [], allowedFlags: [ @@ -1542,7 +1547,8 @@ const COMMAND_SCHEMAS: Record = { allowedFlags: [...SNAPSHOT_FLAGS, 'baseline', 'threshold', 'out', 'overlayRefs'], }, 'ensure-simulator': { - helpDescription: 'Ensure an iOS simulator exists in a device set (create if missing)', + helpDescription: + 'Ensure a local iOS simulator exists in a device set (create if missing; not for remote/cloud device profiles)', summary: 'Ensure iOS simulator exists', positionalArgs: [], allowedFlags: ['runtime', 'boot', 'reuseExisting'], @@ -1606,7 +1612,8 @@ const COMMAND_SCHEMAS: Record = { }, keyboard: { usageOverride: 'keyboard [status|get|dismiss]', - helpDescription: 'Inspect Android keyboard visibility/type or dismiss the device keyboard', + helpDescription: + 'Inspect Android keyboard visibility/type or dismiss the device keyboard (runner-backed paths may be unavailable for remote devices)', summary: 'Inspect or dismiss the device keyboard', positionalArgs: ['action?'], allowedFlags: [], @@ -1622,7 +1629,7 @@ const COMMAND_SCHEMAS: Record = { usageOverride: 'react-devtools [...args]', listUsageOverride: 'react-devtools [...args]', helpDescription: - 'Run pinned agent-react-devtools commands for React Native performance profiling, component trees, props/state/hooks, and render analysis', + 'Run pinned agent-react-devtools commands for React Native performance profiling, component trees, props/state/hooks, and render analysis (remote Android supported when the profile includes DevTools tunnel settings; remote iOS unavailable in the current cloud service)', summary: 'Profile React Native performance and component renders', positionalArgs: ['args?'], allowsExtraPositionals: true, @@ -1665,7 +1672,8 @@ const COMMAND_SCHEMAS: Record = { }, alert: { usageOverride: 'alert [get|accept|dismiss|wait] [timeout]', - helpDescription: 'Inspect or handle alert (iOS simulator and macOS desktop)', + helpDescription: + 'Inspect or handle runner-backed alerts (iOS simulator and macOS desktop; may be unavailable for remote devices)', summary: 'Inspect or handle iOS/macOS alerts', positionalArgs: ['action?', 'timeout?'], allowedFlags: [], diff --git a/test/skillgym/suites/agent-device-smoke-suite.ts b/test/skillgym/suites/agent-device-smoke-suite.ts index 6b22e9ff..382026a2 100644 --- a/test/skillgym/suites/agent-device-smoke-suite.ts +++ b/test/skillgym/suites/agent-device-smoke-suite.ts @@ -1041,6 +1041,46 @@ const SKILL_GUIDANCE_CASES: TestCase[] = [ plannedCommand('react-devtools profile rerenders'), ], }), + makeCase({ + id: 'remote-cloud-avoids-local-device-setup', + contract: [ + 'Remote cloud profile path: ./remote-config.json', + 'Platform: remote Android runtime', + 'The remote service owns device boot and provisioning', + 'App id: com.agentdevice.tester', + 'Need to start the app and inspect the current screen', + ], + task: 'Plan the remote cloud startup commands without using local simulator/emulator setup commands.', + outputs: [ + plannedCommand('connect'), + /--remote-config\s+\.\/remote-config\.json/i, + plannedCommand('open'), + /com\.agentdevice\.tester/i, + plannedCommand('snapshot'), + ], + forbiddenOutputs: [plannedCommand('boot'), plannedCommand('ensure-simulator')], + }), + makeCase({ + id: 'remote-ios-cloud-react-devtools-unavailable', + contract: [ + 'Remote cloud profile is already connected', + 'Platform: remote iOS runtime', + 'React DevTools is unavailable for remote iOS in the current cloud service', + 'Need device/runtime evidence for a sluggish Catalog search interaction', + 'Search field selector: id="catalog-search"', + ], + task: 'Plan commands to gather runtime evidence for the sluggish search without starting React DevTools.', + outputs: [ + /catalog-search/i, + plannedCommandAlternatives(['perf', 'metrics']), + plannedCommandAlternatives(['snapshot', 'screenshot']), + ], + forbiddenOutputs: [ + plannedCommand('boot'), + plannedCommand('ensure-simulator'), + plannedCommand('react-devtools'), + ], + }), makeCase({ id: 'gesture-swipe-carousel', contract: [ diff --git a/website/docs/docs/commands.md b/website/docs/docs/commands.md index 8ed1a8a7..02f036cc 100644 --- a/website/docs/docs/commands.md +++ b/website/docs/docs/commands.md @@ -68,7 +68,7 @@ agent-device app-switcher - Tenant-scoped daemon runs can pass `--tenant`, `--session-isolation tenant`, `--run-id`, and `--lease-id` to enforce lease admission. - Remote daemon clients can pass `--daemon-base-url http(s)://host:port[/base-path]` to skip local daemon discovery/startup and call a remote HTTP daemon directly. - Use `--daemon-auth-token ` (or `AGENT_DEVICE_DAEMON_AUTH_TOKEN`) for explicit service/API-token automation against non-loopback remote daemon URLs; the client sends it in both the JSON-RPC request token and HTTP auth headers. -- For human cloud access, `connect --remote-config ...` refreshes a stored CLI session into a short-lived `adc_agent_...` token. If no CLI session exists, interactive shells start login automatically; CI and non-interactive shells fail with API-token setup instructions. Use `--no-login` to disable implicit login. `AGENT_DEVICE_CLOUD_BASE_URL` is the bridge/control-plane API origin; its `/api-keys` route may redirect to the dashboard for token creation. +- For human cloud access, `connect --remote-config ...` refreshes a stored CLI session into a short-lived `adc_agent_...` token. If no CLI session exists, interactive shells start login automatically; CI and non-interactive shells fail with API-token setup instructions. Use `--no-login` to disable implicit login. `AGENT_DEVICE_CLOUD_BASE_URL` is the cloud/control-plane API origin; its `/api-keys` route may redirect to the dashboard for token creation. - For remote `connect --remote-config` flows, see [Remote Metro workflow](#remote-metro-workflow). - Android React Native relaunch flows require an installed package name for `open --relaunch`; install/reinstall the APK first, then relaunch by package. `open --relaunch` is rejected because runtime hints are written through the installed app sandbox. - For Metro-backed React Native JS changes, use `metro reload` before `open --relaunch`; it mirrors pressing `r` in the Metro terminal and keeps the native process alive. @@ -590,8 +590,8 @@ agent-device react-devtools profile rerenders --limit 5 - Use it when a React Native workflow needs component hierarchy, props, state, hooks, render causes, slow components, or re-render counts. - Keep using `snapshot`, `press`, `fill`, `logs`, `network`, and `perf` for device/app runtime evidence. Use `react-devtools` for React internals. - React Native development builds can connect to the DevTools daemon on port 8097. For Android emulators or physical devices, run `adb reverse tcp:8097 tcp:8097` if the app cannot reach the host. If Metro is local, also run `adb reverse tcp:8081 tcp:8081`. -- For Android and iOS sessions connected through a remote bridge profile, `react-devtools` registers a lease-scoped companion tunnel to the sandbox-local DevTools daemon at `127.0.0.1:8097`. Android bridge profiles use the bridge-owned remote `adb reverse` mapping; iOS bridge profiles use the bridge-owned wildcard Metro host tunnel. The CLI unregisters the companion when the command exits. -- Remote bridge React DevTools assumes the React Native-bundled DevTools behavior in React Native 0.83+. Older browser/Chromium DevTools workflows are not assumed to exist inside remote sandboxes. Expo projects should be verified against the SDK's bundled React Native version before relying on this path; this release does not claim a separately verified Expo SDK version. +- For Android sessions connected through a remote/cloud profile, `react-devtools` registers a lease-scoped companion tunnel to the sandbox-local DevTools daemon at `127.0.0.1:8097`. The remote service owns device-side routing and the CLI unregisters the companion when the command exits. Remote iOS is not available for React DevTools in the current cloud service; use normal device evidence or local iOS when React internals are required. +- Remote React DevTools assumes the React Native-bundled DevTools behavior in React Native 0.83+. Older browser/Chromium DevTools workflows are not assumed to exist inside remote sandboxes. Expo projects should be verified against the SDK's bundled React Native version before relying on this path; this release does not claim a separately verified Expo SDK version. - For cross-platform validation with explicit target selectors, prefer an isolated `--state-dir` over separate named sessions. Named sessions enable bound-session locks during setup. Restart `react-devtools` between iOS and Android runs. ## Metro reload @@ -716,7 +716,7 @@ Example `agent-device.remote.json`: ```json { - "daemonBaseUrl": "https://bridge.example.com/agent-device", + "daemonBaseUrl": "https://cloud.example.com/agent-device", "daemonTransport": "http", "tenant": "acme", "runId": "run-123", @@ -725,7 +725,7 @@ Example `agent-device.remote.json`: "platform": "ios", "leaseBackend": "ios-instance", "metroProjectRoot": ".", - "metroProxyBaseUrl": "https://bridge.example.com" + "metroProxyBaseUrl": "https://cloud.example.com" } ``` @@ -749,15 +749,15 @@ agent-device disconnect --remote-config ./agent-device.remote.json - `connect --remote-config ...` is the main agent flow. It generates a local session name when needed, authenticates to cloud when credentials are missing, stores the remote scope locally, and defers tenant lease allocation plus Metro preparation until a later command needs them. - Auth management commands are available for inspection and recovery: `agent-device auth status`, `agent-device auth login`, and `agent-device auth logout`. Human login stores a revocable CLI session locally; it does not create or persist an `adc_live_...` service token. - Cloud auth uses three credential classes: `adc_agent_...` short-lived command tokens, revocable CLI session refresh credentials, and explicit `adc_live_...` service/API tokens for CI. The CLI implements credential selection, CI refusal, local storage permissions, logout, and output redaction; the cloud API must enforce token expiry, tenant/run scope, revocation, one-time device approval, polling rate limits, and dashboard/API separation. -- `AGENT_DEVICE_CLOUD_BASE_URL` should point at the bridge/control-plane API origin, not necessarily the dashboard origin. API-token setup links use `/api-keys` on that origin so the bridge can redirect users to the right dashboard page. +- `AGENT_DEVICE_CLOUD_BASE_URL` should point at the cloud/control-plane API origin, not necessarily the dashboard origin. API-token setup links use `/api-keys` on that origin so the service can redirect users to the right dashboard page. - Deferred Metro preparation also applies to `batch` when any step opens an app and the batch does not provide its own per-step runtime. - After `connect`, `install-from-source`, `open`, `snapshot`, `devices`, `press`, `fill`, `screenshot`, and other normal commands reuse active connection state so agents do not repeat remote host/session/lease selectors inline. If `connection status` shows `leaseId=pending`, the first platform-bound command allocates or refreshes the lease. Passing the same `--remote-config` to a normal command is also supported for self-contained scripts; the CLI reuses matching saved state or creates it before dispatch. - Self-contained remote scripts should end with `disconnect --remote-config ` or `disconnect` to release the lease and stop the owned Metro companion. - Explicit command-line flags override connected defaults. When `open` uses explicit remote daemon or tenant flags without saved runtime hints, the CLI warns because React Native apps may launch without Metro bundle/runtime hints. -- `metroProxyBaseUrl` is the bridge origin. Do not prebuild `/api/metro/...` paths in the client profile; the CLI calls the bridge endpoints itself. -- For cloud stock React Native iOS, the bridge descriptor supplies direct wildcard HTTPS Metro hints such as `.metro.agent-device.dev:443`. The XCTest runner package is still used for runner-backed device commands, not for Metro reachability. -- Android keeps using bridge-provided runtime routes such as `/api/metro/runtimes//...`. -- `metroPublicBaseUrl` is only needed for direct/non-bridge bundle hints. Bridged profiles can omit it and rely on `metroProxyBaseUrl`. +- `metroProxyBaseUrl` is the remote service origin. Do not prebuild `/api/metro/...` paths in the client profile; the CLI calls the cloud service endpoints itself. +- For cloud stock React Native iOS, the remote service descriptor supplies direct wildcard HTTPS Metro hints such as `.metro.agent-device.dev:443`. The XCTest runner package is still used for runner-backed device commands, not for Metro reachability. +- Android keeps using service-provided runtime routes such as `/api/metro/runtimes//...`. +- `metroPublicBaseUrl` is only needed for direct/local bundle hints. Remote profiles can omit it and rely on `metroProxyBaseUrl`. - `metro prepare --remote-config ...` remains an advanced inspection/debug path and can still write a `--runtime-file ` artifact when needed. - The local Metro companion runs on the same machine as the React Native project and Metro. `disconnect` stops the companion owned by the connection, but it does not stop the user’s Metro server.