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
108 changes: 108 additions & 0 deletions src/platforms/apple/core/__tests__/runner-macos-products.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';

import { test } from 'vitest';

import { MACOS_DEVICE } from '../../../../__tests__/test-utils/device-fixtures.ts';
import { mkdtempForTestSync } from '../../../../__tests__/test-utils/tmp-dir.ts';
import { AppError } from '@agent-device/kernel/errors';
import {
isExpectedRunnerRepairFailure,
repairMacOsRunnerProductsIfNeeded,
} from '../runner/runner-macos-products.ts';
import { createLocalAppleToolProvider, withAppleToolProvider } from '../tool-provider.ts';

const XCTESTRUN_PATH = '/tmp/agent-device-runner.xctestrun';

test('repair discovers and re-signs nested code before the product', async () => {
const { productPath, embeddedItemPaths } = createProduct();
const calls: string[][] = [];
const provider = createCodesignProvider(
calls,
(args) => args.includes('--sign') && args.at(-1) === productPath,
);

await withAppleToolProvider(provider, async () => {
await repairMacOsRunnerProductsIfNeeded(MACOS_DEVICE, [productPath], XCTESTRUN_PATH);
});

const signingArgs = [
'--force',
'--preserve-metadata=identifier,entitlements,flags,runtime',
'--sign',
'-',
];
assert.deepEqual(calls.slice(1, -1), [
...embeddedItemPaths.map((itemPath) => [...signingArgs, itemPath]),
[...signingArgs, productPath],
]);
assert.deepEqual(calls.at(-1), ['--verify', '--deep', '--strict', productPath]);
});

test('repair fails when re-signing leaves the product unverifiable', async () => {
const productPath = createProductPath();
const provider = createCodesignProvider([], () => false);

const error = await withAppleToolProvider(
provider,
async () =>
await repairMacOsRunnerProductsIfNeeded(MACOS_DEVICE, [productPath], XCTESTRUN_PATH).then(
() => null,
(thrown: unknown) => thrown,
),
);

assert.ok(error instanceof AppError);
assert.equal(isExpectedRunnerRepairFailure(error), true);
});

function createProductPath(): string {
return createProduct().productPath;
}

function createProduct(): {
productPath: string;
embeddedItemPaths: string[];
} {
const root = mkdtempForTestSync('agent-device-runner-products-');
const productPath = path.join(root, 'AgentDeviceRunnerUITests-Runner.app');
fs.mkdirSync(productPath);
const frameworksRoot = path.join(productPath, 'Contents', 'Frameworks');
const embeddedItemPaths = [
'FutureTestSupport.framework',
'AnotherTestSupport.framework',
'libFutureTestSupport.dylib',
].map((itemName) => path.join(frameworksRoot, itemName));
for (const itemPath of embeddedItemPaths) {
if (itemPath.endsWith('.framework')) {
fs.mkdirSync(itemPath, { recursive: true });
} else {
fs.mkdirSync(path.dirname(itemPath), { recursive: true });
fs.writeFileSync(itemPath, '');
}
}
return { productPath, embeddedItemPaths: embeddedItemPaths.sort() };
}

function createCodesignProvider(
calls: string[][],
isRepairingSignature: (args: readonly string[]) => boolean,
) {
let repaired = false;
return createLocalAppleToolProvider({
runCommand: async (command, args = []) => {
assert.equal(command, 'codesign');
calls.push([...args]);
if (isRepairingSignature(args)) {
repaired = true;
}
const verifying = args[0] === '--verify';
return {
exitCode: verifying && !repaired ? 1 : 0,
stdout: '',
stderr: '',
};
},
});
}
65 changes: 51 additions & 14 deletions src/platforms/apple/core/runner/runner-macos-products.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fs from 'node:fs';
import path from 'node:path';
import { isMacOs, type DeviceInfo } from '@agent-device/kernel/device';
import { AppError, asAppError } from '@agent-device/kernel/errors';
import { runAppleToolCommand } from '../tool-provider.ts';
Expand All @@ -8,6 +9,14 @@ const RUNNER_PRODUCT_REPAIR_FAILURE_REASONS = new Set([
'RUNNER_PRODUCT_REPAIR_FAILED',
]);

const AD_HOC_RESIGN_ARGS = [
'--force',
// Designated requirements must be regenerated for the ad-hoc identity.
'--preserve-metadata=identifier,entitlements,flags,runtime',
'--sign',
'-',
] as const;

export async function repairMacOsRunnerProductsIfNeeded(
device: DeviceInfo,
productPaths: string[],
Expand Down Expand Up @@ -39,24 +48,52 @@ export async function repairMacOsRunnerProductsIfNeeded(
if (await hasValidCodeSignature(productPath)) {
continue;
}
await runAppleToolCommand('codesign', ['--remove-signature', productPath], {
allowFailure: true,
});
try {
await runAppleToolCommand('codesign', ['--force', '--sign', '-', productPath]);
} catch (error) {
const appError = asAppError(error, 'COMMAND_FAILED');
throw new AppError('COMMAND_FAILED', 'Failed to repair macOS runner product signature', {
reason: 'RUNNER_PRODUCT_REPAIR_FAILED',
productPath,
xctestrunPath,
error: appError.message,
details: appError.details,
});
await resignRunnerProduct(productPath, xctestrunPath);
}
}

async function resignRunnerProduct(productPath: string, xctestrunPath: string): Promise<void> {
try {
const frameworksPath = path.join(productPath, 'Contents', 'Frameworks');
const embeddedCodePaths = fs.existsSync(frameworksPath)
? fs
.readdirSync(frameworksPath)
.sort()
.map((itemName) => path.join(frameworksPath, itemName))
: [];
for (const embeddedCodePath of embeddedCodePaths) {
await runAppleToolCommand('codesign', [...AD_HOC_RESIGN_ARGS, embeddedCodePath]);
}
await runAppleToolCommand('codesign', [...AD_HOC_RESIGN_ARGS, productPath]);
} catch (error) {
const appError = asAppError(error, 'COMMAND_FAILED');
throw repairFailure(productPath, xctestrunPath, appError.message, appError.details);
}

if (!(await hasValidCodeSignature(productPath))) {
throw repairFailure(
productPath,
xctestrunPath,
'Product still fails code signature verification after re-signing',
);
}
}

function repairFailure(
productPath: string,
xctestrunPath: string,
error: string,
details?: unknown,
): AppError {
return new AppError('COMMAND_FAILED', 'Failed to repair macOS runner product signature', {
reason: 'RUNNER_PRODUCT_REPAIR_FAILED',
productPath,
xctestrunPath,
error,
details,
});
}

export function isExpectedRunnerRepairFailure(error: unknown): boolean {
if (!(error instanceof AppError)) {
return false;
Expand Down
Loading