Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,8 @@ Diagnostics files:
- Physical iOS device capture is best-effort: dropped frames are expected and true 60 FPS is not guaranteed even with `--fps 60`.
- Physical iOS device recording defaults to uncapped (max available) FPS.
- Use `agent-device record start [path] --fps <n>` (1-120) to set an explicit FPS cap on physical iOS devices.
- iOS device runs require valid signing/provisioning (Automatic Signing recommended). Optional overrides: `AGENT_DEVICE_IOS_TEAM_ID`, `AGENT_DEVICE_IOS_SIGNING_IDENTITY`, `AGENT_DEVICE_IOS_PROVISIONING_PROFILE`.
- iOS device runs require valid signing/provisioning (Automatic Signing recommended). Optional overrides: `AGENT_DEVICE_IOS_TEAM_ID`, `AGENT_DEVICE_IOS_SIGNING_IDENTITY`, `AGENT_DEVICE_IOS_PROVISIONING_PROFILE`, `AGENT_DEVICE_IOS_BUNDLE_ID`.
- Free Apple Developer (Personal Team) accounts may need a unique runner bundle id; set `AGENT_DEVICE_IOS_BUNDLE_ID` to a reverse-DNS identifier unique to your team (for example `com.yourname.agentdevice.runner`).

## Testing

Expand Down Expand Up @@ -517,6 +518,7 @@ Environment selectors:
- `AGENT_DEVICE_IOS_TEAM_ID=<team-id>` optional Team ID override for iOS device runner signing.
- `AGENT_DEVICE_IOS_SIGNING_IDENTITY=<identity>` optional signing identity override.
- `AGENT_DEVICE_IOS_PROVISIONING_PROFILE=<profile>` optional provisioning profile specifier for iOS device runner signing.
- `AGENT_DEVICE_IOS_BUNDLE_ID=<reverse-dns-id>` optional iOS runner app bundle id base. Tests derive from this as `<id>.uitests`.
- `AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH=<path>` optional override for iOS runner derived data root. By default, simulator uses `~/.agent-device/ios-runner/derived` and physical device uses `~/.agent-device/ios-runner/derived/device`. If you set this override, use separate paths per kind to avoid simulator/device artifact collisions.
- `AGENT_DEVICE_IOS_CLEAN_DERIVED=1` rebuild iOS runner artifacts from scratch for runtime daemon-triggered builds (`pnpm ad ...`) on the selected path. `pnpm build:xcuitest` (alias of `pnpm build:xcuitest:ios`), `pnpm build:xcuitest:tvos`, and `pnpm build:all` already clear their default derived paths and do not require this variable. When `AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH` is set, cleanup is blocked by default; set `AGENT_DEVICE_IOS_ALLOW_OVERRIDE_DERIVED_CLEAN=1` only for trusted custom paths.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID = com.callstack.agentdevice.runner;
AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID = "$(AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID).uitests";
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
Expand Down Expand Up @@ -268,6 +270,8 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID = com.callstack.agentdevice.runner;
AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID = "$(AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID).uitests";
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
Expand Down Expand Up @@ -342,7 +346,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.myapp.AgentDeviceRunner;
PRODUCT_BUNDLE_IDENTIFIER = "$(AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
Expand Down Expand Up @@ -377,7 +381,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.myapp.AgentDeviceRunner;
PRODUCT_BUNDLE_IDENTIFIER = "$(AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
Expand All @@ -400,7 +404,7 @@
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.myapp.AgentDeviceRunnerUITests;
PRODUCT_BUNDLE_IDENTIFIER = "$(AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
Expand All @@ -424,7 +428,7 @@
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.myapp.AgentDeviceRunnerUITests;
PRODUCT_BUNDLE_IDENTIFIER = "$(AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
Expand Down
6 changes: 5 additions & 1 deletion skills/agent-device/references/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Use Automatic Signing in Xcode, or provide optional overrides:
- `AGENT_DEVICE_IOS_TEAM_ID`
- `AGENT_DEVICE_IOS_SIGNING_IDENTITY`
- `AGENT_DEVICE_IOS_PROVISIONING_PROFILE`
- `AGENT_DEVICE_IOS_BUNDLE_ID` (optional runner bundle-id base override)

Free Apple Developer (Personal Team) accounts may reject generic bundle IDs as unavailable.
Set `AGENT_DEVICE_IOS_BUNDLE_ID` to a unique reverse-DNS identifier when that happens.

Security guidance for these overrides:

Expand All @@ -23,7 +27,7 @@ Security guidance for these overrides:

If setup/build takes long, increase:

- `AGENT_DEVICE_DAEMON_TIMEOUT_MS` (default `45000`, for example `120000`)
- `AGENT_DEVICE_DAEMON_TIMEOUT_MS` (default `90000`, for example `120000`)

If daemon startup fails with stale metadata hints, clean stale files and retry:

Expand Down
3 changes: 2 additions & 1 deletion src/daemon/handlers/__tests__/record-trace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import path from 'node:path';
import { handleRecordTraceCommands } from '../record-trace.ts';
import { SessionStore } from '../../session-store.ts';
import type { SessionState } from '../../types.ts';
import { IOS_RUNNER_CONTAINER_BUNDLE_IDS } from '../../../platforms/ios/runner-client.ts';

type RecordTraceDeps = NonNullable<Parameters<typeof handleRecordTraceCommands>[0]['deps']>;
type RunnerCall = {
Expand Down Expand Up @@ -160,7 +161,7 @@ test('record start/stop uses iOS runner on physical iOS devices', async () => {
'--domain-type',
'appDataContainer',
'--domain-identifier',
'com.myapp.AgentDeviceRunnerUITests.xctrunner',
IOS_RUNNER_CONTAINER_BUNDLE_IDS[0] ?? '',
]);
assert.equal(sessionStore.get(sessionName)?.recording, undefined);
});
Expand Down
17 changes: 17 additions & 0 deletions src/platforms/ios/__tests__/runner-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
isRetryableRunnerError,
resolveRunnerEarlyExitHint,
resolveRunnerBuildDestination,
resolveRunnerBundleBuildSettings,
resolveRunnerDestination,
resolveRunnerMaxConcurrentDestinationsFlag,
resolveRunnerSigningBuildSettings,
Expand Down Expand Up @@ -124,6 +125,22 @@ test('resolveRunnerSigningBuildSettings applies optional overrides when provided
]);
});

test('resolveRunnerBundleBuildSettings returns default bundle identifiers', () => {
assert.deepEqual(resolveRunnerBundleBuildSettings({}), [
'AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID=com.callstack.agentdevice.runner',
'AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID=com.callstack.agentdevice.runner.uitests',
]);
});

test('resolveRunnerBundleBuildSettings uses AGENT_DEVICE_IOS_BUNDLE_ID when provided', () => {
assert.deepEqual(resolveRunnerBundleBuildSettings({
AGENT_DEVICE_IOS_BUNDLE_ID: 'com.example.agent-device.runner',
}), [
'AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID=com.example.agent-device.runner',
'AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID=com.example.agent-device.runner.uitests',
]);
});

test('assertSafeDerivedCleanup allows cleaning when no override is set', () => {
assert.doesNotThrow(() => {
assertSafeDerivedCleanup('/tmp/derived', {});
Expand Down
60 changes: 51 additions & 9 deletions src/platforms/ios/runner-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,39 @@ import { resolveTimeoutMs, resolveTimeoutSeconds } from '../../utils/timeouts.ts
import { isRequestCanceled } from '../../daemon/request-cancel.ts';
import { buildSimctlArgsForDevice } from './simctl.ts';

const iosRunnerContainerBundleIds = [
process.env.AGENT_DEVICE_IOS_RUNNER_CONTAINER_BUNDLE_ID,
process.env.AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID,
'com.myapp.AgentDeviceRunnerUITests.xctrunner',
'com.myapp.AgentDeviceRunner',
]
.map((id) => id?.trim() ?? '')
.filter((id) => id.length > 0);
const DEFAULT_IOS_RUNNER_APP_BUNDLE_ID = 'com.callstack.agentdevice.runner';

export const IOS_RUNNER_CONTAINER_BUNDLE_IDS: string[] = Array.from(new Set(iosRunnerContainerBundleIds));
function normalizeBundleId(value: string | undefined): string {
return value?.trim() ?? '';
}

function resolveRunnerAppBundleId(env: NodeJS.ProcessEnv = process.env): string {
const configured = normalizeBundleId(env.AGENT_DEVICE_IOS_BUNDLE_ID)
|| normalizeBundleId(env.AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID);
return configured || DEFAULT_IOS_RUNNER_APP_BUNDLE_ID;
}

function resolveRunnerTestBundleId(env: NodeJS.ProcessEnv = process.env): string {
const configured = normalizeBundleId(env.AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID);
if (configured) {
return configured;
}
return `${resolveRunnerAppBundleId(env)}.uitests`;
}

function resolveRunnerContainerBundleIds(env: NodeJS.ProcessEnv = process.env): string[] {
const appBundleId = resolveRunnerAppBundleId(env);
const testBundleId = resolveRunnerTestBundleId(env);
return Array.from(new Set([
normalizeBundleId(env.AGENT_DEVICE_IOS_RUNNER_CONTAINER_BUNDLE_ID),
`${testBundleId}.xctrunner`,
appBundleId,
'com.myapp.AgentDeviceRunnerUITests.xctrunner',
'com.myapp.AgentDeviceRunner',
].filter((id) => id.length > 0)));
}

export const IOS_RUNNER_CONTAINER_BUNDLE_IDS: string[] = resolveRunnerContainerBundleIds(process.env);

type RunnerCommand = {
command:
Expand Down Expand Up @@ -445,6 +468,7 @@ async function ensureXctestrun(
throw new AppError('COMMAND_FAILED', 'iOS runner project not found', { projectPath });
}

const runnerBundleBuildSettings = resolveRunnerBundleBuildSettings(process.env);
const signingBuildSettings = resolveRunnerSigningBuildSettings(process.env, device.kind === 'device');
const provisioningArgs = device.kind === 'device' ? ['-allowProvisioningUpdates'] : [];
try {
Expand All @@ -464,6 +488,7 @@ async function ensureXctestrun(
resolveRunnerBuildDestination(device),
'-derivedDataPath',
derived,
...runnerBundleBuildSettings,
...provisioningArgs,
...signingBuildSettings,
],
Expand Down Expand Up @@ -591,9 +616,26 @@ export function resolveRunnerSigningBuildSettings(
return args;
}

export function resolveRunnerBundleBuildSettings(
env: NodeJS.ProcessEnv = process.env,
): string[] {
const appBundleId = resolveRunnerAppBundleId(env);
const testBundleId = resolveRunnerTestBundleId(env);
return [
`AGENT_DEVICE_IOS_RUNNER_APP_BUNDLE_ID=${appBundleId}`,
`AGENT_DEVICE_IOS_RUNNER_TEST_BUNDLE_ID=${testBundleId}`,
];
}

function resolveSigningFailureHint(error: AppError): string | undefined {
const details = error.details ? JSON.stringify(error.details) : '';
const combined = `${error.message}\n${details}`.toLowerCase();
if (
combined.includes('failed registering bundle identifier')
|| (combined.includes('app identifier') && combined.includes('not available'))
) {
return 'Set AGENT_DEVICE_IOS_BUNDLE_ID to a unique reverse-DNS value (for example, com.yourname.agentdevice.runner), then retry.';
}
if (combined.includes('requires a development team')) {
return 'Configure signing in Xcode or set AGENT_DEVICE_IOS_TEAM_ID for physical-device runs.';
}
Expand Down
2 changes: 2 additions & 0 deletions website/docs/docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ tail -50 ~/.agent-device/sessions/default/app.log
- `AGENT_DEVICE_IOS_TEAM_ID`
- `AGENT_DEVICE_IOS_SIGNING_IDENTITY` (optional)
- `AGENT_DEVICE_IOS_PROVISIONING_PROFILE`
- `AGENT_DEVICE_IOS_BUNDLE_ID` (runner bundle-id base; tests use `<id>.uitests`)
- Free Apple Developer (Personal Team) accounts can fail on unavailable generic bundle IDs; set `AGENT_DEVICE_IOS_BUNDLE_ID` to a unique reverse-DNS value.
- If first-run XCTest setup/build is slow, increase daemon request timeout:
- `AGENT_DEVICE_DAEMON_TIMEOUT_MS=120000` (default is `90000`)
- For daemon startup troubleshooting:
Expand Down
2 changes: 2 additions & 0 deletions website/docs/docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ npx agent-device open Settings --platform ios
- `AGENT_DEVICE_IOS_TEAM_ID`
- `AGENT_DEVICE_IOS_SIGNING_IDENTITY`
- `AGENT_DEVICE_IOS_PROVISIONING_PROFILE`
- `AGENT_DEVICE_IOS_BUNDLE_ID` (optional runner bundle-id base override)
- Free Apple Developer (Personal Team) accounts can fail with "bundle identifier is not available" for generic IDs; set `AGENT_DEVICE_IOS_BUNDLE_ID` to a unique reverse-DNS value (for example `com.yourname.agentdevice.runner`).
- If device setup is slow, increase daemon timeout:
- `AGENT_DEVICE_DAEMON_TIMEOUT_MS=120000` (default is `90000`)
- If daemon startup reports stale metadata, remove stale files and retry:
Expand Down
Loading